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,6 +2,7 @@
2
2
  # Ensures .devflow/ and all subdirectories exist and the project root .gitignore
3
3
  # ignores .devflow/. Called from capture-prompt, capture-turn, capture-question,
4
4
  # memory-worker, and pre-compact-memory. Idempotent.
5
+ # Returns non-zero, having written nothing, outside a git project or at HOME.
5
6
  # Usage: source ensure-devflow-init "$CWD"
6
7
 
7
8
  [ -z "$1" ] && return 1
@@ -11,23 +12,49 @@
11
12
  # by a hook or directly by tests). Empty → fall back to "$1" (old behavior).
12
13
  _EDI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
14
  source "$_EDI_DIR/resolve-project-root" 2>/dev/null || true
14
- _EDI_ROOT="$(df_resolve_root "$1" 2>/dev/null || true)"
15
- [ -n "$_EDI_ROOT" ] || _EDI_ROOT="$1"
15
+ df_resolve_roots "$1" 2>/dev/null || true
16
+ _EDI_ROOT="${DF_ROOT:-$1}"
17
+ _EDI_LEDGER="${DF_LEDGER_ROOT:-$_EDI_ROOT}"
18
+
19
+ # D-HOOKS-GIT-ONLY (git-marker): scaffold nothing outside a git project, and
20
+ # nothing at a repository rooted at HOME. D-HOOKS-TOPLEVEL-ONLY (git-marker): nor
21
+ # below a checkout's toplevel — when git refuses the cwd (dubious ownership,
22
+ # GIT_CEILING_DIRECTORIES) the root above is that cwd, and a subdirectory holds no
23
+ # `.git` of its own, so it is refused rather than scaffolded. This is the one gate every capture and
24
+ # memory caller (capture-prompt, capture-turn, capture-question, memory-worker,
25
+ # pre-compact-memory) passes through before it writes, and each of them treats a
26
+ # non-zero return as "stop" (`|| exit 0`), so memory and learning stop together
27
+ # in a non-git directory. ensure-root-gitignore itself stays ungated: it is a
28
+ # pure writer its parity suite runs in plain directories (PF-059), and both of
29
+ # its callers now decide before reaching it. A helper that failed to source is a
30
+ # command not found — the refusing branch, so the failure mode writes nothing.
31
+ source "$_EDI_DIR/git-marker" 2>/dev/null || true
32
+ df_is_project_root "$_EDI_ROOT" 2>/dev/null || return 1
16
33
 
17
34
  _DEVFLOW_DIR="$_EDI_ROOT/.devflow"
18
35
 
19
- # Fast-path: if all subdirectories already exist, skip mkdir and gitignore setup
36
+ # learning/ is scaffolded only where the ledger lives. In a linked worktree whose
37
+ # main checkout holds the ledger (D-LEDGER-MAIN-WORKTREE, resolve-project-root) a
38
+ # learning/ here would be an empty directory nothing writes to; the capture hooks
39
+ # create the ledger's own learning/ at the ledger root when they append to it.
40
+ _EDI_LEARNING="$_DEVFLOW_DIR/learning"
41
+ [ "$_EDI_LEDGER" = "$_EDI_ROOT" ] || _EDI_LEARNING=""
42
+
43
+ # Fast-path: if all subdirectories already exist, skip mkdir and gitignore setup.
44
+ # The marker is the CURRENT carve-out version (D-GITIGNORE-V6), bumped with both
45
+ # writers' stamps, so a v5-stamped project misses this once and gains the
46
+ # `!.devflow/project.json` line.
20
47
  if [ -d "$_DEVFLOW_DIR/memory" ] && [ -d "$_DEVFLOW_DIR/docs" ] && \
21
- [ -d "$_DEVFLOW_DIR/learning" ] && \
48
+ { [ -z "$_EDI_LEARNING" ] || [ -d "$_EDI_LEARNING" ]; } && \
22
49
  [ -d "$_DEVFLOW_DIR/features" ] && \
23
- [ -f "$_DEVFLOW_DIR/.root-gitignore-configured-v2" ]; then
50
+ [ -f "$_DEVFLOW_DIR/.root-gitignore-configured-v6" ]; then
24
51
  return 0
25
52
  fi
26
53
 
27
54
  # Create all subdirectories
28
55
  mkdir -p \
29
56
  "$_DEVFLOW_DIR/memory" \
30
- "$_DEVFLOW_DIR/learning" \
57
+ ${_EDI_LEARNING:+"$_EDI_LEARNING"} \
31
58
  "$_DEVFLOW_DIR/features" \
32
59
  "$_DEVFLOW_DIR/docs" \
33
60
  2>/dev/null || return 1
@@ -3,7 +3,7 @@
3
3
  # ensure-proxy — SessionStart + UserPromptSubmit hook
4
4
  # Ensures the Devflow proxy relay is running when external model routing is enabled.
5
5
  # NOT git-gated (proxy is a global user-scope feature, not per-project).
