devflow-kit 2.5.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 (158) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +44 -19
  3. package/dist/agents/git.md +13 -15
  4. package/dist/cli/commands/ambient.js +160 -145
  5. package/dist/cli/commands/capture.js +29 -55
  6. package/dist/cli/commands/compliance.js +32 -61
  7. package/dist/cli/commands/context.js +17 -32
  8. package/dist/cli/commands/debug.js +65 -26
  9. package/dist/cli/commands/flags.js +3 -3
  10. package/dist/cli/commands/hud.js +34 -10
  11. package/dist/cli/commands/init-seed.js +40 -4
  12. package/dist/cli/commands/init.js +249 -271
  13. package/dist/cli/commands/install-report.js +10 -15
  14. package/dist/cli/commands/knowledge/index.js +1 -1
  15. package/dist/cli/commands/knowledge/toggle.js +11 -3
  16. package/dist/cli/commands/learning.js +52 -37
  17. package/dist/cli/commands/legacy-hooks.js +11 -14
  18. package/dist/cli/commands/memory.js +67 -78
  19. package/dist/cli/commands/proxy.js +23 -41
  20. package/dist/cli/commands/security.js +5 -13
  21. package/dist/cli/commands/skills.js +21 -3
  22. package/dist/cli/commands/tracker.js +100 -228
  23. package/dist/cli/commands/uninstall.js +343 -138
  24. package/dist/commands/bug-analysis.md +38 -12
  25. package/dist/commands/code-review.md +70 -21
  26. package/dist/commands/debug.md +37 -7
  27. package/dist/commands/dynamic-build.md +66 -17
  28. package/dist/commands/dynamic-plan.md +19 -8
  29. package/dist/commands/dynamic-profile.md +24 -10
  30. package/dist/commands/dynamic-tickets.md +22 -11
  31. package/dist/commands/explore.md +37 -7
  32. package/dist/commands/implement.md +96 -32
  33. package/dist/commands/plan.md +62 -19
  34. package/dist/commands/release.md +2 -2
  35. package/dist/commands/research.md +34 -8
  36. package/dist/commands/resolve.md +65 -17
  37. package/dist/commands/self-review.md +45 -9
  38. package/dist/core/compliance-compose.js +27 -27
  39. package/dist/core/evidence-policy.js +240 -24
  40. package/dist/core/feature-config.js +94 -25
  41. package/dist/core/feature-switch.js +1 -1
  42. package/dist/core/flags.js +30 -2
  43. package/dist/core/fs-atomic.js +27 -0
  44. package/dist/core/hook-log-dirs.js +104 -0
  45. package/dist/core/learning-tuning-config.js +5 -3
  46. package/dist/core/ledger-root.js +102 -0
  47. package/dist/core/manifest.js +6 -4
  48. package/dist/core/mds-variants.js +34 -97
  49. package/dist/core/migrations.js +49 -23
  50. package/dist/core/plugins.js +5 -4
  51. package/dist/core/project-paths.js +0 -17
  52. package/dist/core/same-location.js +25 -0
  53. package/dist/core/tracker.js +226 -139
  54. package/dist/hud/components/config-counts.js +15 -4
  55. package/dist/hud/components/learning-counts.js +14 -0
  56. package/dist/hud/config.js +2 -1
  57. package/dist/hud/cost-history.js +2 -4
  58. package/dist/hud/git.js +52 -7
  59. package/dist/hud/index.js +7 -9
  60. package/dist/skills/git/references/pr/check-merge-readiness.md +1 -1
  61. package/dist/skills/git/references/pr/ensure-pr-ready.md +1 -1
  62. package/dist/skills/git/references/pr/update-pr-evidence.md +1 -1
  63. package/dist/skills/git/references/tracker/_mcp.md +1 -1
  64. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +1 -1
  65. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +1 -1
  66. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +2 -2
  67. package/dist/skills/git/references/tracker/github/manage-debt.md +3 -3
  68. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +1 -1
  69. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +1 -1
  70. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +2 -2
  71. package/dist/skills/git/references/tracker/jira/manage-debt.md +1 -1
  72. package/dist/skills/git/references/tracker/jira/post-wave-report.md +1 -1
  73. package/dist/skills/git/references/tracker/jira/setup-task.md +1 -1
  74. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +1 -1
  75. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +1 -1
  76. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +2 -2
  77. package/dist/skills/git/references/tracker/linear/manage-debt.md +1 -1
  78. package/dist/skills/git/references/tracker/linear/post-wave-report.md +1 -1
  79. package/dist/skills/git/references/tracker/linear/setup-task.md +1 -1
  80. package/dist/targets/claude-code/claude-paths.js +59 -57
  81. package/dist/targets/claude-code/compliance-install.js +49 -65
  82. package/dist/targets/claude-code/hooks.js +108 -3
  83. package/dist/targets/claude-code/installer.js +30 -57
  84. package/dist/targets/claude-code/post-install.js +232 -139
  85. package/dist/targets/claude-code/tracker-install.js +38 -65
  86. package/package.json +5 -4
  87. package/src/assets/agents/code.md +4 -3
  88. package/src/assets/agents/design.md +1 -0
  89. package/src/assets/agents/git.mds +55 -57
  90. package/src/assets/agents/knowledge.md +2 -2
  91. package/src/assets/agents/review.md +3 -1
  92. package/src/assets/agents/tracker.md +37 -30
  93. package/src/assets/commands/_partials/_compliance.mds +19 -1
  94. package/src/assets/commands/_partials/_decisions.mds +15 -3
  95. package/src/assets/commands/_partials/_docs_root.mds +35 -0
  96. package/src/assets/commands/_partials/_engine.mds +2 -2
  97. package/src/assets/commands/_partials/_evidence_policy.mds +3 -3
  98. package/src/assets/commands/_partials/_factory.mds +1 -1
  99. package/src/assets/commands/_partials/_knowledge.mds +27 -9
  100. package/src/assets/commands/_partials/_plan_contract.mds +2 -2
  101. package/src/assets/commands/_partials/_preamble.mds +1 -1
  102. package/src/assets/commands/_partials/_publication.mds +6 -2
  103. package/src/assets/commands/_partials/_settings.mds +28 -0
  104. package/src/assets/commands/_partials/_ticket_template.mds +3 -3
  105. package/src/assets/commands/_partials/_tracker.mds +4 -4
  106. package/src/assets/commands/_partials/_wave.mds +4 -4
  107. package/src/assets/commands/bug-analysis.mds +19 -17
  108. package/src/assets/commands/code-review.mds +39 -33
  109. package/src/assets/commands/debug.mds +4 -5
  110. package/src/assets/commands/dynamic-build.mds +75 -53
  111. package/src/assets/commands/dynamic-plan.mds +20 -15
  112. package/src/assets/commands/dynamic-profile.mds +24 -11
  113. package/src/assets/commands/dynamic-tickets.mds +25 -20
  114. package/src/assets/commands/explore.mds +4 -5
  115. package/src/assets/commands/implement.mds +58 -45
  116. package/src/assets/commands/plan.mds +34 -29
  117. package/src/assets/commands/release.md +2 -2
  118. package/src/assets/commands/research.mds +11 -9
  119. package/src/assets/commands/resolve.mds +41 -39
  120. package/src/assets/commands/self-review.mds +24 -25
  121. package/src/assets/mds/git/_pr.mds +61 -61
  122. package/src/assets/mds/git/_references.mds +19 -19
  123. package/src/assets/mds/tracker/_common.mds +8 -8
  124. package/src/assets/mds/tracker/_github.mds +71 -71
  125. package/src/assets/mds/tracker/_jira.mds +74 -74
  126. package/src/assets/mds/tracker/_linear.mds +75 -75
  127. package/src/assets/mds/tracker/_mcp.mds +23 -17
  128. package/src/assets/scripts/hooks/background-memory-update +35 -19
  129. package/src/assets/scripts/hooks/capture-prompt +18 -12
  130. package/src/assets/scripts/hooks/capture-question +18 -12
  131. package/src/assets/scripts/hooks/capture-turn +27 -17
  132. package/src/assets/scripts/hooks/debug-trace +11 -6
  133. package/src/assets/scripts/hooks/ensure-devflow-init +33 -6
  134. package/src/assets/scripts/hooks/ensure-proxy +9 -8
  135. package/src/assets/scripts/hooks/ensure-root-gitignore +111 -36
  136. package/src/assets/scripts/hooks/git-marker +48 -0
  137. package/src/assets/scripts/hooks/json-helper.cjs +6 -1
  138. package/src/assets/scripts/hooks/lib/project-paths.cjs +0 -19
  139. package/src/assets/scripts/hooks/log-paths +80 -0
  140. package/src/assets/scripts/hooks/memory-worker +17 -15
  141. package/src/assets/scripts/hooks/pre-compact-memory +41 -16
  142. package/src/assets/scripts/hooks/queue-append +104 -30
  143. package/src/assets/scripts/hooks/resolve-project-root +101 -7
  144. package/src/assets/scripts/hooks/session-start-context +289 -122
  145. package/src/assets/scripts/hooks/session-start-memory +35 -16
  146. package/src/assets/scripts/lib/project-config.cjs +633 -0
  147. package/src/assets/scripts/resolve-evidence-policy.cjs +300 -220
  148. package/src/assets/scripts/resolve-settings.cjs +1054 -0
  149. package/src/assets/scripts/verify-evidence.cjs +1 -1
  150. package/src/assets/skills/compliance/SKILL.md +2 -2
  151. package/src/assets/skills/docs-framework/SKILL.md +6 -7
  152. package/src/assets/skills/docs-framework/references/patterns.md +10 -17
  153. package/src/assets/skills/gap-analysis/SKILL.md +2 -2
  154. package/src/assets/skills/git/references/github-api.md +9 -9
  155. package/src/assets/skills/git/references/patterns.md +1 -1
  156. package/src/assets/skills/worktree-support/SKILL.md +1 -1
  157. package/src/assets/skills/worktree-support/references/roots.md +29 -0
  158. package/src/targets/claude-code/templates/managed-settings.json +25 -9
