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
@@ -6,15 +6,20 @@
6
6
  * Applies ADR-001: compliance is manifest-group (like proxy), not config.json-gated.
7
7
  * Avoids PF-009: per-artifact failures are warn-not-throw.
8
8
  * Avoids PF-015: enable/disable each converge BOTH artifacts unconditionally.
9
+ * The evidence-policy lines (--status, and the --enable/--set suggestion) come
10
+ * from src/core/evidence-policy.ts, the seam onto the package's own resolvers;
11
+ * the CLI prints the keys to add to .devflow/project.json and never writes it
12
+ * (applies ADR-024).
9
13
  */
10
14
  import { Command } from 'commander';
11
15
  import { promises as fs } from 'fs';
12
16
  import * as path from 'path';
13
17
  import * as p from '@clack/prompts';
14
18
  import color from 'picocolors';
15
- import { ALWAYS_PRESENT_REFS, COMPLIANCE_FRAMEWORKS, normalizeFrameworks, parseFrameworkList, } from '../../core/compliance.js';
19
+ import { COMPLIANCE_FRAMEWORKS, normalizeFrameworks, parseFrameworkList, } from '../../core/compliance.js';
16
20
  import { frameworkChoices, FRAMEWORK_SELECT_MESSAGE } from './compliance-prompts.js';
17
21
  import { COMPLIANCE_SKILL_TOKENS } from '../../core/compliance-compose.js';
22
+ import { evidencePolicyStatusLine, evidencePolicySuggestion, formatEvidencePolicyUnavailable, loadEvidencePolicyModule, loadSettingsModule, repoComplianceStatusLines, } from '../../core/evidence-policy.js';
18
23
  import { readManifest, writeManifest } from '../../core/manifest.js';
19
24
  import { convergeFromManifest } from '../../targets/claude-code/compliance-install.js';
20
25
  import { validateRuleShadow, validateSkillShadow } from '../../targets/claude-code/installer.js';
@@ -61,7 +66,7 @@ export function resolveComplianceCliAction(current, action, setFrameworks) {
61
66
  messages: [
62
67
  {
63
68
  level: 'success',
64
- text: 'Compliance disabled — artifacts removed, frameworks remembered for re-enable',
69
+ text: 'Compliance disabled — rule removed, frameworks remembered for re-enable',
65
70
  },
66
71
  ],
67
72
  };
@@ -89,23 +94,17 @@ export function resolveComplianceCliAction(current, action, setFrameworks) {
89
94
  }
90
95
  }
91
96
  }
92
- // ── Drift classification ───────────────────────────────────────────────────────
97
+ // ── Manifest classification ────────────────────────────────────────────────────
93
98
  /**
94
- * Classify a list of manifest framework IDs that are not currently installed.
95
- * Separates valid (registry-known) IDs from invalid (unknown) IDs so the status
96
- * display can recommend the correct remediation for each class.
99
+ * The manifest framework IDs the registry does not know — a hand-edited or
100
+ * newer-devflow manifest. Every install drops them (normalizeFrameworks), so
101
+ * `--status` names them with the one remedy that removes them: `--set`.
97
102
  *
98
- * Called by the --status handler to compute drift between the manifest and
99
- * installed artifacts. Also exported to allow unit testing of the classification
100
- * logic in isolation.
103
+ * Installed reference files are no drift signal: every install carries all six
104
+ * (D-COMPLIANCE-INSTALL-ALWAYS), whatever the manifest selects.
101
105
  */
102
- export function classifyDriftMissing(manifestFrameworks, installedRefIds, registryIds) {
103
- const installedSet = new Set(installedRefIds);
104
- const missing = manifestFrameworks.filter(id => !installedSet.has(id));
105
- return {
106
- validMissing: missing.filter(id => registryIds.has(id)),
107
- invalidIds: missing.filter(id => !registryIds.has(id)),
108
- };
106
+ export function unknownFrameworkIds(manifestFrameworks, registryIds) {
107
+ return manifestFrameworks.filter(id => !registryIds.has(id));
109
108
  }
110
109
  // ── Status helpers ─────────────────────────────────────────────────────────────
111
110
  /** Returns true if the compliance skill dir exists at the install target. */
@@ -118,23 +117,6 @@ async function skillInstalled(claudeDir) {
118
117
  return false;
119
118
  }
120
119
  }