6
- # NOT project-scoped: proxy state lives at $DEVFLOW_DIR/proxy.json (user-scope).
6
+ # NOT project-scoped: proxy state lives at $HOME/.devflow/proxy.json (machine-wide).
7
7
  #
8
8
  # SessionStart: probe port → if DOWN attempt spawn → inject additionalContext warning if still down
9
9
  # UserPromptSubmit: fast exit (both port-up and port-down) — silent; SessionStart handles all warnings
@@ -55,9 +55,10 @@ if [ "$HOOK_EVENT" = "UserPromptSubmit" ]; then
55
55
  fi
56
56
 
57
57
  # ── Proxy state ────────────────────────────────────────────────────────────────
58
- # Proxy is user-scope (global), not project-scoped. Use $DEVFLOW_DIR (env) or default.
59
- DEVFLOW_DIR="${DEVFLOW_DIR:-$HOME/.devflow}"
60
- PROXY_STATE_FILE="$DEVFLOW_DIR/proxy.json"
58
+ # Proxy is machine-wide, not project-scoped: state lives at the machine root,
59
+ # always $HOME/.devflow (D-ONE-HOME).
60
+ MACHINE_DEVFLOW_DIR="$HOME/.devflow"
61
+ PROXY_STATE_FILE="$MACHINE_DEVFLOW_DIR/proxy.json"
61
62
 
62
63
  if [ ! -f "$PROXY_STATE_FILE" ]; then
63
64
  dbg "EXIT: no proxy state file"
@@ -93,7 +94,7 @@ if [ "${DEVFLOW_HOOK_DEBUG:-}" = "1" ]; then
93
94
  fi
94
95
 
95
96
  # ── Log setup (SessionStart only) ──────────────────────────────────────────────
96
- LOG_DIR="$DEVFLOW_DIR/logs"
97
+ LOG_DIR="$MACHINE_DEVFLOW_DIR/logs"
97
98
  mkdir -p "$LOG_DIR" 2>/dev/null || true
98
99
  # SEC-2: harden log directory to 0700 (best-effort; pre-existing dirs may be wider).
99
100
  chmod 700 "$LOG_DIR" 2>/dev/null || true
@@ -101,7 +102,7 @@ LOG_FILE="$LOG_DIR/proxy.log"
101
102
 
102
103
  # Size guard: 2MB max → truncate to 1MB tail (matches hook-log-init guard pattern).
103
104
  # Sharing hook-log-init is not feasible here: hook-log-init requires $CWD and targets
104
- # the per-project log path; ensure-proxy uses the user-scope $DEVFLOW_DIR/logs path.
105
+ # the per-project log path; ensure-proxy uses the machine-wide $MACHINE_DEVFLOW_DIR/logs path.
105
106
  # The existence guard is required: wc -c uses a shell redirect whose failure is emitted
106
107
  # by bash itself (before wc starts), bypassing 2>/dev/null.
107
108
  _LOG_MAX_BYTES=2097152
@@ -268,7 +269,7 @@ fi
268
269
  source "$SCRIPT_DIR/get-mtime" 2>/dev/null || true
269
270
  source "$SCRIPT_DIR/learning-lock" 2>/dev/null || true
270
271
 
271
- SPAWN_LOCK="$DEVFLOW_DIR/.proxy-spawn.lock"
272
+ SPAWN_LOCK="$MACHINE_DEVFLOW_DIR/.proxy-spawn.lock"
272
273
 
273
274
  if ! learning_lock_acquire "$SPAWN_LOCK" 2 2>/dev/null; then
274
275
  # Another session is racing to start the proxy — give it a moment then re-probe
@@ -324,7 +325,7 @@ log "relay spawned with pid $_RELAY_PID"
324
325
  # Best-effort pid record for `devflow proxy --status` — mirrors the CLI enable path.
325
326
  # Written unconditionally after spawn (like the CLI): a stale pid from a relay that
326
327
  # never came up is harmless, since --status liveness-checks it before display.
327
- printf '%s' "$_RELAY_PID" > "$DEVFLOW_DIR/proxy.pid" 2>/dev/null || \
328
+ printf '%s' "$_RELAY_PID" > "$MACHINE_DEVFLOW_DIR/proxy.pid" 2>/dev/null || \
328
329
  log "warn: could not write proxy.pid (non-fatal)"
329
330
 
330
331
  # Bounded wait: 80×0.1s = 8s maximum (well within 15s hook timeout)
@@ -3,14 +3,19 @@
3
3
  # rules that govern .devflow/.
4
4
  #
5
5
  # .devflow/ holds per-developer runtime state (memory, learning, docs, locks) —
6
- # local by default. TWO exceptions are shared via git:
6
+ # local by default. FOUR exceptions are shared via git:
7
7
  # 1. Feature knowledge bases under .devflow/features/: index.md and every
8
8
  # {slug}/KNOWLEDGE.md (tracked, committed by the Knowledge agent at workflow end).
9
9
  # 2. .devflow/conventions.md — naming-convention authority written by the Git
10
10
  # learn-conventions operation; shared so the team uses one naming source.