@@ -1,11 +1,9 @@
1
1
  import { promises as fs, writeFileSync, unlinkSync } from 'fs';
2
2
  import { execFileSync } from 'child_process';
3
3
  import * as path from 'path';
4
- import * as os from 'os';
5
4
  import * as p from '@clack/prompts';
6
5
  import { getManagedSettingsPath } from './claude-paths.js';
7
- import { getGitignoreEntries, getDocsDir } from '../../core/project-paths.js';
8
- import { writeFileAtomicExclusive } from '../../core/fs-atomic.js';
6
+ import { writeSettingsFileAtomic } from '../../core/fs-atomic.js';
9
7
  function isNodeSystemError(error) {
10
8
  return (error instanceof Error &&
11
9
  'code' in error &&
@@ -13,18 +11,15 @@ function isNodeSystemError(error) {
13
11
  }
14
12
  /**
15
13
  * Replace ${DEVFLOW_DIR} placeholders in a settings template.
14
+ *
15
+ * D-ONE-HOME: the settings template's install-time placeholder is the one
16
+ * surviving spelling of that name. It is a template token substituted here with
17
+ * the machine root (always ~/.devflow) — not an environment variable, and no
18
+ * runtime reader resolves it (tests/guards/one-home.test.ts pins both sites).
16
19
  */
17
20
  export function substituteSettingsTemplate(template, devflowDir) {
18
21
  return template.replace(/\$\{DEVFLOW_DIR\}/g, devflowDir);
19
22
  }
20
- /**
21
- * Compute which entries need appending to a .gitignore file.
22
- * Returns only entries not already present.
23
- */
24
- export function computeGitignoreAppend(existingContent, entries) {
25
- const existingLines = existingContent.split('\n').map(l => l.trim());
26
- return entries.filter(entry => !existingLines.includes(entry));
27
- }
28
23
  /**
29
24
  * Sentinel line whose presence means the current (v3-and-later) carve-out block is
30
25
  * installed. Devflow-unique: no user writes `!.devflow/conventions.md` by hand.
@@ -40,13 +35,24 @@ const CLAUDEIGNORE_LINE = '.claudeignore';
40
35
  /** A user's explicit un-ignore of `.claudeignore`; never overridden. */
41
36
  const CLAUDEIGNORE_NEGATION = '!.claudeignore';
42
37
  /**
43
- * Re-includes the team-owned evidence policy file (D-GITIGNORE-V5). A COMPLETION
44
- * line, never a presence sentinel: users may author it themselves, so its presence
38
+ * Re-includes the retired evidence-policy file (D-GITIGNORE-V5,
39
+ * D-POLICY-JSON-RETIRED). A COMPLETION line, never a presence sentinel: users may author it themselves, so its presence
45
40
  * proves nothing about the devflow block (avoids PF-059). It sits after `.devflow/*`
46
41
  * (which it overrides under last-match-wins) and before `.claudeignore`, so the
47
42
  * block's final line stays `.claudeignore`.
48
43
  */
49
44
  const DEVFLOW_POLICY_LINE = '!.devflow/policy.json';
45
+ /**
46
+ * Re-includes the team-committed project settings file (D-GITIGNORE-V6). The same
47
+ * contract as the policy line: a COMPLETION line, never a presence sentinel — a
48
+ * user may author it before devflow ever runs, so its presence proves nothing about
49
+ * the block (avoids PF-059). It sits after the policy line and before `.claudeignore`,
50
+ * so a v5 block, which ends in `.claudeignore`, gains it just before that line
51
+ * (D-GITIGNORE-IN-BLOCK, computeDevflowGitignore). Without it
52
+ * `.devflow/*` ignores `.devflow/project.json`, and a team could only commit it with
53
+ * `git add -f`. Devflow never writes the file itself (ADR-024).
54
+ */
55
+ const DEVFLOW_PROJECT_LINE = '!.devflow/project.json';
50
56
  /**
51
57
  * The shared .devflow/ gitignore block. Everything under .devflow/ is local
52
58
  * (memory, learning, docs, locks) EXCEPT:
@@ -54,9 +60,14 @@ const DEVFLOW_POLICY_LINE = '!.devflow/policy.json';
54
60
  * + committed (the Knowledge agent commits them at workflow end).
55
61
  * - conventions.md: naming-convention authority written by the Git learn-conventions
56
62
  * operation; GIT-TRACKED so the team shares a single naming source.
57
- * - policy.json: the team-owned evidence policy, read from the default branch by
58
- * resolve-evidence-policy.cjs; GIT-TRACKED so a team can commit it without `git add -f`.
59
- * Devflow never writes it.
63
+ * - policy.json: the retired evidence-policy file. resolve-evidence-policy.cjs never
64
+ * parses it, but where project.json has no `evidence` its presence holds the
65
+ * repository at `required` (D-POLICY-JSON-RETIRED); GIT-TRACKED so a team's
66
+ * committed copy stays shared until its value moves into project.json. Devflow
67
+ * never writes it.
68
+ * - project.json: the team-committed settings (evidence, compliance, tracker, review
69
+ * publication, narrow-only feature switches) both resolvers read; GIT-TRACKED for
70
+ * the same reason (D-GITIGNORE-V6). Devflow never writes it.
60
71
  *
61
72
  * Re-including files under an ignored tree needs a `dir/*` + `!dir/keep` pair at
62
73
  * each level — a bare `.devflow/` excludes the directory so git never descends and
@@ -68,9 +79,10 @@ const DEVFLOW_POLICY_LINE = '!.devflow/policy.json';
68
79
  const DEVFLOW_GITIGNORE_BLOCK_LINES = [
69
80
  '# Devflow runtime data — local by default (memory, learning, docs, locks).',
70
81
  '# Shared via git: feature knowledge bases under .devflow/features/ (index.md and',
71
- '# every {slug}/KNOWLEDGE.md), .devflow/conventions.md (naming authority) and',
72
- '# .devflow/policy.json (evidence policy). To stop sharing the first two, re-add',
73
- '# `.devflow/features/` or `.devflow/conventions.md` to your own .gitignore.',
82
+ '# every {slug}/KNOWLEDGE.md), .devflow/conventions.md (naming authority),',
83
+ '# .devflow/policy.json (retired; presence only) and .devflow/project.json (team settings).',
84
+ '# To stop sharing the first two, re-add `.devflow/features/` or',
85
+ '# `.devflow/conventions.md` to your own .gitignore.',
74
86
  '.devflow/*',
75
87
  '!.devflow/features/',
76
88
  '.devflow/features/*',
@@ -80,10 +92,29 @@ const DEVFLOW_GITIGNORE_BLOCK_LINES = [
80
92
  DEVFLOW_GITIGNORE_SENTINEL_V2,
81
93
  DEVFLOW_GITIGNORE_SENTINEL_V3,
82
94
  DEVFLOW_POLICY_LINE,
95
+ DEVFLOW_PROJECT_LINE,
83
96
  CLAUDEIGNORE_LINE,
84
97
  ];
85
98
  /** The full carve-out block, `.claudeignore` line included. */
86
99
  export const DEVFLOW_GITIGNORE_BLOCK = DEVFLOW_GITIGNORE_BLOCK_LINES.join('\n');
100
+ /**
101
+ * The entries directly under a repository's `.devflow/` that the team shares
102
+ * through git (a trailing `/` marks a directory): the feature knowledge bases,
103
+ * the naming conventions, the retired evidence-policy file and the committed
104
+ * project settings.
105
+ *
106
+ * D-UNINSTALL-CARVE-OUT: uninstall's project-data step never deletes these — a
107
+ * confirmed removal takes everything else under `.devflow/` and keeps them
108
+ * byte-identical, because an uncommitted edit to a tracked file is not
109
+ * recoverable from git. Kept next to the gitignore block it mirrors: every path
110
+ * the block re-includes must appear here (pinned by tests/uninstall-logic.test.ts).
111
+ */
112
+ export const DEVFLOW_TRACKED_PATHS = Object.freeze([
113
+ 'features/',
114
+ 'conventions.md',
115
+ 'policy.json',
116
+ 'project.json',
117
+ ]);
87
118
  /**
88
119
  * The carve-out block without its final `.claudeignore` line — emitted instead of
89
120
  * the full block when the target .gitignore already carries a `.claudeignore` or
@@ -92,15 +123,23 @@ export const DEVFLOW_GITIGNORE_BLOCK = DEVFLOW_GITIGNORE_BLOCK_LINES.join('\n');
92
123
  export const DEVFLOW_GITIGNORE_BLOCK_WITHOUT_CLAUDEIGNORE = DEVFLOW_GITIGNORE_BLOCK_LINES.slice(0, -1).join('\n');
93
124
  /** The legacy wholesale comment our pre-carve-out writers emitted. */
94
125
  const LEGACY_DEVFLOW_COMMENT = '# Devflow runtime data (local by default; remove to share via git)';
126
+ /**
127
+ * The most lines a top-up run extends below its anchor — one per line the run may
128
+ * hold (the sentinel, the policy line, the project line). The shell twin's loop has
129
+ * the same bound.
130
+ */
131
+ const BLOCK_RUN_MAX = 3;
95
132
  /**
96
133
  * PURE: given existing .gitignore content, return the content that ignores
97
- * `.devflow/` with the feature-knowledge + conventions.md + policy.json carve-out —
98
- * or `null` when no change is needed. Idempotent: feeding its own output back returns `null`.
134
+ * `.devflow/` with the feature-knowledge + conventions.md + policy.json + project.json
135
+ * carve-out — or `null` when no change is needed. Idempotent: feeding its own output
136
+ * back returns `null`.
99
137
  *
100
- * D-GITIGNORE-V5: the block is detected ONLY by its own devflow-unique sentinel
101
- * (`!.devflow/conventions.md`). `.claudeignore` and `!.devflow/policy.json` are
102
- * COMPLETION lines — users legitimately author both themselves — so each is topped up
103
- * when missing and never read as proof the block exists. A presence check on a
138
+ * D-GITIGNORE-V5 / D-GITIGNORE-V6: the block is detected ONLY by its own
139
+ * devflow-unique sentinel (`!.devflow/conventions.md`). `.claudeignore`,
140
+ * `!.devflow/policy.json` and `!.devflow/project.json` are COMPLETION lines — users
141
+ * legitimately author all three themselves — so each is topped up when missing and
142
+ * never read as proof the block exists. A presence check on a
104
143
  * user-authored line inverts both halves of the contract: projects that already carry
105
144
  * that line are told the block is installed when it is not, and a user's
106
145
  * `!.claudeignore` un-ignore is silently reversed by re-appending `.claudeignore`
@@ -108,26 +147,36 @@ const LEGACY_DEVFLOW_COMMENT = '# Devflow runtime data (local by default; remove
108
147
  *
109
148
  * `hasClaudeignoreEntry` is true when some whole line, trimmed, is exactly
110
149
  * `.claudeignore` OR `!.claudeignore`. Treating both forms as "present" both honours
111
- * an un-ignore and makes every branch converge on re-run. `hasPolicyLine` is true when
112
- * some whole line, trimmed, is exactly `!.devflow/policy.json`. The missing completion
113
- * lines, in block order, are [policy line, `.claudeignore` (only when
150
+ * an un-ignore and makes every branch converge on re-run. `hasPolicyLine` and
151
+ * `hasProjectLine` are true when some whole line, trimmed, is exactly
152
+ * `!.devflow/policy.json` / `!.devflow/project.json`. The missing completion lines, in
153
+ * block order, are [policy line, project line, `.claudeignore` (only when
114
154
  * `!hasClaudeignoreEntry`)].
115
155
  *
116
156
  * 1. A `/.devflow/` line present → `null` (user opt-out; respect manual config).
117
- * 2. v3 sentinel present → append the missing completion lines; `null` when none are
118
- * missing. This is the v4→v5 upgrade: a v4 block gains only the policy line, after
119
- * its `.claudeignore` line, and keeps its old comment.
120
- * 3. v2 sentinel present, no v3 → append `!.devflow/conventions.md` followed by the
121
- * missing completion lines.
157
+ * 2. v3 sentinel present → insert the missing completion lines into the block;
158
+ * `null` when none are missing. This is the v4→v6 and v5→v6 upgrade: a v5 block
159
+ * gains only the project line, just before its `.claudeignore` line, and a v4
160
+ * block the policy and project lines, each keeping its old comment.
161
+ * 3. v2 sentinel present, no v3 → insert `!.devflow/conventions.md` followed by the
162
+ * missing completion lines right after the v2 sentinel.
122
163
  * 4. Legacy bare `.devflow/` present → strip it (+ our old comment), then append the
123
164
  * block; no block at all → append the block. The block is emitted MINUS its final
124
- * `.claudeignore` line when `hasClaudeignoreEntry`. A user's own policy line is
125
- * duplicated harmlessly here, and the re-run is a no-op.
126
- * 5. Neither completion line is ever a sentinel. The marker file
127
- * (`.devflow/.root-gitignore-configured-v5`) is a fast-path claim, never proof.
165
+ * `.claudeignore` line when `hasClaudeignoreEntry`. A user's own policy or project
166
+ * line is duplicated harmlessly here, and the re-run is a no-op.
167
+ * 5. No completion line is ever a sentinel. The marker file
168
+ * (`.devflow/.root-gitignore-configured-v6`) is a fast-path claim, never proof.
169
+ *
170
+ * D-GITIGNORE-IN-BLOCK: lines topped up into an existing block (2 and 3) go INSIDE
171
+ * it, where a fresh block holds them — never at the end of the file. gitignore is
172
+ * last-match-wins, so a `!.devflow/project.json` appended after a user's own later
173
+ * `.devflow/project.json` would silently override their re-ignore. The missing
174
+ * lines are inserted as one run, in block order, after the first sentinel line and
175
+ * the lines right after it that a fresh block places before the first missing line
176
+ * (blockRunBefore; at most three). Every other byte of the file is kept.
128
177
  *
129
178
  * Line matching is whole-line, whitespace-tolerant, exact text — never substring.
130
- * Both append forms are mirrored byte-for-byte in the shell twin
179
+ * The insert and the append form are mirrored byte-for-byte in the shell twin
131
180
  * (src/assets/scripts/hooks/ensure-root-gitignore), which is what the cross-implementation
132
181
  * parity table in tests/shell-hooks.test.ts pins.
133
182
  */
@@ -136,16 +185,39 @@ export function computeDevflowGitignore(existingContent) {
136
185
  const trimmed = lines.map(l => l.trim());
137
186
  const hasClaudeignoreEntry = trimmed.some(l => l === CLAUDEIGNORE_LINE || l === CLAUDEIGNORE_NEGATION);
138
187
  const hasPolicyLine = trimmed.includes(DEVFLOW_POLICY_LINE);
188
+ const hasProjectLine = trimmed.includes(DEVFLOW_PROJECT_LINE);
139
189
  /** The block-completing lines this file lacks, in block order. */
140
190
  const missingCompletionLines = [
141
191
  ...(hasPolicyLine ? [] : [DEVFLOW_POLICY_LINE]),
192
+ ...(hasProjectLine ? [] : [DEVFLOW_PROJECT_LINE]),
142
193
  ...(hasClaudeignoreEntry ? [] : [CLAUDEIGNORE_LINE]),
143
194
  ];
144
195
  /**
145
- * Continue an existing devflow block with the lines it is missing. One newline
146
- * guard, no blank separator — the appended lines belong to the block above them.
196
+ * The block lines a top-up run follows: the v3 sentinel, then whichever of the
197
+ * policy and project lines a fresh block places before the first missing line.
198
+ * Mirrors `_ERG_RUN_RE` in the shell twin.
199
+ */
200
+ const blockRunBefore = !hasPolicyLine
201
+ ? [DEVFLOW_GITIGNORE_SENTINEL_V3]
202
+ : !hasProjectLine
203
+ ? [DEVFLOW_GITIGNORE_SENTINEL_V3, DEVFLOW_POLICY_LINE]
204
+ : [DEVFLOW_GITIGNORE_SENTINEL_V3, DEVFLOW_POLICY_LINE, DEVFLOW_PROJECT_LINE];
205
+ /**
206
+ * Insert `inserted` into an existing devflow block: after the first line whose
207
+ * trimmed text is `anchor`, and after up to BLOCK_RUN_MAX lines right below it
208
+ * whose trimmed text is in `run`. Every other byte is kept. A run that ends on a
209
+ * last line with no newline gets one first, so the inserted lines never fuse onto
210
+ * it. Mirrors `_erg_insert_in_block` in the shell twin (D-GITIGNORE-IN-BLOCK).
147
211
  */
148
- const appendLines = (body, block) => `${body}${body.endsWith('\n') ? '' : '\n'}${block}\n`;
212
+ const insertInBlock = (anchor, run, inserted) => {
213
+ let end = trimmed.indexOf(anchor);
214
+ for (let k = 0; k < BLOCK_RUN_MAX && end + 1 < lines.length && run.includes(trimmed[end + 1]); k++) {
215
+ end++;
216
+ }
217
+ if (end === lines.length - 1)
218
+ return `${existingContent}\n${inserted.join('\n')}\n`;
219
+ return [...lines.slice(0, end + 1), ...inserted, ...lines.slice(end + 1)].join('\n');
220
+ };
149
221
  /**
150
222
  * Start a new block after unrelated content: one blank separator line. Existing
151
223
  * trailing newlines are preserved verbatim (no trimEnd, no blank-line dedupe) so
@@ -161,11 +233,12 @@ export function computeDevflowGitignore(existingContent) {
161
233
  if (trimmed.includes(DEVFLOW_GITIGNORE_SENTINEL_V3)) {
162
234
  return missingCompletionLines.length === 0
163
235
  ? null
164
- : appendLines(existingContent, missingCompletionLines.join('\n'));
236
+ : insertInBlock(DEVFLOW_GITIGNORE_SENTINEL_V3, blockRunBefore, missingCompletionLines);
165
237
  }
166
- // 3. v2 block installed — append the lines it lacks, in block order.
238
+ // 3. v2 block installed — insert the lines it lacks, in block order, right after
239
+ // its sentinel, the last carve-out line it has.
167
240
  if (trimmed.includes(DEVFLOW_GITIGNORE_SENTINEL_V2)) {
168
- return appendLines(existingContent, [DEVFLOW_GITIGNORE_SENTINEL_V3, ...missingCompletionLines].join('\n'));
241
+ return insertInBlock(DEVFLOW_GITIGNORE_SENTINEL_V2, [], [DEVFLOW_GITIGNORE_SENTINEL_V3, ...missingCompletionLines]);
169
242
  }
170
243
  // 4. No devflow block — install one, respecting any .claudeignore entry of the user's own.
171
244
  const block = hasClaudeignoreEntry
@@ -182,8 +255,12 @@ export function computeDevflowGitignore(existingContent) {
182
255
  }
183
256
  /**
184
257
  * Merge Devflow deny entries into an existing settings JSON object.
185
- * Preserves existing entries (including allow and sibling keys), deduplicates,
186
- * and returns the merged JSON string with trailing newline.
258
+ * Preserves existing entries (including allow and sibling keys) except those named
259
+ * in `retired`, deduplicates, and returns the merged JSON string with trailing newline.
260
+ *
261
+ * `retired` is how an install converges an older one: pass retiredDenyEntries(template)
262
+ * so entries Devflow once shipped and has since dropped do not linger. An entry that
263
+ * `newDenyEntries` carries is never dropped, whatever `retired` says.
187
264
  *
188
265
  * PURE + idempotent: calling with the same inputs always yields byte-equal output.
189
266
  * Non-array `deny` (e.g. a string, null) is treated as empty — neither throws nor spreads chars.
@@ -191,22 +268,33 @@ export function computeDevflowGitignore(existingContent) {
191
268
  * @throws {SyntaxError} on malformed JSON — callers must pre-validate (e.g. via detectDenyState)
192
269
  * or wrap in try/catch.
193
270
  */
194
- export function mergeDenyList(existingJson, newDenyEntries) {
271
+ export function mergeDenyList(existingJson, newDenyEntries, retired = new Set()) {
195
272
  const existing = JSON.parse(existingJson);
196
273
  const rawDeny = existing.permissions?.deny;
197
274
  const currentDeny = Array.isArray(rawDeny) ? rawDeny : [];
198
- const merged = [...new Set([...currentDeny, ...newDenyEntries])];
275
+ const kept = currentDeny.filter(e => !retired.has(e));
276
+ const merged = [...new Set([...kept, ...newDenyEntries])];
199
277
  existing.permissions = { ...(existing.permissions ?? {}), deny: merged };
200
278
  return JSON.stringify(existing, null, 2) + '\n';
201
279
  }
202
280
  /**
203
281
  * Historical superset of every deny entry Devflow has ever shipped.
204
- * Append every future entry here; never remove entries.
205
- * Used by stripUserDenyList to identify Devflow-managed entries in legacy installs.
282
+ * Append every future entry here; never remove entries — a retired template entry
283
+ * stays here so removal (stripUserDenyList, removeManagedSettings) and install
284
+ * convergence (retiredDenyEntries) still recognise it in an older install.
206
285
  *
207
286
  * Load-time assertion below verifies this is a superset of the current template.
208
287
  */
209
288
  // D-SECURITY-01: frozen at module load — any future template entry must appear here too.
289
+ // D-SECURITY-02 (#399): the nine v1 piped rules (`Bash(curl * | bash*)` and kin) are
290
+ // RETIRED — kept here, dropped from the template. Claude Code splits a Bash command at
291
+ // `|` (and `&&`, `||`, `;`, `|&`, `&`, newlines) and matches every rule against each
292
+ // subcommand alone, so a rule holding ` | ` can never match anything. The exact
293
+ // shell-on-stdin denies in the v2 batch (`Bash(bash)`, `Bash(sh -s *)`, ...) match the
294
+ // shell subcommand of such a pipeline instead.
295
+ // Only entries a release actually shipped belong here: removal and install convergence
296
+ // strip every entry this set names that the template does not, so a rule Devflow never
297
+ // shipped would be taken from a user who wrote it (ADR-024, prove-you-wrote-it).
210
298
  export const DEVFLOW_HISTORICAL_DENY = Object.freeze(new Set([
211
299
  // v1 batch — 154 entries shipped in src/targets/claude-code/templates/managed-settings.json
212
300
  'Bash(rm -rf /*)',
@@ -363,7 +451,54 @@ export const DEVFLOW_HISTORICAL_DENY = Object.freeze(new Set([
363
451
  'Read(/etc/shadow)',
364
452
  'Read(/etc/sudoers)',
365
453
  'Read(/etc/passwd)',
454
+ // v2 batch (#399) — 25 template entries: a shell reading its script from stdin,
455
+ // `zsh -c` beside the v1 `sh -c`/`bash -c`, OrbStack VM control, docker
456
+ // pull/delete/prune and whole-disk or privileged runs.
457
+ 'Bash(bash)',
458
+ 'Bash(sh)',
459
+ 'Bash(zsh)',
460
+ 'Bash(bash - *)',
461
+ 'Bash(sh - *)',
462
+ 'Bash(zsh - *)',
463
+ 'Bash(bash -s *)',
464
+ 'Bash(sh -s *)',
465
+ 'Bash(zsh -s *)',
466
+ 'Bash(zsh -c *)',
467
+ 'Bash(docker run*--privileged*)',
468
+ 'Bash(docker run*-v /:*)',
469
+ 'Bash(docker run*--volume /:*)',
470
+ 'Bash(docker run*--volume=/:*)',
471
+ 'Bash(docker pull *)',
472
+ 'Bash(docker image pull *)',
473
+ 'Bash(docker rm *)',
474
+ 'Bash(docker container rm *)',
475
+ 'Bash(docker rmi *)',
476
+ 'Bash(docker image rm *)',
477
+ 'Bash(docker volume rm *)',
478
+ 'Bash(docker*prune*)',
479
+ 'Bash(orb *)',
480
+ 'Bash(orbctl *)',
481
+ 'Bash(open *OrbStack*)',
366
482
  ]));
483
+ /**
484
+ * The Devflow deny entries an older install may carry that the current template no
485
+ * longer ships: DEVFLOW_HISTORICAL_DENY minus the template. PURE.
486
+ *
487
+ * An empty template (loadTemplateDenyEntries' failure value) retires nothing — an
488
+ * unreadable template must never read as "Devflow dropped every entry it ever shipped".
489
+ *
490
+ * Accepted trade-off (ADR-024): a deny entry is a bare string, so a user who typed a
491
+ * retired entry themselves is indistinguishable from Devflow's copy and loses it on the
492
+ * next install, exactly as `security --disable` and uninstall already strip every
493
+ * historical entry. Retire an entry only when losing a user's identical copy is
494
+ * harmless; the #399 piped rules qualify because none could ever match (D-SECURITY-02).
495
+ */
496
+ export function retiredDenyEntries(templateEntries) {
497
+ if (templateEntries.length === 0)
498
+ return new Set();
499
+ const current = new Set(templateEntries);
500
+ return new Set([...DEVFLOW_HISTORICAL_DENY].filter(e => !current.has(e)));
501
+ }
367
502
  /**
368
503
  * Assert that DEVFLOW_HISTORICAL_DENY is a superset of the provided template entries.
369
504
  * Throws if any template entry is missing from the historical set.
@@ -539,8 +674,8 @@ export function resolveSecurityAction(flag, manifestMode, detected, isTTY) {
539
674
  }
540
675
  /**
541
676
  * Load the deny entry array from the managed-settings.json template.
542
- * Canonical single-source helper used by installManagedSettings, removeManagedSettings,
543
- * init.ts's security step, and security.ts's --enable/--disable paths.
677
+ * Canonical single-source helper used by installManagedSettings, init.ts's security
678
+ * step, and security.ts's --enable path. Removal keys on DEVFLOW_HISTORICAL_DENY instead.
544
679
  *
545
680
  * Defensive read: treats file as `Record<string, unknown>`, guards with Array.isArray,
546
681
  * coerces each element to string. Returns [] on any read or parse failure (never throws).
@@ -584,7 +719,7 @@ export async function installManagedSettings(rootDir, verbose) {
584
719
  let content;
585
720
  try {
586
721
  const existing = await fs.readFile(managedPath, 'utf-8');
587
- content = mergeDenyList(existing, newDenyEntries);
722
+ content = mergeDenyList(existing, newDenyEntries, retiredDenyEntries(newDenyEntries));
588
723
  }
589
724
  catch {
590
725
  // File doesn't exist — use template as-is
@@ -669,15 +804,11 @@ export async function removeManagedSettings(rootDir, verbose, managedPathOverrid
669
804
  catch {
670
805
  return false; // File doesn't exist
671
806
  }
672
- // Load our deny entries to identify which to remove
673
- const devflowDenyEntries = await loadTemplateDenyEntries(rootDir);
674
- if (devflowDenyEntries.length === 0) {
675
- return false;
676
- }
807
+ // Key on every entry Devflow has ever shipped, not the current template: an install
808
+ // from an older release carries entries the template has since retired (D-SECURITY-02).
677
809
  const existing = JSON.parse(existingContent);
678
810
  const currentDeny = existing.permissions?.deny ?? [];
679
- const devflowSet = new Set(devflowDenyEntries);
680
- const remaining = currentDeny.filter(entry => !devflowSet.has(entry));
811
+ const remaining = currentDeny.filter(entry => !DEVFLOW_HISTORICAL_DENY.has(entry));
681
812
  // Determine the target action: delete file entirely or write updated content
682
813
  let shouldDelete = false;
683
814
  let updatedContent = null;
@@ -771,8 +902,8 @@ export async function applyUserSecurityDenyList(settingsPath, currentTemplateDen
771
902
  catch {
772
903
  existing = '{}';
773
904
  }
774
- const merged = mergeDenyList(existing, currentTemplateDeny);
775
- await writeFileAtomicExclusive(settingsPath, merged);
905
+ const merged = mergeDenyList(existing, currentTemplateDeny, retiredDenyEntries(currentTemplateDeny));
906
+ await writeSettingsFileAtomic(settingsPath, merged);
776
907
  return merged;
777
908
  }
778
909
  /**
@@ -780,7 +911,7 @@ export async function applyUserSecurityDenyList(settingsPath, currentTemplateDen
780
911
  * Colocated with applyUserSecurityDenyList — the remove-side counterpart.
781
912
  *
782
913
  * Sequence: read → stripUserDenyList → guard (stripped !== existing) →
783
- * writeFileAtomicExclusive → return { removed }.
914
+ * writeSettingsFileAtomic → return { removed }.
784
915
  * Atomic write (temp+rename) upholds the never-truncate-on-crash invariant.
785
916
  * ENOENT is swallowed (file absent = nothing to strip). Other errors propagate.
786
917
  *
@@ -805,7 +936,7 @@ export async function stripUserSecurityDenyList(settingsPath) {
805
936
  if (stripped === existing) {
806
937
  return null;
807
938
  }
808
- await writeFileAtomicExclusive(settingsPath, stripped);
939
+ await writeSettingsFileAtomic(settingsPath, stripped);
809
940
  return { removed };
810
941
  }
811
942
  /** True for a non-null, non-array object literal. */
@@ -916,7 +1047,7 @@ export async function installSettings(claudeDir, rootDir, devflowDir, verbose) {
916
1047
  settingsExists = false;
917
1048
  }
918
1049
  if (!settingsExists) {
919
- await fs.writeFile(settingsPath, settingsContent, 'utf-8');
1050
+ await writeSettingsFileAtomic(settingsPath, settingsContent);
920
1051
  if (verbose) {
921
1052
  p.log.success('Settings configured');
922
1053
  }
@@ -943,7 +1074,7 @@ export async function installSettings(claudeDir, rootDir, devflowDir, verbose) {
943
1074
  // Already fully configured — nothing to do
944
1075
  return;
945
1076
  }
946
- await writeFileAtomicExclusive(settingsPath, JSON.stringify(existingParsed, null, 2) + '\n');
1077
+ await writeSettingsFileAtomic(settingsPath, JSON.stringify(existingParsed, null, 2) + '\n');
947
1078
  if (verbose) {
948
1079
  p.log.success('Settings updated with Devflow hooks and HUD');
949
1080
  }
@@ -981,11 +1112,12 @@ export async function installClaudeignore(gitRoot, rootDir, verbose) {
981
1112
  }
982
1113
  /**
983
1114
  * Discover git repository roots from Claude's project history.
984
- * Parses ~/.claude/history.jsonl for unique project paths that are valid git repos.
985
- * @param homeDir - Override home directory (dependency injection for tests)
1115
+ * Parses `<claudeDir>/history.jsonl` for unique project paths that are valid git repos.
1116
+ * @param claudeDir - The Claude Code directory whose history is read — the caller
1117
+ * passes getClaudeDirectory() (D-CLAUDE-CONFIG-DIR), tests a sandbox.
986
1118
  */
987
- export async function discoverProjectGitRoots(homeDir) {
988
- const historyPath = path.join(homeDir ?? os.homedir(), '.claude', 'history.jsonl');
1119
+ export async function discoverProjectGitRoots(claudeDir) {
1120
+ const historyPath = path.join(claudeDir, 'history.jsonl');
989
1121
  let content;
990
1122
  try {
991
1123
  content = await fs.readFile(historyPath, 'utf-8');
@@ -1016,53 +1148,31 @@ export async function discoverProjectGitRoots(homeDir) {
1016
1148
  .map((r) => r.value);
1017
1149
  return gitRoots.sort();
1018
1150
  }
1019
- /**
1020
- * Update .gitignore with Devflow entries (for local scope installs).
1021
- */
1022
- export async function updateGitignore(gitRoot, verbose) {
1023
- try {
1024
- const gitignorePath = path.join(gitRoot, '.gitignore');
1025
- const entriesToAdd = getGitignoreEntries();
1026
- let gitignoreContent = '';
1027
- try {
1028
- gitignoreContent = await fs.readFile(gitignorePath, 'utf-8');
1029
- }
1030
- catch { /* doesn't exist */ }
1031
- const linesToAdd = computeGitignoreAppend(gitignoreContent, entriesToAdd);
1032
- if (linesToAdd.length > 0) {
1033
- const newContent = gitignoreContent
1034
- ? `${gitignoreContent.trimEnd()}\n\n# Devflow local installation\n${linesToAdd.join('\n')}\n`
1035
- : `# Devflow local installation\n${linesToAdd.join('\n')}\n`;
1036
- await fs.writeFile(gitignorePath, newContent, 'utf-8');
1037
- if (verbose) {
1038
- p.log.success('.gitignore updated');
1039
- }
1040
- }
1041
- }
1042
- catch (error) {
1043
- if (verbose) {
1044
- p.log.warn(`Could not update .gitignore: ${error instanceof Error ? error.message : error}`);
1045
- }
1046
- }
1047
- }
1048
1151
  /**
1049
1152
  * Current carve-out marker version. Bump when the block format changes — together
1050
1153
  * with the shell twin's stamp (ensure-root-gitignore) and the ensure-devflow-init
1051
- * fast path, in one commit (D-GITIGNORE-V5).
1154
+ * fast path, in one commit.
1155
+ *
1156
+ * D-GITIGNORE-V6 (#392): v6 adds the `!.devflow/project.json` completion line. A
1157
+ * v5-stamped project misses the v6 fast path once, gains that line (and nothing else
1158
+ * — its comment and every other line stay byte-identical) and is re-stamped v6, so
1159
+ * a team can commit `.devflow/project.json` without `git add -f`. v2–v5 inputs all
1160
+ * converge on the same completion order: policy, project, `.claudeignore`.
1052
1161
  */
1053
- const GITIGNORE_MARKER_V5 = '.root-gitignore-configured-v5';
1162
+ const GITIGNORE_MARKER_V6 = '.root-gitignore-configured-v6';
1054
1163
  /**
1055
1164
  * Earlier markers, the unversioned (v1) one included — every one is removed
1056
- * whenever the project is v5-stamped, on the fast path too: an older devflow can
1057
- * re-stamp one beside v5, and the shell twin drops the same four.
1165
+ * whenever the project is v6-stamped, on the fast path too: an older devflow can
1166
+ * re-stamp one beside v6, and the shell twin drops the same five.
1058
1167
  */
1059
1168
  const LEGACY_GITIGNORE_MARKERS = [
1169
+ '.root-gitignore-configured-v5',
1060
1170
  '.root-gitignore-configured-v4',
1061
1171
  '.root-gitignore-configured-v3',
1062
1172
  '.root-gitignore-configured-v2',
1063
1173
  '.root-gitignore-configured',
1064
1174
  ];
1065
- /** Remove every legacy marker; an absent one is a no-op. Call only once v5 is stamped. */
1175
+ /** Remove every legacy marker; an absent one is a no-op. Call only once v6 is stamped. */
1066
1176
  async function removeLegacyGitignoreMarkers(devflowDir) {
1067
1177
  for (const legacy of LEGACY_GITIGNORE_MARKERS) {
1068
1178
  try {
@@ -1073,23 +1183,23 @@ async function removeLegacyGitignoreMarkers(devflowDir) {
1073
1183
  }
1074
1184
  /**
1075
1185
  * Deterministically ensure the project root .gitignore applies the `.devflow/`
1076
- * carve-out (local by default; feature knowledge, conventions.md and the evidence
1077
- * policy shared via git).
1186
+ * carve-out (local by default; feature knowledge, conventions.md, the evidence
1187
+ * policy and the project settings shared via git).
1078
1188
  *
1079
- * Manages ONLY `.devflow/` — never `.claude/` — because user-scope installs must
1080
- * not gitignore `.claude/`. This is the init-time counterpart to the always-on
1189
+ * Manages ONLY `.devflow/` — never `.claude/` — because a project's `.claude/`
1190
+ * is its own to share or ignore. This is the init-time counterpart to the always-on
1081
1191
  * src/assets/scripts/hooks/ensure-root-gitignore shell helper; both resolve the same
1082
1192
  * shape for a given .gitignore — DEVFLOW_GITIGNORE_BLOCK, or
1083
1193
  * DEVFLOW_GITIGNORE_BLOCK_WITHOUT_CLAUDEIGNORE when the project owns that entry — and
1084
1194
  * emit identical bytes, so the two paths are byte-compatible and mutually idempotent.
1085
- * Called unconditionally (independent of install scope and every feature toggle)
1086
- * whenever a git root is known.
1195
+ * Called unconditionally (independent of every feature toggle) whenever a git
1196
+ * root is known.
1087
1197
  *
1088
- * Uses a versioned project-local marker file (`.devflow/.root-gitignore-configured-v5`)
1198
+ * Uses a versioned project-local marker file (`.devflow/.root-gitignore-configured-v6`)
1089
1199
  * for fast-path detection — the same pattern as the shell twin. The marker is a claim,
1090
1200
  * not proof, so even a marked install re-reads .gitignore and re-runs
1091
1201
  * computeDevflowGitignore; bumping the version forces a re-run once per install, which
1092
- * is how a v4-marked project gains the policy line and is re-stamped v5.
1202
+ * is how a v5-marked project gains the project line and is re-stamped v6.
1093
1203
  *
1094
1204
  * Idempotent: computeDevflowGitignore returns null for a converged file, so a
1095
1205
  * marked install performs one read and no write. Errors are swallowed
@@ -1098,20 +1208,20 @@ async function removeLegacyGitignoreMarkers(devflowDir) {
1098
1208
  export async function ensureDevflowGitignore(gitRoot, verbose) {
1099
1209
  try {
1100
1210
  const devflowDir = path.join(gitRoot, '.devflow');
1101
- const markerV5 = path.join(devflowDir, GITIGNORE_MARKER_V5);
1211
+ const markerV6 = path.join(devflowDir, GITIGNORE_MARKER_V6);
1102
1212
  const gitignorePath = path.join(gitRoot, '.gitignore');
1103
- // Fast-path with verification: v5 marker normally means the block is installed,
1213
+ // Fast-path with verification: v6 marker normally means the block is installed,
1104
1214
  // but the marker is a claim, not proof — a merge-conflict resolution may have
1105
1215
  // dropped the block. Even when the marker exists, read .gitignore (one cheap
1106
1216
  // read) and run computeDevflowGitignore; write only when it returns non-null.
1107
1217
  // Idempotent: converged file → computeDevflowGitignore returns null → no write.
1108
- let v5Marked = false;
1218
+ let v6Marked = false;
1109
1219
  try {
1110
- await fs.access(markerV5);
1111
- v5Marked = true;
1220
+ await fs.access(markerV6);
1221
+ v6Marked = true;
1112
1222
  }
1113
1223
  catch { /* absent */ }
1114
- if (v5Marked) {
1224
+ if (v6Marked) {
1115
1225
  let existingContent = '';
1116
1226
  try {
1117
1227
  existingContent = await fs.readFile(gitignorePath, 'utf-8');
@@ -1121,7 +1231,7 @@ export async function ensureDevflowGitignore(gitRoot, verbose) {
1121
1231
  if (healContent !== null) {
1122
1232
  await fs.writeFile(gitignorePath, healContent, 'utf-8');
1123
1233
  if (verbose) {
1124
- p.log.success('.gitignore configured (.devflow/ local; feature knowledge + conventions + evidence policy shared)');
1234
+ p.log.success('.gitignore configured (.devflow/ local; feature knowledge + conventions + retired policy.json + project settings shared)');
1125
1235
  }
1126
1236
  }
1127
1237
  await removeLegacyGitignoreMarkers(devflowDir);
@@ -1136,12 +1246,12 @@ export async function ensureDevflowGitignore(gitRoot, verbose) {
1136
1246
  if (newContent !== null) {
1137
1247
  await fs.writeFile(gitignorePath, newContent, 'utf-8');
1138
1248
  if (verbose) {
1139
- p.log.success('.gitignore configured (.devflow/ local; feature knowledge + conventions + evidence policy shared)');
1249
+ p.log.success('.gitignore configured (.devflow/ local; feature knowledge + conventions + retired policy.json + project settings shared)');
1140
1250
  }
1141
1251
  }
1142
- // Stamp v5 marker so subsequent runs fast-path; drop every legacy marker.
1252
+ // Stamp v6 marker so subsequent runs fast-path; drop every legacy marker.
1143
1253
  await fs.mkdir(devflowDir, { recursive: true });
1144
- await fs.writeFile(markerV5, '', 'utf-8');
1254
+ await fs.writeFile(markerV6, '', 'utf-8');
1145
1255
  await removeLegacyGitignoreMarkers(devflowDir);
1146
1256
  }
1147
1257
  catch (error) {
@@ -1150,21 +1260,4 @@ export async function ensureDevflowGitignore(gitRoot, verbose) {
1150
1260
  }
1151
1261
  }
1152
1262
  }
1153
- /**
1154
- * Create .devflow/docs/ directory structure for Devflow artifacts.
1155
- */
1156
- export async function createDocsStructure(verbose) {
1157
- const docsDir = getDocsDir(process.cwd());
1158
- try {
1159
- await Promise.all([
1160
- fs.mkdir(path.join(docsDir, 'status', 'compact'), { recursive: true }),
1161
- fs.mkdir(path.join(docsDir, 'reviews'), { recursive: true }),
1162
- fs.mkdir(path.join(docsDir, 'releases'), { recursive: true }),
1163
- ]);
1164
- if (verbose) {
1165
- p.log.success('.devflow/docs/ structure ready');
1166
- }
1167
- }
1168
- catch { /* may already exist */ }
1169
- }
1170
1263
  //# sourceMappingURL=post-install.js.map