121
- /** Returns the set of installed framework reference IDs from the skill dir. */
122
- async function installedRefIds(claudeDir) {
123
- const refDir = path.join(claudeDir, 'skills', 'devflow:compliance', 'references');
124
- try {
125
- const entries = await fs.readdir(refDir);
126
- return entries
127
- .filter(e => e.endsWith('.md') && !ALWAYS_PRESENT_REFS.includes(e))
128
- .map(e => path.basename(e, '.md'))
129
- // S58: sanitize — keep only entries whose basename matches the expected id
130
- // shape (lowercase letters, digits, hyphens). Strips terminal-escape sequences
131
- // or path segments that could be injected via a crafted filename.
132
- .filter(id => /^[a-z0-9-]+$/.test(id));
133
- }
134
- catch {
135
- return [];
136
- }
137
- }
138
120
  /** Returns true if the compliance rule file exists at the install target. */
139
121
  async function ruleInstalled(claudeDir) {
140
122
  try {
@@ -176,8 +158,8 @@ async function skillShadowState(devflowDir) {
176
158
  export const complianceCommand = new Command('compliance')
177
159
  .description('Enable, disable, or configure the compliance feature')
178
160
  .option('--enable', 'Enable compliance (restores previously selected frameworks)')
179
- .option('--disable', 'Disable compliance (artifacts removed; frameworks remembered for re-enable)')
180
- .option('--status', 'Show compliance state: manifest, installed artifacts, and shadow presence')
161
+ .option('--disable', 'Disable compliance (rule removed; frameworks remembered for re-enable)')
162
+ .option('--status', 'Show compliance state: manifest, installed artifacts, shadow presence, and the evidence policy for the current repository')
181
163
  .option('--set <list>', 'Set active frameworks (comma-separated IDs); enables compliance. Use --set "" for zero frameworks (generic controls only)')
182
164
  .action(async (options) => {
183
165
  const claudeDir = getClaudeDirectory();
@@ -217,24 +199,22 @@ export const complianceCommand = new Command('compliance')
217
199
  ? current.frameworks.join(', ')
218
200
  : color.dim('none declared');
219
201
  const rulesEnabled = manifest.features.rules;
220
- const [skillOk, refIds, ruleOk, isRuleShadowed, skillShadow] = await Promise.all([
202
+ const [skillOk, ruleOk, isRuleShadowed, skillShadow] = await Promise.all([
221
203
  skillInstalled(claudeDir),
222
- installedRefIds(claudeDir),
223
204
  ruleInstalled(claudeDir),
224
205
  ruleShadowed(devflowDir),
225
206
  skillShadowState(devflowDir),
226
207
  ]);
227
- // Detect framework drift: manifest says X, installed refs say Y.
228
- // Invalid IDs (not in the registry) are reported separately from valid-but-missing
229
- // IDs so the suggested remediation is correct: --enable can reconcile valid IDs,
230
- // but only --set can remove IDs that are not in the registry.
231
- const registrySet = new Set(COMPLIANCE_FRAMEWORKS.map(fw => fw.id));
232
- const manifestSet = new Set(current.frameworks);
233
- const driftInstalled = refIds.filter(id => !manifestSet.has(id));
234
- const { validMissing, invalidIds } = classifyDriftMissing(current.frameworks, refIds, registrySet);
208
+ const invalidIds = unknownFrameworkIds(current.frameworks, new Set(COMPLIANCE_FRAMEWORKS.map(fw => fw.id)));
209
+ // The repository's own declaration (.devflow/project.json) and, while the
210
+ // retired policy file is in the working tree, the hint to migrate it. Both come from
211
+ // the local settings resolver — one git call, no network — and add nothing
212
+ // when the repository declares nothing (the block is then unchanged).
213
+ const repoLines = repoComplianceStatusLines(loadSettingsModule(), { dir: process.cwd() });
235
214
  const lines = [
236
215
  `State: ${enabledLabel}`,
237
216
  `Frameworks: ${fwLabel}`,
217
+ ...repoLines,
238
218
  '',
239
219
  `Skill: ${skillOk ? color.green('installed') : color.dim('not installed')}` +
240
220
  (skillShadow === 'composition-skipped'
@@ -247,18 +227,14 @@ export const complianceCommand = new Command('compliance')
247
227
  ? color.yellow(' (withheld — rules disabled)')
248
228
  : '') +
249
229
  (isRuleShadowed ? color.green(' [shadowed]') : ''),
230
+ '',
231
+ // The repository in cwd, resolved by the package's own resolver with the
232
+ // compliance state already read above (D-POLICY-CJS-SEAM). Bounded: at most
233
+ // three `gh` calls, each with a timeout, so offline degrades to a flagged line.
234
+ evidencePolicyStatusLine(loadEvidencePolicyModule(), { dir: process.cwd(), compliance: current }),
250
235
  ];
251
- if (driftInstalled.length > 0 || validMissing.length > 0 || invalidIds.length > 0) {
236
+ if (invalidIds.length > 0) {
252
237
  lines.push('');
253
- if (driftInstalled.length > 0 || validMissing.length > 0) {
254
- lines.push(color.yellow('Artifact drift detected (run devflow compliance --enable to reconcile):'));
255
- if (driftInstalled.length > 0) {
256
- lines.push(` Installed not in manifest: ${driftInstalled.join(', ')}`);
257
- }
258
- if (validMissing.length > 0) {
259
- lines.push(` In manifest but not installed: ${validMissing.join(', ')}`);
260
- }
261
- }
262
238
  for (const id of invalidIds) {
263
239
  lines.push(color.red(` unknown framework id in manifest (ignored): ${id} — remove with --set`));
264
240
  }
@@ -333,5 +309,22 @@ export const complianceCommand = new Command('compliance')
333
309
  p.log.info(color.dim('Note: compliance rule withheld (rules disabled) — ' +
334
310
  'run `devflow rules --enable` to install the stamped rule'));
335
311
  }
312
+ // Suggest the team file compliance now implies. Printed, never written:
313
+ // .devflow/project.json is team-owned (D-POLICY-NO-WRITE, applies ADR-024).
314
+ if (resolved.nextState.enabled) {
315
+ const policyModule = loadEvidencePolicyModule();
316
+ const settingsModule = loadSettingsModule();
317
+ if (!policyModule.ok) {
318
+ p.log.warn(formatEvidencePolicyUnavailable(policyModule.error));
319
+ }
320
+ else if (!settingsModule.ok) {
321
+ p.log.warn(formatEvidencePolicyUnavailable(settingsModule.error));
322
+ }
323
+ else {
324
+ const suggestion = evidencePolicySuggestion(resolved.nextState, policyModule.value, settingsModule.value);
325
+ if (suggestion !== null)
326
+ p.note(suggestion, 'Evidence policy');
327
+ }
328
+ }
336
329
  });
337
330
  //# sourceMappingURL=compliance.js.map
@@ -1,9 +1,16 @@
1
- import * as path from 'path';
1
+ import { devflowHookOwner, hasHook, removeHooks, runHookCommand, } from '../../targets/claude-code/hooks.js';
2
2
  // ─── Context hook utilities ────────────────────────────────────────────────
3
3
  //
4
4
  // The session-start-context hook is always-on (registered unconditionally by
5
5
  // init, removed by uninstall). It has internal sentinel awareness per feature.
6
6
  const CONTEXT_HOOK_MARKER = 'session-start-context';
7
+ /**
8
+ * D-EXACT-HOOK-OWNER: the context hook is devflow's only when its command ends in
9
+ * `/scripts/hooks/run-hook session-start-context` (hooks.ts), under any directory.
10
+ * It has been registered through run-hook since it first shipped, so there is no
11
+ * legacy form to recognise.
12
+ */
13
+ const isContextHook = devflowHookOwner([CONTEXT_HOOK_MARKER]);
7
14
  /**
8
15
  * Add the session-start-context hook to SessionStart in settings JSON.
9
16
  * Idempotent — returns unchanged JSON if hook already present.
@@ -13,46 +20,24 @@ export function addContextHook(settingsJson, devflowDir) {
13
20
  return settingsJson;
14
21
  }
15
22
  const settings = JSON.parse(settingsJson);
16
- if (!settings.hooks) {
17
- settings.hooks = {};
18
- }
19
- const hookCommand = path.join(devflowDir, 'scripts', 'hooks', 'run-hook') + ` ${CONTEXT_HOOK_MARKER}`;
20
- const newEntry = {
21
- hooks: [
22
- {
23
- type: 'command',
24
- command: hookCommand,
25
- timeout: 10,
26
- },
27
- ],
28
- };
29
- if (!settings.hooks.SessionStart) {
30
- settings.hooks.SessionStart = [];
31
- }
32
- settings.hooks.SessionStart.push(newEntry);
23
+ settings.hooks ??= {};
24
+ settings.hooks.SessionStart ??= [];
25
+ settings.hooks.SessionStart.push({
26
+ hooks: [{ type: 'command', command: runHookCommand(devflowDir, CONTEXT_HOOK_MARKER), timeout: 10 }],
27
+ });
33
28
  return JSON.stringify(settings, null, 2) + '\n';
34
29
  }
35
30
  /**
36
31
  * Remove the session-start-context hook from settings JSON.
37
32
  * Idempotent — returns unchanged JSON if hook not present.
38
- * Preserves all other SessionStart hooks.
33
+ * Removes the single hook, so the other hooks of its matcher group and every
34
+ * other SessionStart group stay in place (D-EXACT-HOOK-OWNER).
39
35
  */
40
36
  export function removeContextHook(settingsJson) {
41
37
  const settings = JSON.parse(settingsJson);
42
- if (!settings.hooks?.SessionStart) {
38
+ if (!removeHooks(settings, 'SessionStart', isContextHook)) {
43
39
  return settingsJson;
44
40
  }
45
- const before = settings.hooks.SessionStart.length;
46
- settings.hooks.SessionStart = settings.hooks.SessionStart.filter((matcher) => !matcher.hooks.some((h) => h.command.includes(CONTEXT_HOOK_MARKER)));
47
- if (settings.hooks.SessionStart.length === before) {
48
- return settingsJson;
49
- }
50
- if (settings.hooks.SessionStart.length === 0) {
51
- delete settings.hooks.SessionStart;
52
- }
53
- if (Object.keys(settings.hooks).length === 0) {
54
- delete settings.hooks;
55
- }
56
41
  return JSON.stringify(settings, null, 2) + '\n';
57
42
  }
58
43
  /**
@@ -61,6 +46,6 @@ export function removeContextHook(settingsJson) {
61
46
  */
62
47
  export function hasContextHook(input) {
63
48
  const settings = typeof input === 'string' ? JSON.parse(input) : input;
64
- return settings.hooks?.SessionStart?.some((matcher) => matcher.hooks.some((h) => h.command.includes(CONTEXT_HOOK_MARKER))) ?? false;
49
+ return hasHook(settings, 'SessionStart', isContextHook);
65
50
  }
66
51
  //# sourceMappingURL=context.js.map
@@ -4,17 +4,47 @@ import * as path from 'path';
4
4
  import * as p from '@clack/prompts';
5
5
  import color from 'picocolors';
6
6
  import { getClaudeDirectory, getHomeDirectory } from '../../targets/claude-code/claude-paths.js';
7
- // ─── Pure functions — no I/O, fully testable ─────────────────────────────────
7
+ import { writeSettingsFileAtomic } from '../../core/fs-atomic.js';
8
+ function isPlainObject(value) {
9
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
10
+ }
11
+ /**
12
+ * The settings object and its `env` (undefined when absent), or why not.
13
+ *
14
+ * D-DEBUG-ENV-OBJECT: Claude Code reads `env` as an object of variables. A
15
+ * present `env` that is anything else — an array, a string, `null` — is
16
+ * rejected, never repaired and never written through: a key set on an array is
17
+ * dropped by JSON.stringify, so the command would report success and write
18
+ * nothing, and replacing the value would discard what the user wrote.
19
+ */
20
+ function parseSettingsEnv(settingsJson) {
21
+ let settings;
22
+ try {
23
+ settings = JSON.parse(settingsJson);
24
+ }
25
+ catch {
26
+ return { ok: false, error: { kind: 'malformed' } };
27
+ }
28
+ if (!isPlainObject(settings))
29
+ return { ok: false, error: { kind: 'malformed' } };
30
+ if (!Object.prototype.hasOwnProperty.call(settings, 'env'))
31
+ return { ok: true, settings, env: undefined };
32
+ const env = settings.env;
33
+ if (!isPlainObject(env))
34
+ return { ok: false, error: { kind: 'env-not-object' } };
35
+ return { ok: true, settings, env };
36
+ }
8
37
  /**
9
38
  * Apply DEVFLOW_HOOK_DEBUG=1 to a settings JSON string.
10
39
  * Returns a new serialized settings string. Does not mutate.
11
40
  * Follows the applyFlags pattern from flags.ts.
12
41
  */
13
42
  export function applyDebugTrace(settingsJson) {
14
- const settings = JSON.parse(settingsJson);
15
- settings.env ??= {};
16
- settings.env.DEVFLOW_HOOK_DEBUG = '1';
17
- return JSON.stringify(settings, null, 2) + '\n';
43
+ const parsed = parseSettingsEnv(settingsJson);
44
+ if (!parsed.ok)
45
+ return parsed;
46
+ const next = { ...parsed.settings, env: { ...parsed.env, DEVFLOW_HOOK_DEBUG: '1' } };
47
+ return { ok: true, value: JSON.stringify(next, null, 2) + '\n' };
18
48
  }
19
49
  /**
20
50
  * Remove DEVFLOW_HOOK_DEBUG from a settings JSON string.
@@ -23,15 +53,28 @@ export function applyDebugTrace(settingsJson) {
23
53
  * Follows the stripFlags pattern from flags.ts.
24
54
  */
25
55
  export function stripDebugTrace(settingsJson) {
26
- const settings = JSON.parse(settingsJson);
27
- const env = settings.env;
28
- if (env) {
29
- delete env.DEVFLOW_HOOK_DEBUG;
30
- if (Object.keys(env).length === 0) {
31
- delete settings.env;
56
+ const parsed = parseSettingsEnv(settingsJson);
57
+ if (!parsed.ok)
58
+ return parsed;
59
+ if (parsed.env === undefined)
60
+ return { ok: true, value: JSON.stringify(parsed.settings, null, 2) + '\n' };
61
+ const without = (obj, key) => Object.fromEntries(Object.entries(obj).filter(([k]) => k !== key));
62
+ const env = without(parsed.env, 'DEVFLOW_HOOK_DEBUG');
63
+ const next = Object.keys(env).length === 0 ? without(parsed.settings, 'env') : { ...parsed.settings, env };
64
+ return { ok: true, value: JSON.stringify(next, null, 2) + '\n' };
65
+ }
66
+ /** The message a rejected settings file gets. Pure. */
67
+ export function describeDebugSettingsError(error) {
68
+ switch (error.kind) {
69
+ case 'malformed':
70
+ return 'settings.json is malformed — fix it before modifying env vars';
71
+ case 'env-not-object':
72
+ return 'settings.json has an "env" that is not an object — fix it before modifying env vars';
73
+ default: {
74
+ const exhaustive = error;
75
+ return exhaustive;
32
76
  }
33
77
  }
34
- return JSON.stringify(settings, null, 2) + '\n';
35
78
  }
36
79
  /**
37
80
  * Read the debug tracing state from a settings JSON string.
@@ -90,29 +133,25 @@ export const debugCommand = new Command('debug')
90
133
  settingsJson = '{}';
91
134
  }
92
135
  if (options.enable) {
93
- let updated;
94
- try {
95
- updated = applyDebugTrace(settingsJson);
96
- }
97
- catch {
98
- p.log.error('settings.json is malformed — fix it before modifying env vars');
136
+ const updated = applyDebugTrace(settingsJson);
137
+ if (!updated.ok) {
138
+ p.log.error(describeDebugSettingsError(updated.error));
139
+ process.exitCode = 1;
99
140
  return;
100
141
  }
101
- await fs.writeFile(settingsPath, updated, 'utf-8');
142
+ await writeSettingsFileAtomic(settingsPath, updated.value);
102
143
  p.log.success('Hook debug tracing enabled');
103
144
  p.log.info(color.dim('Remember to disable after debugging: devflow debug --disable'));
104
145
  return;
105
146
  }
106
147
  if (options.disable) {
107
- let updated;
108
- try {
109
- updated = stripDebugTrace(settingsJson);
110
- }
111
- catch {
112
- p.log.error('settings.json is malformed — fix it before modifying env vars');
148
+ const updated = stripDebugTrace(settingsJson);
149
+ if (!updated.ok) {
150
+ p.log.error(describeDebugSettingsError(updated.error));
151
+ process.exitCode = 1;
113
152
  return;
114
153
  }
115
- await fs.writeFile(settingsPath, updated, 'utf-8');
154
+ await writeSettingsFileAtomic(settingsPath, updated.value);
116
155
  p.log.success('Hook debug tracing disabled');
117
156
  return;
118
157
  }
@@ -23,7 +23,7 @@ import color from 'picocolors';
23
23
  import { getClaudeDirectory, getDevFlowDirectory, } from '../../targets/claude-code/claude-paths.js';
24
24
  import { FLAG_REGISTRY, findFlag, convergeFlagsIntoSettings, parseFlagValueInput, formatFlagValue, effectiveDisplay, neutralValueOf, describeFlagKind, expectedInputFor, } from '../../core/flags.js';
25
25
  import { readManifest, writeManifest } from '../../core/manifest.js';
26
- import { writeFileAtomicExclusive } from '../../core/fs-atomic.js';
26
+ import { writeSettingsFileAtomic } from '../../core/fs-atomic.js';
27
27
  import { sanitizeCell } from '../tui/cells.js';
28
28
  // Static imports for pure view-state helpers — no TTY machinery (applies PF-017).
29
29
  // runFlagsTui stays lazily imported in handleBare to keep TTY module out of
@@ -98,7 +98,7 @@ manifest, opts = { viewModeExplicit: false }) {
98
98
  // Settings write — independent error path (avoids PF-015 fan-out).
99
99
  const settingsPath = path.join(claudeDir, 'settings.json');
100
100
  try {
101
- await writeFileAtomicExclusive(settingsPath, updatedSettings);
101
+ await writeSettingsFileAtomic(settingsPath, updatedSettings);
102
102
  }
103
103
  catch (err) {
104
104
  p.log.error(`Failed to write settings.json: ${err instanceof Error ? err.message : String(err)}`);
@@ -456,7 +456,7 @@ async function handleBare(claudeDir, devflowDir) {
456
456
  // The read captured before runFlagsTui is a stale snapshot by the time the
457
457
  // user saves — any concurrent writer (proxy enable, devflow agents, Claude
458
458
  // Code /config) that ran during the session would be silently overwritten by
459
- // the atomic rename in writeFileAtomicExclusive. Re-reading rebases the flag
459
+ // the atomic rename in writeSettingsFileAtomic. Re-reading rebases the flag
460
460
  // write onto current content and ensures convergeFlagsIntoSettings sees the
461
461
  // fresh viewMode (applies PF-022 — file state, not config state, is reality).
462
462
  const freshSettings = await readSettingsSafe(path.join(claudeDir, 'settings.json'));
@@ -5,12 +5,13 @@ import * as p from '@clack/prompts';
5
5
  import color from 'picocolors';
6
6
  import { getClaudeDirectory, getDevFlowDirectory } from '../../targets/claude-code/claude-paths.js';
7
7
  import { syncManifestFeature } from '../../core/manifest.js';
8
- import { writeFileAtomicExclusive } from '../../core/fs-atomic.js';
8
+ import { writeSettingsFileAtomic } from '../../core/fs-atomic.js';
9
9
  import { HUD_COMPONENTS, loadConfig, saveConfig, } from '../../hud/config.js';
10
10
  /**
11
11
  * Add the HUD statusLine to settings JSON.
12
12
  * Idempotent — returns unchanged JSON if HUD already set.
13
- * Upgrades legacy statusline.sh to hud.sh automatically.
13
+ * Upgrades devflow's legacy statusline.sh to hud.sh automatically; a statusLine
14
+ * that is not devflow's is returned unchanged (D-HUD-EXACT-OWNER).
14
15
  */
15
16
  export function addHudStatusLine(settingsJson, devflowDir) {
16
17
  const settings = JSON.parse(settingsJson);
@@ -54,16 +55,39 @@ export function hasHudStatusLine(settingsJson) {
54
55
  return false;
55
56
  return isDevFlowStatusLine(settings.statusLine);
56
57
  }
58
+ /**
59
+ * The statusLine command endings devflow has ever written: the HUD, and the
60
+ * pre-HUD `statusline.sh` it replaced.
61
+ */
62
+ const DEVFLOW_STATUSLINE_SUFFIXES = [
63
+ '/.devflow/scripts/hud.sh',
64
+ '/.devflow/scripts/statusline.sh',
65
+ ];
57
66
  /**
58
67
  * Check if an existing statusLine belongs to Devflow (HUD or legacy statusline).
59
- * Matches paths containing 'hud.sh', 'statusline.sh', or a '/devflow/' directory segment.
68
+ *
69
+ * D-HUD-EXACT-OWNER: a statusLine is devflow's only when its command ends in
70
+ * `/.devflow/scripts/hud.sh` or the legacy `/.devflow/scripts/statusline.sh`, under
71
+ * any parent directory — so installs from the custom-directory and local-scope era
72
+ * are still recognised. A bare `statusline.sh` (the Claude Code docs' own example,
73
+ * `~/.claude/statusline.sh`) or a path that merely contains a `devflow` segment is
74
+ * the user's (applies ADR-024: remove or replace only what devflow provably wrote).
75
+ * Backslashes are read as slashes so a Windows install is matched the same way. A
76
+ * hand-edited command that is not a string is the user's, as in `endsWithAny`.
77
+ *
78
+ * Every caller converges through this one predicate: `addHudStatusLine` (init's
79
+ * settings pass with the HUD on, `init --hud-only`, `hud --enable`),
80
+ * `removeHudStatusLine` (init's settings pass with `--no-hud`, `hud --disable`,
81
+ * uninstall's `runCleanupPhase`), and `hasHudStatusLine` / `hasNonDevFlowStatusLine`
82
+ * (`hud --enable`, `hud --status`).
60
83
  */
61
84
  function isDevFlowStatusLine(statusLine) {
62
- const cmd = statusLine.command ?? '';
63
- return (cmd.includes('hud.sh') ||
64
- cmd.includes('statusline.sh') ||
65
- cmd.includes('/devflow/') ||
66
- cmd.includes('\\devflow\\'));
85
+ // Parsed from a hand-editable settings.json, so the declared type is not a guarantee.
86
+ const raw = statusLine.command;
87
+ if (typeof raw !== 'string')
88
+ return false;
89
+ const cmd = raw.trim().replace(/\\/g, '/');
90
+ return DEVFLOW_STATUSLINE_SUFFIXES.some((suffix) => cmd.endsWith(suffix));
67
91
  }
68
92
  /**
69
93
  * Check if an existing statusLine belongs to a non-Devflow tool.
@@ -171,7 +195,7 @@ export function createHudCommand() {
171
195
  }
172
196
  }
173
197
  const updated = addHudStatusLine(settingsContent, devflowDir);
174
- await writeFileAtomicExclusive(settingsPath, updated);
198
+ await writeSettingsFileAtomic(settingsPath, updated);
175
199
  }
176
200
  // Always update config and sync manifest — removing the already-enabled
177
201
  // early-return makes --enable self-healing symmetric with --disable.
@@ -209,7 +233,7 @@ export function createHudCommand() {
209
233
  const settingsContent = await fs.readFile(settingsPath, 'utf-8');
210
234
  const updated = removeHudStatusLine(settingsContent);
211
235
  if (updated !== settingsContent) {
212
- await writeFileAtomicExclusive(settingsPath, updated);
236
+ await writeSettingsFileAtomic(settingsPath, updated);
213
237
  statusLineRemoved = true;
214
238
  }
215
239
  }