11
+ # 3. .devflow/policy.json — the retired evidence-policy file: never parsed, but
12
+ # where project.json has no `evidence` its presence holds the repository at
13
+ # required (D-POLICY-JSON-RETIRED). Devflow never writes it.
14
+ # 4. .devflow/project.json — the team-committed settings both resolvers read
15
+ # (D-GITIGNORE-V6). Devflow never writes it.
11
16
  #
12
- # Everything else under .devflow/ stays local. A user opts back out by re-adding
13
- # `.devflow/features/` or `.devflow/conventions.md` to their own .gitignore.
17
+ # Everything else under .devflow/ stays local. A user opts back out of the first two
18
+ # by re-adding `.devflow/features/` or `.devflow/conventions.md` to their own .gitignore.
14
19
  #
15
20
  # Re-including files under an ignored tree requires un-ignoring each level with a
16
21
  # `dir/*` + `!dir/keep` pair: a bare `.devflow/` excludes the directory outright,
@@ -23,9 +28,35 @@
23
28
  # Both reach this one writer so the rule is applied identically everywhere; this
24
29
  # decouples git-tracking of .devflow/ from any single feature toggle (avoids PF-014).
25
30
  #
26
- # Idempotent and O(1) after the first run via the .root-gitignore-configured-v3
27
- # marker. The marker is versioned: bumping it (v2 → v3) forces existing installs
28
- # to re-run once and upgrade their block (adds !.devflow/conventions.md line).
31
+ # Idempotent and O(1) after the first run via the .root-gitignore-configured-v6
32
+ # marker under .devflow/ (project-local). The marker is a claim, not proof, so the
33
+ # fast path also requires the block's own sentinel, the policy line, the project
34
+ # line AND a .claudeignore entry to be present; a .gitignore that lost any of them (a
35
+ # merge resolution, a hand edit) is healed on the next run. A v5-marked project
36
+ # misses the fast path, gains the project line just before its .claudeignore line
37
+ # and is re-stamped v6 (D-GITIGNORE-V6); a v4-marked one gains the policy and
38
+ # project lines.
39
+ #
40
+ # D-GITIGNORE-IN-BLOCK: a line topped up into an existing block goes INSIDE the
41
+ # block, where a fresh block holds it, never at the end of the file. gitignore is
42
+ # last-match-wins, so a `!.devflow/project.json` appended after a user's own later
43
+ # `.devflow/project.json` would silently override their re-ignore. The missing
44
+ # lines are inserted as one run, in block order, right after the block sentinel
45
+ # and the block lines that precede the first missing one in a fresh block (at most
46
+ # three: the sentinel, the policy line, the project line). Every other byte of the
47
+ # file is kept.
48
+ #
49
+ # D-GITIGNORE-V5 / D-GITIGNORE-V6: the block is detected ONLY by its devflow-unique
50
+ # sentinel `!.devflow/conventions.md` — NEVER by `.claudeignore`,
51
+ # `!.devflow/policy.json` or `!.devflow/project.json`, which users legitimately author
52
+ # themselves. Those three are completion lines: each is topped up when missing, in
53
+ # block order (policy, project, then .claudeignore). Keying presence
54
+ # off a user-authored line inverts both halves of the contract: projects that already
55
+ # carry that line never receive the carve-out (so .devflow/ runtime files leak into
56
+ # git), and re-appending `.claudeignore` after a user's `!.claudeignore` reverses
57
+ # their intent under gitignore's last-match-wins. Hence: a `.claudeignore` OR
58
+ # `!.claudeignore` entry means "the user owns that line" — emit the block without it.
59
+ # All matching is whole-line and whitespace-tolerant; never substring.
29
60
  #
30
61
  # Usage: source ensure-root-gitignore "$PROJECT_ROOT"
31
62
  # Sourced helper: uses `return` (never exit), _ERG_-prefixed locals (never clobbers
@@ -34,31 +65,62 @@
34
65
  [ -z "$1" ] && return 1
35
66
 
36
67
  _ERG_DEVFLOW_DIR="$1/.devflow"
37
- _ERG_MARKER="$_ERG_DEVFLOW_DIR/.root-gitignore-configured-v3"
68
+ _ERG_MARKER="$_ERG_DEVFLOW_DIR/.root-gitignore-configured-v6"
38
69
  _ERG_GITIGNORE="$1/.gitignore"
39
70
 
40
- # Fast-path with verification: marker normally means the block is installed, but
41
- # the marker is a claim, not proof — a merge-conflict resolution may have dropped
42
- # the block. Gate the fast-path return on the v3 sentinel actually being present
43
- # in .gitignore. Idempotent: sentinel present → return 0; sentinel absent → heal.
44
- if [ -f "$_ERG_MARKER" ]; then
45
- grep -qF '!.devflow/conventions.md' "$_ERG_GITIGNORE" 2>/dev/null && return 0
46
- # Sentinel absent — marker is stale; fall through to re-apply the block.
71
+ # Whole-line, whitespace-tolerant matchers. `*` and `.` are escaped so the ERE
72
+ # matches the literal gitignore patterns.
73
+ _ERG_RE_OPTOUT='^[[:space:]]*/\.devflow/[[:space:]]*$'
74
+ _ERG_RE_LEGACY='^[[:space:]]*\.devflow/[[:space:]]*$'
75
+ _ERG_RE_SENTINEL='^[[:space:]]*!\.devflow/conventions\.md[[:space:]]*$'
76
+ _ERG_RE_SENTINEL_V2='^[[:space:]]*!\.devflow/features/\*/KNOWLEDGE\.md[[:space:]]*$'
77
+ _ERG_RE_POLICY='^[[:space:]]*!\.devflow/policy\.json[[:space:]]*$'
78
+ _ERG_RE_PROJECT='^[[:space:]]*!\.devflow/project\.json[[:space:]]*$'
79
+ _ERG_RE_CLAUDEIGNORE='^[[:space:]]*!?\.claudeignore[[:space:]]*$'
80
+
81
+ # Drop every legacy marker beside v6 — builtin `[ -e ]` tests, so rm forks only for a
82
+ # marker that exists. Called from the fast path and from the on-success stamp below,
83
+ # so an older devflow's marker is dropped whichever path a run takes.
84
+ # Always returns 0 (never fails the caller under `set -e`): the last iteration's
85
+ # `[ -e ]` test is false whenever the unversioned marker is absent (the common case),
86
+ # and a function call's own exit status is NOT exempt from errexit the way an inlined
87
+ # for-loop's is — unlike this same loop when it sat directly inside the caller's `{ }`.
88
+ _erg_drop_legacy_markers() {
89
+ for _ERG_OLD in -v5 -v4 -v3 -v2 ''; do
90
+ [ -e "$_ERG_DEVFLOW_DIR/.root-gitignore-configured$_ERG_OLD" ] \
91
+ && rm -f "$_ERG_DEVFLOW_DIR/.root-gitignore-configured$_ERG_OLD" 2>/dev/null
92
+ done
93
+ return 0
94
+ }
95
+
96
+ # Fast path: converged only when the marker is stamped AND the block sentinel is
97
+ # present AND the policy and project lines are present AND a .claudeignore entry
98
+ # exists. Every other state falls through and recomputes.
99
+ if [ -f "$_ERG_MARKER" ] \
100
+ && grep -qE "$_ERG_RE_SENTINEL" "$_ERG_GITIGNORE" 2>/dev/null \
101
+ && grep -qE "$_ERG_RE_POLICY" "$_ERG_GITIGNORE" 2>/dev/null \
102
+ && grep -qE "$_ERG_RE_PROJECT" "$_ERG_GITIGNORE" 2>/dev/null \
103
+ && grep -qE "$_ERG_RE_CLAUDEIGNORE" "$_ERG_GITIGNORE" 2>/dev/null; then
104
+ _erg_drop_legacy_markers
105
+ return 0
47
106
  fi
48
107
 
49
108
  # The marker lives under .devflow/, so the directory must exist before we touch it.
50
109
  # (ensure-devflow-init creates it earlier; session-start-context may reach here first.)
51
110
  mkdir -p "$_ERG_DEVFLOW_DIR" 2>/dev/null || return 1
52
111
 
53
- # The carve-out block, built once into _ERG_BLOCK (emitted on create and append).
54
- # Keep byte-identical to ensureDevflowGitignore in src/targets/claude-code/post-install.ts.
55
- # D-GITIGNORE-V3: v3 adds !.devflow/conventions.md (naming authority, git-tracked).
56
- printf -v _ERG_BLOCK '%s\n' \
112
+ # The carve-out block, built once. _ERG_BLOCK_NO_CI is the block a project that owns
113
+ # its own .claudeignore entry receives; _ERG_BLOCK is that plus the final line.
114
+ # Both trail a newline. Keep byte-identical to DEVFLOW_GITIGNORE_BLOCK and
115
+ # DEVFLOW_GITIGNORE_BLOCK_WITHOUT_CLAUDEIGNORE in
116
+ # src/targets/claude-code/post-install.ts.
117
+ printf -v _ERG_BLOCK_NO_CI '%s\n' \
57
118
  '# Devflow runtime data — local by default (memory, learning, docs, locks).' \
58
- '# Two exceptions are shared via git: feature knowledge bases under .devflow/features/' \
59
- '# (index.md and every {slug}/KNOWLEDGE.md) and .devflow/conventions.md (naming' \
60
- '# authority). To stop sharing, re-add `.devflow/features/` or `.devflow/conventions.md`' \
61
- '# to your own .gitignore.' \
119
+ '# Shared via git: feature knowledge bases under .devflow/features/ (index.md and' \
120
+ '# every {slug}/KNOWLEDGE.md), .devflow/conventions.md (naming authority),' \
121
+ '# .devflow/policy.json (retired; presence only) and .devflow/project.json (team settings).' \
122
+ '# To stop sharing the first two, re-add `.devflow/features/` or' \
123
+ '# `.devflow/conventions.md` to your own .gitignore.' \
62
124
  '.devflow/*' \
63
125
  '!.devflow/features/' \
64
126
  '.devflow/features/*' \
@@ -66,51 +128,165 @@ printf -v _ERG_BLOCK '%s\n' \
66
128
  '!.devflow/features/*/' \
67
129
  '.devflow/features/*/*' \
68
130
  '!.devflow/features/*/KNOWLEDGE.md' \
69
- '!.devflow/conventions.md'
131
+ '!.devflow/conventions.md' \
132
+ '!.devflow/policy.json' \
133
+ '!.devflow/project.json'
134
+ printf -v _ERG_LINE_SENTINEL '%s\n' '!.devflow/conventions.md'
135
+ printf -v _ERG_LINE_POLICY '%s\n' '!.devflow/policy.json'
136
+ printf -v _ERG_LINE_PROJECT '%s\n' '!.devflow/project.json'
137
+ printf -v _ERG_LINE_CLAUDEIGNORE '%s\n' '.claudeignore'
138
+ _ERG_BLOCK="$_ERG_BLOCK_NO_CI$_ERG_LINE_CLAUDEIGNORE"
70
139
 
71
- _ERG_OK=0
72
- if [ ! -f "$_ERG_GITIGNORE" ]; then
73
- # No .gitignore yet — create it with the block.
74
- printf '%s' "$_ERG_BLOCK" > "$_ERG_GITIGNORE" && _ERG_OK=1
75
- elif grep -qF '!.devflow/conventions.md' "$_ERG_GITIGNORE"; then
76
- # v3 carve-out already present — nothing to do.
77
- _ERG_OK=1
78
- elif grep -qE '^/\.devflow/[[:space:]]*$' "$_ERG_GITIGNORE"; then
140
+ # Does the file already carry the policy line, the project line, or a .claudeignore
141
+ # entry of the user's own (either form)? Computed BEFORE the legacy filter below,
142
+ # which rewrites the file.
143
+ _ERG_HAS_POLICY=0
144
+ grep -qE "$_ERG_RE_POLICY" "$_ERG_GITIGNORE" 2>/dev/null && _ERG_HAS_POLICY=1
145
+ _ERG_HAS_PROJECT=0
146
+ grep -qE "$_ERG_RE_PROJECT" "$_ERG_GITIGNORE" 2>/dev/null && _ERG_HAS_PROJECT=1
147
+ _ERG_HAS_CI=0
148
+ grep -qE "$_ERG_RE_CLAUDEIGNORE" "$_ERG_GITIGNORE" 2>/dev/null && _ERG_HAS_CI=1
149
+
150
+ # The completion lines this file lacks, in block order (policy, project, then
151
+ # .claudeignore). Mirrors missingCompletionLines in the TS twin.
152
+ _ERG_COMPLETION=''
153
+ if [ "$_ERG_HAS_POLICY" = 0 ]; then
154
+ _ERG_COMPLETION="$_ERG_LINE_POLICY"
155
+ fi
156
+ if [ "$_ERG_HAS_PROJECT" = 0 ]; then
157
+ _ERG_COMPLETION="$_ERG_COMPLETION$_ERG_LINE_PROJECT"
158
+ fi
159
+ if [ "$_ERG_HAS_CI" = 0 ]; then
160
+ _ERG_COMPLETION="$_ERG_COMPLETION$_ERG_LINE_CLAUDEIGNORE"
161
+ fi
162
+
163
+ # The block lines a top-up run is inserted after: the sentinel, then whichever of
164
+ # the policy and project lines precede the first missing line in a fresh block.
165
+ # Mirrors blockRunBefore in the TS twin.
166
+ if [ "$_ERG_HAS_POLICY" = 0 ]; then
167
+ _ERG_RUN_RE='^[[:space:]]*(!\.devflow/conventions\.md)[[:space:]]*$'
168
+ elif [ "$_ERG_HAS_PROJECT" = 0 ]; then
169
+ _ERG_RUN_RE='^[[:space:]]*(!\.devflow/conventions\.md|!\.devflow/policy\.json)[[:space:]]*$'
170
+ else
171
+ _ERG_RUN_RE='^[[:space:]]*(!\.devflow/conventions\.md|!\.devflow/policy\.json|!\.devflow/project\.json)[[:space:]]*$'
172
+ fi
173
+
174
+ # Classify: noop (nothing to do) | continue (extend an existing block) |
175
+ # block (install a fresh block) | fail (a rewrite failed; leave the file alone).
176
+ # continue inserts _ERG_APPEND after the first line matching _ERG_ANCHOR_RE and the
177
+ # run of lines matching _ERG_RUN_RE right after it (empty: no run).
178
+ _ERG_MODE=noop
179
+ _ERG_APPEND=''
180
+ _ERG_ANCHOR_RE=''
181
+ if grep -qE "$_ERG_RE_OPTOUT" "$_ERG_GITIGNORE" 2>/dev/null; then
79
182
  # User-authored `/.devflow/` (leading slash) — respect it; don't force the carve-out.
80
- # Checked BEFORE the v2 sentinel so a file containing both keeps the user-authored
81
- # entry (matches ensureDevflowGitignore TS order: v3→/.devflow/→v2→wholesale→append).
82
- _ERG_OK=1
83
- elif grep -qF '!.devflow/features/*/KNOWLEDGE.md' "$_ERG_GITIGNORE"; then
84
- # v2→v3 upgrade: v2 sentinel present but conventions.md line absent — append just the
85
- # missing line. A .gitignore whose last byte is not a newline would otherwise fuse the
86
- # appended line onto the last existing one, corrupting BOTH patterns; the sibling append
87
- # branches below get this for free by emitting a leading '\n'. `tail -c 1` yields the
88
- # empty string when the file already ends in a newline (command substitution strips it).
89
- if [ -n "$(tail -c 1 "$_ERG_GITIGNORE" 2>/dev/null)" ]; then
90
- printf '\n' >> "$_ERG_GITIGNORE"
183
+ # Checked FIRST so a file containing both it and a sentinel keeps the user's entry.
184
+ _ERG_MODE=noop
185
+ elif grep -qE "$_ERG_RE_SENTINEL" "$_ERG_GITIGNORE" 2>/dev/null; then
186
+ # Block installed (v3 and later) — top up only the completion lines it lacks.
187
+ if [ -n "$_ERG_COMPLETION" ]; then
188
+ _ERG_MODE=continue
189
+ _ERG_APPEND="$_ERG_COMPLETION"
190
+ _ERG_ANCHOR_RE="$_ERG_RE_SENTINEL"
91
191
  fi
92
- printf '!.devflow/conventions.md\n' >> "$_ERG_GITIGNORE" && _ERG_OK=1
93
- elif grep -qE '^\.devflow/[[:space:]]*$' "$_ERG_GITIGNORE"; then
94
- # Upgrade our legacy wholesale entry: strip the bare `.devflow/` line and our old
95
- # comment, then append the carve-out block. Portable (grep filter + mv; no sed -i,
96
- # which differs on BSD/GNU). An empty filter result is fine — the block is appended
97
- # regardless; a leading blank is added only when prior content survived.
98
- _ERG_TMP="$_ERG_GITIGNORE.devflow-tmp.$$"
99
- grep -vE '^\.devflow/[[:space:]]*$' "$_ERG_GITIGNORE" 2>/dev/null \
100
- | grep -vF '# Devflow runtime data (local by default; remove to share via git)' \
101
- > "$_ERG_TMP" 2>/dev/null
102
- { [ -s "$_ERG_TMP" ] && printf '\n'; printf '%s' "$_ERG_BLOCK"; } >> "$_ERG_TMP" \
103
- && mv "$_ERG_TMP" "$_ERG_GITIGNORE" && _ERG_OK=1
104
- [ "$_ERG_OK" = 1 ] || rm -f "$_ERG_TMP" 2>/dev/null
192
+ elif grep -qE "$_ERG_RE_SENTINEL_V2" "$_ERG_GITIGNORE" 2>/dev/null; then
193
+ # v2 block installed — insert the lines it lacks, in block order, right after
194
+ # its sentinel, the last carve-out line it has.
195
+ _ERG_MODE=continue
196
+ _ERG_APPEND="$_ERG_LINE_SENTINEL$_ERG_COMPLETION"
197
+ _ERG_ANCHOR_RE="$_ERG_RE_SENTINEL_V2"
198
+ _ERG_RUN_RE=''
105
199
  else
106
- # .gitignore exists but has no Devflow entry — append the block.
107
- { printf '\n'; printf '%s' "$_ERG_BLOCK"; } >> "$_ERG_GITIGNORE" && _ERG_OK=1
200
+ # No devflow block — install one, omitting the .claudeignore line the user owns.
201
+ _ERG_MODE=block
202
+ if [ "$_ERG_HAS_CI" = 1 ]; then
203
+ _ERG_APPEND="$_ERG_BLOCK_NO_CI"
204
+ else
205
+ _ERG_APPEND="$_ERG_BLOCK"
206
+ fi
207
+ if grep -qE "$_ERG_RE_LEGACY" "$_ERG_GITIGNORE" 2>/dev/null; then
208
+ # Upgrade our legacy wholesale entry: strip the bare `.devflow/` line and our old
209
+ # comment first, then fall through to the shared append below. Portable (grep
210
+ # filter + mv; no sed -i, which differs on BSD/GNU).
211
+ # The pipeline's own status is deliberately ignored: grep -v exits 1 when it
212
+ # emits no lines, which is the correct outcome for a .gitignore whose only
213
+ # content was the legacy entry. The redirect creates _ERG_TMP either way.
214
+ _ERG_TMP="$_ERG_GITIGNORE.devflow-tmp.$$"
215
+ grep -vE "$_ERG_RE_LEGACY" "$_ERG_GITIGNORE" 2>/dev/null \
216
+ | grep -vF '# Devflow runtime data (local by default; remove to share via git)' \
217
+ > "$_ERG_TMP" 2>/dev/null
218
+ if [ -f "$_ERG_TMP" ] && mv "$_ERG_TMP" "$_ERG_GITIGNORE"; then
219
+ :
220
+ else
221
+ rm -f "$_ERG_TMP" 2>/dev/null
222
+ _ERG_MODE=fail
223
+ fi
224
+ fi
108
225
  fi
109
226
 
110
- # On success, stamp the current-format v3 marker and drop legacy markers.
227
+ # _erg_insert_in_block <anchor-ERE> <run-ERE or ''> <text> — rewrite the file with
228
+ # <text> inserted after the first line matching <anchor-ERE> and the run of up to
229
+ # three lines matching <run-ERE> right after it (D-GITIGNORE-IN-BLOCK). Mirrors
230
+ # insertInBlock in the TS twin byte for byte: head and tail copy every other byte
231
+ # unchanged, and when the run ends on a last line with no newline, one is added
232
+ # before <text>. Each read ends in a trailing `x`, so command substitution cannot
233
+ # strip the file's own trailing newlines. The file is rewritten in place with one
234
+ # `>`, as the TS twin's writeFile does, so a symlinked .gitignore keeps its link.
235
+ # Returns non-zero, having written nothing, when the anchor line cannot be found.
236
+ _erg_insert_in_block() {
237
+ local _anchor="$1" _run="$2" _text="$3" _hit="" _n="" _next="" _line="" _i=0 _head="" _tail=""
238
+ _hit="$(grep -n -m 1 -E "$_anchor" "$_ERG_GITIGNORE" 2>/dev/null)" || return 1
239
+ _n="${_hit%%:*}"
240
+ case "$_n" in ''|*[!0-9]*) return 1 ;; esac
241
+ if [ -n "$_run" ]; then
242
+ _next="$(sed -n "$((_n + 1)),$((_n + 3))p" "$_ERG_GITIGNORE" 2>/dev/null)"
243
+ while [ "$_i" -lt 3 ] && [ -n "$_next" ]; do
244
+ _line="${_next%%$'\n'*}"
245
+ if [ "$_line" = "$_next" ]; then _next=''; else _next="${_next#*$'\n'}"; fi
246
+ [[ $_line =~ $_run ]] || break
247
+ _n=$((_n + 1))
248
+ _i=$((_i + 1))
249
+ done
250
+ fi
251
+ # The `x` is printed only when the read succeeded, so a failed read leaves none
252
+ # and nothing is written — never a truncated file.
253
+ _head="$(head -n "$_n" "$_ERG_GITIGNORE" 2>/dev/null && printf x)"
254
+ case "$_head" in *x) _head="${_head%x}" ;; *) return 1 ;; esac
255
+ _tail="$(tail -n "+$((_n + 1))" "$_ERG_GITIGNORE" 2>/dev/null && printf x)"
256
+ case "$_tail" in *x) _tail="${_tail%x}" ;; *) return 1 ;; esac
257
+ case "$_head" in *$'\n') ;; *) _head="$_head"$'\n' ;; esac
258
+ printf '%s%s%s' "$_head" "$_text" "$_tail" > "$_ERG_GITIGNORE"
259
+ }
260
+
261
+ # The fresh-block append shape, mirrored byte-for-byte by the TS twin's appendBlock.
262
+ # `tail -c 1` yields the empty string when the file already ends in a newline
263
+ # (command substitution strips it), so a file whose last byte is not a newline gets
264
+ # one before the appended text fuses onto its last line. Existing trailing newlines
265
+ # are preserved verbatim — no trimming, no blank-line dedupe.
266
+ _ERG_OK=0
267
+ case "$_ERG_MODE" in
268
+ noop)
269
+ _ERG_OK=1
270
+ ;;
271
+ continue)
272
+ _erg_insert_in_block "$_ERG_ANCHOR_RE" "$_ERG_RUN_RE" "$_ERG_APPEND" && _ERG_OK=1
273
+ ;;
274
+ block)
275
+ if [ ! -s "$_ERG_GITIGNORE" ]; then
276
+ # Absent or empty — the block is the whole file, with no leading blank line.
277
+ printf '%s' "$_ERG_APPEND" > "$_ERG_GITIGNORE" && _ERG_OK=1
278
+ else
279
+ if [ -n "$(tail -c 1 "$_ERG_GITIGNORE" 2>/dev/null)" ]; then
280
+ printf '\n' >> "$_ERG_GITIGNORE"
281
+ fi
282
+ { printf '\n'; printf '%s' "$_ERG_APPEND"; } >> "$_ERG_GITIGNORE" && _ERG_OK=1
283
+ fi
284
+ ;;
285
+ esac
286
+
287
+ # On success, stamp the current-format v6 marker and drop legacy markers.
111
288
  [ "$_ERG_OK" = 1 ] && {
112
289
  touch "$_ERG_MARKER"
113
- rm -f "$_ERG_DEVFLOW_DIR/.root-gitignore-configured-v2" 2>/dev/null
114
- rm -f "$_ERG_DEVFLOW_DIR/.root-gitignore-configured" 2>/dev/null
290
+ _erg_drop_legacy_markers
115
291
  }
116
292
  return 0
@@ -14,3 +14,51 @@ df_has_git_marker() {
14
14
  done
15
15
  return 1
16
16
  }
17
+
18
+ # df_is_project_root <root> — returns 0 when <root> may hold per-project devflow
19
+ # data: it is a checkout's toplevel — it holds its own `.git` entry — AND it is not
20
+ # the home directory.
21
+ #
22
+ # D-HOOKS-GIT-ONLY: the hooks that scaffold `.devflow/` and the root `.gitignore`
23
+ # carve-out — session-start-context and ensure-devflow-init, which every capture
24
+ # and memory hook reaches — run only for a git project. Outside one there is no
25
+ # repository for the carve-out to protect and no branch for memory to describe,
26
+ # and scaffolding there would leave `.devflow/` and a `.gitignore` block in `~`,
27
+ # Downloads or a folder of repos — wherever Claude Code happened to start.
28
+ # A repository rooted at HOME (a dotfiles repo) is refused too: its `.devflow` IS
29
+ # the machine root, so per-project data would land inside `~/.devflow` and its
30
+ # carve-out in the user's home `.gitignore`.
31
+ #
32
+ # D-HOOKS-TOPLEVEL-ONLY: the `.git` entry must sit AT <root>, not merely above it.
33
+ # Callers pass the root df_resolve_roots resolved, and when `git rev-parse` fails
34
+ # from inside a checkout — the repository has dubious ownership, or
35
+ # GIT_CEILING_DIRECTORIES stops git's search — that root is the raw cwd, possibly a
36
+ # subdirectory. An ancestor walk would still find the checkout's `.git` and admit
37
+ # it, and `.devflow/` plus a `.gitignore` block would land in that subdirectory.
38
+ # Requiring the marker at the root makes the gate and the resolver agree: git's
39
+ # toplevel always holds `.git` (a directory, or the file a linked worktree or
40
+ # submodule carries), a subdirectory never does. A directory that holds its own
41
+ # `.git` still passes when git cannot read it — the marker contract is the zero-fork
42
+ # one above, and writing there scatters nothing, because it is the checkout's root.
43
+ #
44
+ # The comparison is between PHYSICAL paths, because the two sides reach it by
45
+ # different routes: git reports a toplevel with every symlink resolved, while
46
+ # $HOME is whatever the login shell was handed — on macOS the temp tree is
47
+ # /var → /private/var, and a home directory can itself sit behind a symlink. A
48
+ # textual compare would miss exactly the case it exists for.
49
+ #
50
+ # Zero forks, like the marker walk above: a builtin `-e` test, then `cd -P`, which
51
+ # resolves a path physically and publishes it in $PWD — both shell builtins, so no
52
+ # `$(...)` subshell and no realpath/readlink binary is involved. The caller's directory is restored before
53
+ # returning. An unresolvable <root> is refused (fail closed: nothing is written);
54
+ # an unset or unresolvable HOME cannot equal a resolved root, so it refuses nothing.
55
+ df_is_project_root() {
56
+ local _root="$1" _back="$PWD" _root_real="" _home_real=""
57
+ [ -n "$_root" ] || return 1
58
+ [ -e "$_root/.git" ] || return 1
59
+ if cd -P "$_root" 2>/dev/null; then _root_real="$PWD"; fi
60
+ if [ -n "${HOME:-}" ] && cd -P "$HOME" 2>/dev/null; then _home_real="$PWD"; fi
61
+ cd "$_back" 2>/dev/null || true
62
+ [ -n "$_root_real" ] || return 1
63
+ [ "$_root_real" != "$_home_real" ]
64
+ }
@@ -29,9 +29,11 @@ LOG_FILE="$LOG_DIR/.${_HOOK_LOG_NAME}.log"
29
29
 
30
30
  # Size guard: truncate log to tail 1MB when it exceeds 2MB (runs once per hook invocation)
31
31
  # stat -f%z (macOS/BSD) → stat -c%s (GNU/Linux) → wc -c (last resort), matching debug-trace pattern.
32
+ # `2>/dev/null` precedes `<`: redirections apply left to right, so the shell's own
33
+ # "No such file or directory" for a missing log (first run for a cwd) lands in /dev/null.
32
34
  _LOG_SIZE=$(stat -f%z "$LOG_FILE" 2>/dev/null) \
33
35
  || _LOG_SIZE=$(stat -c%s "$LOG_FILE" 2>/dev/null) \
34
- || _LOG_SIZE=$(wc -c < "$LOG_FILE" 2>/dev/null) \
36
+ || _LOG_SIZE=$(wc -c 2>/dev/null < "$LOG_FILE") \
35
37
  || _LOG_SIZE=0
36
38
  if [ -f "$LOG_FILE" ] && [ "$_LOG_SIZE" -gt 2097152 ]; then
37
39
  _LTMP="$LOG_FILE.tmp.$$"