devflow-kit 2.4.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/README.md +86 -18
  3. package/dist/agents/git.md +824 -0
  4. package/dist/cli/commands/agents.js +6 -1
  5. package/dist/cli/commands/attribution-prompts.js +1 -1
  6. package/dist/cli/commands/compliance-prompts.js +1 -1
  7. package/dist/cli/commands/compliance.js +23 -1
  8. package/dist/cli/commands/init-seed.js +24 -26
  9. package/dist/cli/commands/init.js +502 -71
  10. package/dist/cli/commands/install-report.js +205 -0
  11. package/dist/cli/commands/knowledge/index.js +2 -2
  12. package/dist/cli/commands/knowledge/toggle.js +27 -37
  13. package/dist/cli/commands/learning.js +37 -30
  14. package/dist/cli/commands/memory.js +79 -69
  15. package/dist/cli/commands/prompt-io.js +4 -4
  16. package/dist/cli/commands/security.js +76 -16
  17. package/dist/cli/commands/skills.js +53 -7
  18. package/dist/cli/commands/tracker-prompts.js +145 -0
  19. package/dist/cli/commands/tracker.js +405 -0
  20. package/dist/cli/commands/uninstall.js +211 -65
  21. package/dist/cli.js +2 -0
  22. package/dist/commands/bug-analysis.md +22 -4
  23. package/dist/commands/code-review.md +44 -15
  24. package/dist/commands/debug.md +20 -6
  25. package/dist/commands/dynamic-build.md +289 -67
  26. package/dist/commands/dynamic-plan.md +60 -21
  27. package/dist/commands/dynamic-profile.md +1 -1
  28. package/dist/commands/dynamic-tickets.md +58 -8
  29. package/dist/commands/explore.md +2 -2
  30. package/dist/commands/implement.md +241 -53
  31. package/dist/commands/plan.md +88 -17
  32. package/dist/commands/release.md +64 -17
  33. package/dist/commands/resolve.md +138 -58
  34. package/dist/commands/self-review.md +2 -2
  35. package/dist/core/agent-models.js +55 -12
  36. package/dist/core/assets.js +58 -2
  37. package/dist/core/evidence-policy.js +147 -0
  38. package/dist/core/feature-config.js +130 -64
  39. package/dist/core/feature-switch.js +112 -0
  40. package/dist/core/flags.js +4 -4
  41. package/dist/core/manifest.js +33 -7
  42. package/dist/core/mds-variants.js +861 -0
  43. package/dist/core/model-discovery.js +12 -1
  44. package/dist/core/plugins.js +357 -9
  45. package/dist/core/project-paths.js +1 -1
  46. package/dist/core/proxy-log.js +8 -6
  47. package/dist/core/proxy-state.js +11 -8
  48. package/dist/core/reference-sweep.js +136 -0
  49. package/dist/core/tracker.js +407 -0
  50. package/dist/skills/git/references/decision-markers.md +19 -0
  51. package/dist/skills/git/references/learn-conventions.md +56 -0
  52. package/dist/skills/git/references/pr/check-ci-status.md +14 -0
  53. package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
  54. package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
  55. package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
  56. package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
  57. package/dist/skills/git/references/pr/post-review-summary.md +42 -0
  58. package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
  59. package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
  60. package/dist/skills/git/references/pr/validate-branch.md +18 -0
  61. package/dist/skills/git/references/publication-gate.md +13 -0
  62. package/dist/skills/git/references/tracker/_mcp.md +153 -0
  63. package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
  64. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
  65. package/dist/skills/git/references/tracker/github/create-release.md +11 -0
  66. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
  67. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
  68. package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
  69. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
  70. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
  71. package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
  72. package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
  73. package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
  74. package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
  75. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
  76. package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
  77. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
  78. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
  79. package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
  80. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
  81. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
  82. package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
  83. package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
  84. package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
  85. package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
  86. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
  87. package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
  88. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
  89. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
  90. package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
  91. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
  92. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
  93. package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
  94. package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
  95. package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
  96. package/dist/skills/git/references/trust-rule.md +7 -0
  97. package/dist/targets/claude-code/installer.js +1213 -31
  98. package/dist/targets/claude-code/legacy.js +5 -0
  99. package/dist/targets/claude-code/post-install.js +196 -74
  100. package/dist/targets/claude-code/tracker-install.js +161 -0
  101. package/package.json +4 -3
  102. package/src/assets/agents/code.md +42 -4
  103. package/src/assets/agents/design.md +1 -1
  104. package/src/assets/agents/git.mds +827 -0
  105. package/src/assets/agents/knowledge.md +1 -1
  106. package/src/assets/agents/learning.md +11 -0
  107. package/src/assets/agents/synthesize.md +1 -1
  108. package/src/assets/agents/test.md +16 -5
  109. package/src/assets/agents/tracker.md +467 -0
  110. package/src/assets/agents/validate.md +7 -5
  111. package/src/assets/commands/_partials/_engine.mds +11 -9
  112. package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
  113. package/src/assets/commands/_partials/_knowledge.mds +2 -2
  114. package/src/assets/commands/_partials/_plan_contract.mds +22 -7
  115. package/src/assets/commands/_partials/_preamble.mds +1 -1
  116. package/src/assets/commands/_partials/_publication.mds +3 -1
  117. package/src/assets/commands/_partials/_ticket_template.mds +3 -2
  118. package/src/assets/commands/_partials/_tracker.mds +18 -0
  119. package/src/assets/commands/_partials/_wave.mds +16 -10
  120. package/src/assets/commands/bug-analysis.mds +15 -5
  121. package/src/assets/commands/code-review.mds +34 -14
  122. package/src/assets/commands/debug.mds +11 -4
  123. package/src/assets/commands/dynamic-build.mds +227 -41
  124. package/src/assets/commands/dynamic-plan.mds +35 -13
  125. package/src/assets/commands/dynamic-tickets.mds +47 -5
  126. package/src/assets/commands/implement.mds +206 -52
  127. package/src/assets/commands/plan.mds +70 -17
  128. package/src/assets/commands/release.md +64 -17
  129. package/src/assets/commands/resolve.mds +126 -56
  130. package/src/assets/mds/git/_pr.mds +331 -0
  131. package/src/assets/mds/git/_references.mds +135 -0
  132. package/src/assets/mds/tracker/_common.mds +156 -0
  133. package/src/assets/mds/tracker/_github.mds +472 -0
  134. package/src/assets/mds/tracker/_jira.mds +407 -0
  135. package/src/assets/mds/tracker/_linear.mds +449 -0
  136. package/src/assets/mds/tracker/_mcp.mds +299 -0
  137. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
  138. package/src/assets/scripts/hooks/background-memory-update +14 -9
  139. package/src/assets/scripts/hooks/capture-prompt +6 -2
  140. package/src/assets/scripts/hooks/capture-question +6 -2
  141. package/src/assets/scripts/hooks/capture-turn +6 -2
  142. package/src/assets/scripts/hooks/ensure-devflow-init +1 -1
  143. package/src/assets/scripts/hooks/ensure-root-gitignore +161 -60
  144. package/src/assets/scripts/hooks/hook-log-init +3 -1
  145. package/src/assets/scripts/hooks/json-helper.cjs +223 -5
  146. package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -1
  147. package/src/assets/scripts/hooks/memory-worker +15 -8
  148. package/src/assets/scripts/hooks/pre-compact-memory +12 -8
  149. package/src/assets/scripts/hooks/preamble +1 -4
  150. package/src/assets/scripts/hooks/queue-append +68 -24
  151. package/src/assets/scripts/hooks/session-start-context +355 -8
  152. package/src/assets/scripts/hooks/session-start-memory +12 -8
  153. package/src/assets/scripts/pr-evidence.cjs +1961 -0
  154. package/src/assets/scripts/redact-secrets.cjs +490 -62
  155. package/src/assets/scripts/release-trace.cjs +1143 -0
  156. package/src/assets/scripts/resolve-evidence-policy.cjs +1065 -0
  157. package/src/assets/scripts/verify-evidence.cjs +1822 -0
  158. package/src/assets/skills/compliance/SKILL.md +2 -0
  159. package/src/assets/skills/docs-framework/SKILL.md +5 -3
  160. package/src/assets/skills/git/SKILL.md +8 -78
  161. package/src/assets/skills/git/references/github-api.md +179 -141
  162. package/src/assets/skills/git/references/patterns.md +11 -6
  163. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  164. package/src/assets/skills/review-methodology/references/patterns.md +6 -61
  165. package/src/assets/skills/review-methodology/references/violations.md +14 -22
  166. package/src/assets/agents/git.md +0 -938
@@ -7,13 +7,15 @@ import color from 'picocolors';
7
7
  import { getInstallationPaths } from '../../targets/claude-code/claude-paths.js';
8
8
  import { getGitRoot } from '../../core/git.js';
9
9
  import { installViaFileCopy, composeScripts } from '../../targets/claude-code/installer.js';
10
+ import { formatOverlaySummary, formatSkillScopeSummary, formatTrackerAssetSummary, isPluginListUnchanged } from './install-report.js';
11
+ import { convergeTrackerArtifacts } from '../../targets/claude-code/tracker-install.js';
10
12
  import { installSettings, installManagedSettings, installClaudeignore, discoverProjectGitRoots, updateGitignore, ensureDevflowGitignore, createDocsStructure, applyUserSecurityDenyList, detectDenyState, resolveSecurityAction, assertHistoricalDenySuperset, loadTemplateDenyEntries, stripUserSecurityDenyList, } from '../../targets/claude-code/post-install.js';
11
- import { DEVFLOW_PLUGINS, LEGACY_COMMAND_NAMES, LEGACY_RULE_NAMES, buildAssetMaps, buildFullSkillsMap, buildRulesMap, partitionSelectablePlugins, WORKFLOW_ORDER, parsePluginSelection, resolveFeatureRedirect } from '../../core/plugins.js';
13
+ import { DEVFLOW_PLUGINS, LEGACY_COMMAND_NAMES, LEGACY_RULE_NAMES, buildAssetMaps, buildScopedSkillsMap, buildRulesMap, partitionSelectablePlugins, WORKFLOW_ORDER, parsePluginSelection, resolveFeatureRedirect } from '../../core/plugins.js';
12
14
  import { LEGACY_SKILL_NAMES } from '../../targets/claude-code/legacy.js';
13
15
  import { detectPlatform, detectShell, getProfilePath, getSafeDeleteInfo, hasSafeDelete } from '../../core/safe-delete.js';
14
16
  import { generateSafeDeleteBlock, installToProfile, removeFromProfile, getInstalledVersion, SAFE_DELETE_BLOCK_VERSION } from '../../core/safe-delete-install.js';
15
17
  import { addAmbientHook, removeAmbientHook } from './ambient.js';
16
- import { addMemoryHooks, removeMemoryHooks } from './memory.js';
18
+ import { convergeMemoryHooks, drainMemoryQueue } from './memory.js';
17
19
  import { addCaptureHooks, removeCaptureHooks } from './capture.js';
18
20
  import { removeDreamHook } from './legacy-hooks.js';
19
21
  import { addProxyHooks, removeProxyHooks, applyProxyEnv, stripProxyEnv, runProxyPreflight, buildRealPreflightDeps } from './proxy.js';
@@ -26,13 +28,16 @@ import { loadConfig as loadHudConfig, saveConfig as saveHudConfig } from '../../
26
28
  import { readManifest, writeManifest, resolvePluginList, detectUpgrade } from '../../core/manifest.js';
27
29
  import { convergeFlagsIntoSettings, countActiveFlags, readViewMode } from '../../core/flags.js';
28
30
  import { addContextHook, removeContextHook, hasContextHook } from './context.js';
29
- import { writeConfig, readConfigIfPresent } from '../../core/feature-config.js';
31
+ import { writeManagedConfig, readConfigIfPresent, DEFAULT_CONFIG } from '../../core/feature-config.js';
32
+ import { drainLearningQueue } from '../../core/learning-queue-cleanup.js';
33
+ import { removeManagedDenyList, describeManagedDenyRemoval } from './security.js';
30
34
  import { resolveInitSeed, applyCliToggles, resolveResetGatedInputs } from './init-seed.js';
31
35
  import { parseFrameworkList, normalizeFrameworks } from '../../core/compliance.js';
32
36
  import { formatComplianceSummary, shouldRunComplianceStep, runComplianceStep, buildClackCompliancePrompts, } from './compliance-prompts.js';
37
+ import { applyTrackerSentinel, parseTrackerId, rearmTrackerInference, renameStaleTrackerConventions, DEFAULT_TRACKER_PROVIDER, } from '../../core/tracker.js';
38
+ import { formatTrackerSummary, shouldRunTrackerStep, runTrackerStep, buildClackTrackerPrompts, } from './tracker-prompts.js';
33
39
  import { shouldRunAttributionStep, runAttributionStep, buildClackAttributionPrompts, applyAttributionAnswer, attributionSeedFrom, } from './attribution-prompts.js';
34
40
  import { convergeFromManifest } from '../../targets/claude-code/compliance-install.js';
35
- import { getPendingTurnsPath, getPendingTurnsProcessingPath } from '../../core/project-paths.js';
36
41
  import * as os from 'os';
37
42
  // Re-export pure functions for tests (canonical source is post-install.ts)
38
43
  export { substituteSettingsTemplate, computeGitignoreAppend, mergeDenyList, discoverProjectGitRoots } from '../../targets/claude-code/post-install.js';
@@ -99,6 +104,31 @@ export function formatSweepSummary(report) {
99
104
  }
100
105
  return lines;
101
106
  }
107
+ /**
108
+ * Log each summary line at the severity it carries — the one dispatch every
109
+ * `SummaryLine[]` renderer shares.
110
+ *
111
+ * Exhaustive over `SummaryLine['level']` rather than an `if/else`: a level added
112
+ * to the interface has to be routed here, at compile time, instead of silently
113
+ * degrading to `info` at every call site.
114
+ */
115
+ function logSummaryLines(lines) {
116
+ for (const line of lines) {
117
+ switch (line.level) {
118
+ case 'info':
119
+ p.log.info(line.message);
120
+ break;
121
+ case 'warn':
122
+ p.log.warn(line.message);
123
+ break;
124
+ default: {
125
+ const _exhaustive = line.level;
126
+ void _exhaustive;
127
+ break;
128
+ }
129
+ }
130
+ }
131
+ }
102
132
  /**
103
133
  * Classify the safe-delete installation state based on the installed version
104
134
  * in the user's shell profile.
@@ -162,6 +192,250 @@ export function resolveComplianceInitState(complianceOption, seedFrameworks) {
162
192
  // Re-export formatComplianceSummary from compliance-prompts.ts so existing test imports
163
193
  // (tests/init-logic.test.ts:1615 — imports from '../src/cli/commands/init.js') keep resolving.
164
194
  export { formatComplianceSummary } from './compliance-prompts.js';
195
+ /**
196
+ * Parse the --tracker CLI option into a tracker override.
197
+ *
198
+ * Pure function — no I/O, no side effects; extracted for testability.
199
+ *
200
+ * Returns:
201
+ * {ok: true, value} — override state derived from the option
202
+ * {ok: false, error} — not a registry provider ID (caller handles exit)
203
+ * undefined — option was not supplied; no override
204
+ *
205
+ * There is no `--no-tracker` (decision D-E): `--tracker github` IS the off
206
+ * switch, because `provider:'github'` is the off position. Parsing is strict —
207
+ * reject, never repair — so `--tracker jira-cloud` exits rather than silently
208
+ * selecting jira.
209
+ */
210
+ export function resolveTrackerInitState(trackerOption) {
211
+ if (typeof trackerOption !== 'string')
212
+ return undefined;
213
+ const parsed = parseTrackerId(trackerOption);
214
+ if (!parsed.ok)
215
+ return { ok: false, error: parsed.error };
216
+ return { ok: true, value: { provider: parsed.value } };
217
+ }
218
+ /**
219
+ * The outcome line for a provider that arrived as `--tracker <id>`.
220
+ *
221
+ * D-TRACKER-CLI-SURFACE [PF-029]: the Advanced path prints no end-of-wizard
222
+ * summary, and `--tracker` suppresses the wizard step that would otherwise
223
+ * print one, so the CLI-override arm is the only place the selection can
224
+ * surface there. Without this line `devflow init --advanced --tracker jira`
225
+ * changes the machine-wide provider with nothing on screen — the same
226
+ * unreachable-step failure the wizard gate exists to prevent, arrived at from
227
+ * the flag side. Recommended already has its surface in the summary note's
228
+ * Tracker row; both paths or the step is invisible on one.
229
+ *
230
+ * Pure — the caller renders. The summary half is `formatTrackerSummary`, the one
231
+ * spelling every tracker surface shares.
232
+ */
233
+ export function trackerOverrideMessage(provider) {
234
+ return {
235
+ level: provider === DEFAULT_TRACKER_PROVIDER ? 'info' : 'success',
236
+ text: `Tracker: ${formatTrackerSummary(provider)}`,
237
+ };
238
+ }
239
+ /** The real adapter — the ONE binding of each owner into the init lifecycle. */
240
+ export function buildTrackerLifecycleIO() {
241
+ return {
242
+ writeManifest,
243
+ renameStaleConventions: renameStaleTrackerConventions,
244
+ convergeArtifacts: (claudeDir, provider, warn) => convergeTrackerArtifacts({ claudeDir, provider, warn }),
245
+ rearmInference: rearmTrackerInference,
246
+ applySentinel: applyTrackerSentinel,
247
+ };
248
+ }
249
+ /**
250
+ * Persist the installation manifest, then converge the tracker artifacts against
251
+ * the provider that was actually persisted.
252
+ *
253
+ * D-TRACKER-CONVERGE: the manifest write and the three tracker file-lifecycle
254
+ * owners are ONE unit because their relative order is the invariant, not an
255
+ * implementation detail (PF-015). The manifest write is explicitly failable —
256
+ * init must not abort on it — so converging the sentinel, the attempt counter or
257
+ * the conventions file ahead of it leaves the artifacts disagreeing in both
258
+ * directions: github→jira writes a sentinel for a provider the manifest never
259
+ * records (a per-session fork cost forever), and jira→github removes the
260
+ * sentinel, renames tracker.md to .bak and leaves the manifest on jira (silent
261
+ * permanent degradation with no re-trigger). Writing first and gating the three
262
+ * owners on `manifestWritten` makes the artifacts converge all-or-none, and puts
263
+ * this call site in the same order as the sibling `devflow tracker --set`
264
+ * (src/cli/commands/tracker.ts): rename → persist → rearm → sentinel.
265
+ *
266
+ * The provider is read from `manifestData.features.tracker.provider` rather than
267
+ * taken as a separate argument, so there is exactly one binding and the artifacts
268
+ * cannot converge on a value other than the one on disk.
269
+ *
270
+ * `previousProvider` is the caller's REAL prior manifest value, never the
271
+ * --reset-gated seed: under --reset the resolved provider collapses to github
272
+ * while the prior provider is still jira/linear, and that IS a transition the
273
+ * stale-conventions rename has to fire on.
274
+ *
275
+ * Every step reports rather than aborts (PF-009's isolation posture): a
276
+ * feature-state change must never fail `devflow init`.
277
+ */
278
+ export async function persistManifestThenConvergeTracker(opts) {
279
+ const { devflowDir, claudeDir, manifestData, previousProvider, io } = opts;
280
+ const provider = manifestData.features.tracker.provider;
281
+ const messages = [];
282
+ // The gate. Non-fatal for the install (which has already succeeded) but
283
+ // decisive for the tracker artifacts: an unpersisted selection converges none
284
+ // of them, so the on-disk state stays internally consistent and the next
285
+ // `devflow init` retries the whole transition from an unchanged starting point.
286
+ try {
287
+ await io.writeManifest(devflowDir, manifestData);
288
+ }
289
+ catch (error) {
290
+ messages.push({
291
+ level: 'warn',
292
+ text: `Failed to write installation manifest (install succeeded): ${error instanceof Error ? error.message : error}`,
293
+ });
294
+ messages.push({
295
+ level: 'warn',
296
+ text: `Tracker selection (${provider}) was not persisted — the sentinel, attempt counter and ` +
297
+ `conventions file are unchanged. Re-run devflow init, or devflow tracker --set ${provider}.`,
298
+ });
299
+ return { manifestWritten: false, converged: false, agent: 'unchanged', messages };
300
+ }
301
+ // Move a now-stale conventions file aside (the writer arm of the provider change).
302
+ //
303
+ // D-TRACKER-PARALLEL: the rename stays strictly ahead of the other two. It is
304
+ // the only step that reads the PREVIOUS provider and the only one that reports
305
+ // a transition, so keeping it first fixes the message order (the transition
306
+ // notice always precedes any owner warning) and keeps the sequence readable as
307
+ // "settle the old provider, then converge the new one". The two that follow
308
+ // touch disjoint files — the attempt counter and the presence sentinel —
309
+ // depend on nothing the other writes, and both report through TrackerResult
310
+ // instead of throwing (PF-014), so they run concurrently and their warnings
311
+ // are pushed in a fixed order regardless of which settles first.
312
+ const transition = await io.renameStaleConventions(devflowDir, previousProvider, provider);
313
+ if (transition.kind === 'renamed') {
314
+ messages.push({
315
+ level: 'info',
316
+ text: `Tracker provider changed — previous ${transition.previous} conventions moved to ` +
317
+ `${color.dim(transition.to)}`,
318
+ });
319
+ }
320
+ else if (transition.kind === 'failed') {
321
+ messages.push({ level: 'warn', text: transition.error });
322
+ }
323
+ // The fourth owner — the Tracker agent file. SEQUENTIAL, and strictly before
324
+ // the pair below, because the sentinel's WRITE is gated on its outcome: a
325
+ // sentinel that advertises jira while the agent it would spawn is missing is
326
+ // the drifted state this whole ordering exists to prevent (design review H6 —
327
+ // the parallel pair stays a parallel pair, it is not flattened to make room).
328
+ const agentWarnings = [];
329
+ const artifacts = await io.convergeArtifacts(claudeDir, provider, (msg) => agentWarnings.push(msg));
330
+ for (const text of agentWarnings)
331
+ messages.push({ level: 'warn', text });
332
+ // C2: the sentinel converges in BOTH directions, and only the WRITE is gated.
333
+ // provider ≠ github → a write, when a spawnable agent is actually there.
334
+ // provider = github → a removal. ALWAYS attempted, because leaving a stale
335
+ // sentinel behind costs every future session a fork for a provider the
336
+ // user has left, and a failed agent removal is not a reason to keep it.
337
+ //
338
+ // The write gate reads `agentPresent`, not `converged`. `converged` answers
339
+ // "did THIS run copy it", and reading that alone is wrong in both directions:
340
+ // a re-copy that fails over an already-installed agent would disable a provider
341
+ // that still works, and merely SUPPRESSING the write leaves the PREVIOUS
342
+ // provider's sentinel in place — so a jira → linear init whose agent copy
343
+ // failed goes on advertising jira, which is the state the suppression exists to
344
+ // prevent. Not-spawnable therefore REMOVES, through the one sentinel owner in
345
+ // src/core/tracker.ts (D-TRACKER-OWNER), never an inline fs.rm here.
346
+ const advertisable = provider === DEFAULT_TRACKER_PROVIDER || artifacts.agentPresent;
347
+ const [rearm, sentinel] = await Promise.all([
348
+ // [DR-22] The documented re-arm path: devflow init resets the attempt counter
349
+ // so a previously-capped inference gets another five tries.
350
+ io.rearmInference(devflowDir),
351
+ // [DR-10] Converge the presence sentinel: written for jira/linear, removed for
352
+ // github. This is what keeps the GitHub SessionStart path at one stat and zero forks.
353
+ io.applySentinel(devflowDir, advertisable ? provider : DEFAULT_TRACKER_PROVIDER),
354
+ ]);
355
+ if (!rearm.ok)
356
+ messages.push({ level: 'warn', text: rearm.error });
357
+ if (!sentinel.ok)
358
+ messages.push({ level: 'warn', text: sentinel.error });
359
+ if (!advertisable) {
360
+ messages.push({
361
+ level: 'warn',
362
+ text: `Tracker sentinel removed — no ${provider} agent is installed, so nothing advertises a ` +
363
+ `provider whose agent is missing and no session will try to spawn it. ` +
364
+ `Re-run devflow init, or devflow tracker --set ${provider}.`,
365
+ });
366
+ }
367
+ return {
368
+ manifestWritten: true,
369
+ converged: artifacts.converged && sentinel.ok,
370
+ agent: artifacts.agent,
371
+ messages,
372
+ };
373
+ }
374
+ /**
375
+ * Drain this repo's memory and learning queues for each feature init switched
376
+ * off, so stale turns are not processed on a future re-enable — the same drains
377
+ * `devflow memory --disable` and `devflow learning --disable` perform. Other
378
+ * repos' queues are inert: every gate reads the machine-wide switch, so nothing
379
+ * appends to or processes them while the feature is off.
380
+ *
381
+ * D-INIT-DRAIN-AFTER-SWITCH: the drain runs only AFTER the manifest holding the
382
+ * switch is persisted (`manifestWritten`), the order the standalone toggles use
383
+ * (write the switch, then drain). The capture hooks read the manifest on every
384
+ * turn, so draining first left a window — the whole install — in which a
385
+ * concurrent session, still reading the old "on", appended turns that then
386
+ * survived the disable. When the manifest write failed the feature is still on
387
+ * everywhere, so its queue is live and is left alone.
388
+ */
389
+ export async function drainDisabledFeatureQueues(opts, io = { drainMemoryQueue, drainLearningQueue }) {
390
+ if (!opts.manifestWritten || opts.gitRoot === null)
391
+ return;
392
+ if (!opts.memoryEnabled)
393
+ await io.drainMemoryQueue(opts.gitRoot);
394
+ if (!opts.learningEnabled)
395
+ await io.drainLearningQueue(opts.gitRoot);
396
+ }
397
+ /**
398
+ * The manifest `devflow init --hud-only` writes. Pure — never mutates `existing`.
399
+ *
400
+ * D-HUD-ONLY-PRESERVE: over a prior install, --hud-only installs the HUD and
401
+ * nothing else, so the manifest keeps every recorded value — plugins, version,
402
+ * scope, installedAt and every feature — and only `features.hud` turns on. The
403
+ * earlier shape rewrote the whole record as a HUD-only fresh install (ambient,
404
+ * memory, learning, knowledge, rules and proxy all `false`, plugins `[]`) while
405
+ * leaving those features' artifacts on disk: the record stopped describing the
406
+ * machine, the next re-init seeded every feature off (ADR-014), and with
407
+ * memory/learning/knowledge switched by the manifest alone
408
+ * (D-FEATURES-MACHINE-WIDE) a HUD install would have really disabled them
409
+ * everywhere. `version` is kept too: --hud-only reinstalls no plugin, and a
410
+ * bumped version would make the next init skip the upgrade it still owes.
411
+ *
412
+ * With no prior manifest the result is the fresh HUD-only record: every other
413
+ * feature off, which is what is installed.
414
+ */
415
+ export function buildHudOnlyManifest(existing, version, scope, now) {
416
+ if (existing !== null) {
417
+ return {
418
+ ...existing,
419
+ features: { ...existing.features, hud: true },
420
+ updatedAt: now,
421
+ };
422
+ }
423
+ return {
424
+ version,
425
+ plugins: [],
426
+ scope,
427
+ features: {
428
+ ambient: false, memory: false, hud: true, knowledge: false,
429
+ learning: false, rules: false, flags: {}, proxy: false,
430
+ compliance: { enabled: false, frameworks: [] },
431
+ // The exported default, not a literal — the one constant every other
432
+ // module reads, so a moved default moves here too.
433
+ tracker: { provider: DEFAULT_TRACKER_PROVIDER },
434
+ },
435
+ installedAt: now,
436
+ updatedAt: now,
437
+ };
438
+ }
165
439
  export const initCommand = new Command('init')
166
440
  .description('Initialize Devflow for Claude Code')
167
441
  .option('--scope <type>', 'Installation scope: user or local (project-only)', /^(user|local)$/i)
@@ -183,6 +457,7 @@ export const initCommand = new Command('init')
183
457
  .option('--no-proxy', 'Disable external model routing')
184
458
  .option('--compliance <list>', 'Enable compliance with comma-separated framework IDs (e.g., gdpr,hipaa)')
185
459
  .option('--no-compliance', 'Disable compliance (artifacts removed; frameworks remembered for re-enable)')
460
+ .option('--tracker <id>', 'Issue tracker provider: github, jira, or linear')
186
461
  .option('--security <mode>', 'Security deny list location: user, managed, or none', /^(user|managed|none)$/i)
187
462
  .option('--hud-only', 'Install only the HUD (no plugins, hooks, or extras)')
188
463
  .option('--recommended', 'Apply recommended defaults after plugin selection (skip advanced prompts)')
@@ -261,28 +536,15 @@ export const initCommand = new Command('init')
261
536
  p.log.error(`Failed to install HUD scripts: ${error instanceof Error ? error.message : error}`);
262
537
  process.exit(1);
263
538
  }
264
- // Read existing manifest to preserve user-set compliance state (disable-keeps-frameworks
265
- // contract: the hud-only path must not erase frameworks the user previously selected).
539
+ // Read the existing manifest: over a prior install, --hud-only touches only
540
+ // the HUD (D-HUD-ONLY-PRESERVE in buildHudOnlyManifest).
266
541
  let existingHudManifest = null;
267
542
  try {
268
543
  existingHudManifest = await readManifest(devflowDir);
269
544
  }
270
545
  catch { /* absent on fresh install — existingHudManifest stays null */ }
271
- // Write minimal manifest
272
- const now = new Date().toISOString();
273
546
  try {
274
- await writeManifest(devflowDir, {
275
- version,
276
- plugins: [],
277
- scope,
278
- features: {
279
- ambient: false, memory: false, hud: true, knowledge: false,
280
- learning: false, rules: false, flags: {}, proxy: false,
281
- compliance: existingHudManifest?.features.compliance ?? { enabled: false, frameworks: [] },
282
- },
283
- installedAt: now,
284
- updatedAt: now,
285
- });
547
+ await writeManifest(devflowDir, buildHudOnlyManifest(existingHudManifest, version, scope, new Date().toISOString()));
286
548
  }
287
549
  catch { /* non-fatal */ }
288
550
  p.log.success('HUD installed');
@@ -319,7 +581,7 @@ export const initCommand = new Command('init')
319
581
  // registry defaults — including viewMode 'default' (an externally-set /focus in settings.json
320
582
  // must not survive a factory reset).
321
583
  const { seedManifest, seedConfig, seedSettings } = resolveResetGatedInputs(!!options.reset, existingManifest, earlyProjectConfig, earlySettingsJson ?? '');
322
- const seed = resolveInitSeed(seedManifest, seedConfig, seedSettings, DEVFLOW_PLUGINS);
584
+ const seed = resolveInitSeed(seedManifest, seedSettings, DEVFLOW_PLUGINS);
323
585
  // Early validation: parse --compliance <list> at the boundary before any prompts (PF-parse-at-boundary).
324
586
  // options.compliance: string → --compliance <list>; false → --no-compliance; undefined → not passed
325
587
  let cliComplianceOverride;
@@ -333,6 +595,20 @@ export const initCommand = new Command('init')
333
595
  cliComplianceOverride = complianceStateResult.value;
334
596
  }
335
597
  }
598
+ // Early validation: parse --tracker <id> at the boundary before any prompts.
599
+ // Strict — reject, never repair — so a typo'd provider exits here rather than
600
+ // installing mechanics for a tracker the user did not name.
601
+ let cliTrackerOverride;
602
+ {
603
+ const trackerStateResult = resolveTrackerInitState(options.tracker);
604
+ if (trackerStateResult !== undefined) {
605
+ if (!trackerStateResult.ok) {
606
+ p.log.error(trackerStateResult.error);
607
+ process.exit(1);
608
+ }
609
+ cliTrackerOverride = trackerStateResult.value;
610
+ }
611
+ }
336
612
  // Select plugins to install
337
613
  let selectedPlugins = [];
338
614
  if (options.plugin) {
@@ -506,6 +782,9 @@ export const initCommand = new Command('init')
506
782
  // CLI override applied below in both Recommended and Advanced paths.
507
783
  let complianceEnabled = seed.features.compliance.enabled;
508
784
  let complianceFrameworks = seed.features.compliance.frameworks;
785
+ // tracker: manifest-group (like proxy and compliance); seed from prior manifest.
786
+ // CLI override applied below in both Recommended and Advanced paths.
787
+ let trackerProvider = seed.features.tracker.provider;
509
788
  let enabledFlags = { ...seed.flags };
510
789
  // viewModeExplicit: true when --reset is passed; signals resolveFinalViewMode to let the
511
790
  // seed-time view-mode win over an externally-set value in settings.json.
@@ -520,6 +799,47 @@ export const initCommand = new Command('init')
520
799
  // The final value is written to the manifest and consumed by the dedicated security step.
521
800
  let securityMode = 'user'; // placeholder; overwritten below by resolve
522
801
  let managedSettingsConfirmed = false;
802
+ /**
803
+ * Run the tracker wizard step for one wizard path.
804
+ *
805
+ * D-TRACKER-CALLSHAPE: both paths share one gate (shouldRunTrackerStep), one
806
+ * cancel idiom and one prompt adapter; they differ only in the mode they
807
+ * declare, the provider they seed from, and whether the step's own outcome
808
+ * line is emitted. Holding all three differences as parameters keeps the
809
+ * shared half single-sourced, so the paths cannot drift the way two
810
+ * hand-copied call sites do.
811
+ *
812
+ * Returns the chosen state, or undefined when the gate declined to run — the
813
+ * caller then owns the CLI-override fallback, which is the only other way the
814
+ * provider can change on that path.
815
+ */
816
+ const runTrackerStepAt = async (mode, seedProvider, emitMessages) => {
817
+ if (!shouldRunTrackerStep({
818
+ mode,
819
+ modePromptShown,
820
+ isTTY: process.stdin.isTTY,
821
+ hasCliOverride: cliTrackerOverride !== undefined,
822
+ })) {
823
+ return undefined;
824
+ }
825
+ const trackerStep = await runTrackerStep({
826
+ seed: { provider: seedProvider },
827
+ prompts: buildClackTrackerPrompts(),
828
+ });
829
+ if (trackerStep.kind === 'cancelled') {
830
+ p.cancel('Installation cancelled.');
831
+ process.exit(0);
832
+ }
833
+ if (emitMessages) {
834
+ for (const msg of trackerStep.messages) {
835
+ if (msg.level === 'success')
836
+ p.log.success(msg.text);
837
+ else
838
+ p.log.info(msg.text);
839
+ }
840
+ }
841
+ return trackerStep.state;
842
+ };
523
843
  // Safe-delete detection (both paths need this)
524
844
  const platform = detectPlatform();
525
845
  const shell = detectShell();
@@ -552,6 +872,13 @@ export const initCommand = new Command('init')
552
872
  // Step messages not emitted here — the Recommended summary note (below) already
553
873
  // prints the Compliance line from complianceSummary via formatComplianceSummary.
554
874
  }
875
+ // Tracker wizard step — same gate as compliance, so both wizard paths are
876
+ // governed by the one documented gate table (AC-3.6). Runs only when the
877
+ // Setup-mode prompt actually ran, so --recommended and !isTTY stay promptless.
878
+ // emitMessages=false: the Recommended summary note (below) prints the
879
+ // Tracker line via formatTrackerSummary, so the step's own outcome line
880
+ // would be a duplicate.
881
+ const wizardTracker = await runTrackerStepAt('recommended', seed.features.tracker.provider, false);
555
882
  // No attribution step here: the suppress-attribution question is Advanced-only (D27).
556
883
  // Recommended silently carries the seeded value in enabledFlags — fresh installs get
557
884
  // the registry default (off), re-inits get prior state. See shouldRunAttributionStep.
@@ -567,6 +894,8 @@ export const initCommand = new Command('init')
567
894
  rules: options.rules,
568
895
  proxy: options.proxy,
569
896
  compliance: cliComplianceOverride ?? wizardCompliance,
897
+ // Precedence: cliOverride ?? wizardResult ?? seed (applyCliToggles supplies the seed arm).
898
+ tracker: cliTrackerOverride ?? wizardTracker,
570
899
  });
571
900
  ambientEnabled = effectiveFeatures.ambient;
572
901
  memoryEnabled = effectiveFeatures.memory;
@@ -577,6 +906,7 @@ export const initCommand = new Command('init')
577
906
  proxyEnabled = effectiveFeatures.proxy;
578
907
  complianceEnabled = effectiveFeatures.compliance.enabled;
579
908
  complianceFrameworks = effectiveFeatures.compliance.frameworks;
909
+ trackerProvider = effectiveFeatures.tracker.provider;
580
910
  // enabledFlags is already initialised to seed.flags above.
581
911
  // Compute safe-delete block synchronously so we know whether to fetch installed version
582
912
  if (profilePath && safeDeleteAvailable) {
@@ -612,6 +942,10 @@ export const initCommand = new Command('init')
612
942
  `Knowledge bases: ${knowledgeEnabled ? 'enabled' : 'disabled'}`,
613
943
  `Ext model routing: ${proxyEnabled ? 'enabled' : 'disabled'}`,
614
944
  `Compliance: ${complianceSummary}`,
945
+ // Recommended emits no per-step outcome lines, so this summary row is the
946
+ // tracker step's ONLY surface on this path — both surfaces or it is
947
+ // invisible on one path.
948
+ `Tracker: ${formatTrackerSummary(trackerProvider)}`,
615
949
  `View mode: ${readViewMode(enabledFlags)}`,
616
950
  `Claude Code flags: ${defaultFlagCount} configured`,
617
951
  `${claudeignoreEnabled ? '.claudeignore: created' : ''}`,
@@ -657,7 +991,8 @@ export const initCommand = new Command('init')
657
991
  'compaction. Clear your session at any point and resume right\n' +
658
992
  'where you left off.\n\n' +
659
993
  'Runs a background agent on session stop that consumes additional\n' +
660
- 'tokens. Consider skipping if token usage is a concern.', 'Working Memory');
994
+ 'tokens. Consider skipping if token usage is a concern.\n' +
995
+ 'Applies to every project.', 'Working Memory');
661
996
  const memoryChoice = await p.confirm({
662
997
  message: 'Enable working memory? (Recommended)',
663
998
  initialValue: seed.features.memory,
@@ -690,7 +1025,8 @@ export const initCommand = new Command('init')
690
1025
  else {
691
1026
  p.note('Per-feature knowledge bases capture cross-cutting patterns,\n' +
692
1027
  'conventions, and gotchas. Created and updated automatically\n' +
693
- 'when workflows touch a documented area (write-through model).', 'Feature Knowledge Bases');
1028
+ 'when workflows touch a documented area (write-through model).\n' +
1029
+ 'Applies to every project.', 'Feature Knowledge Bases');
694
1030
  const knowledgeChoice = await p.confirm({
695
1031
  message: 'Enable feature knowledge bases? (Recommended)',
696
1032
  initialValue: seed.features.knowledge,
@@ -707,7 +1043,7 @@ export const initCommand = new Command('init')
707
1043
  else {
708
1044
  p.note('Detects architectural decisions and pitfalls from your session\n' +
709
1045
  'dialogs. Runs a background agent on session stop that consumes\n' +
710
- 'additional tokens.', 'Learning (Decision/Pitfall Tracking)');
1046
+ 'additional tokens. Applies to every project.', 'Learning (Decision/Pitfall Tracking)');
711
1047
  const learningChoice = await p.confirm({
712
1048
  message: 'Enable learning? (Recommended)',
713
1049
  initialValue: seed.features.learning,
@@ -797,6 +1133,28 @@ export const initCommand = new Command('init')
797
1133
  // No third case in practice: on this path the predicate only returns false for a
798
1134
  // CLI override (isTTY is guaranteed true by the non-TTY guard above). If it ever
799
1135
  // did, the seed values assigned at declaration stand — which is the right default.
1136
+ // Tracker feature (after compliance, before attribution). Gated by the same
1137
+ // shouldRunTrackerStep predicate as the Recommended path, so the documented
1138
+ // gate table is the single authority for both and they cannot drift.
1139
+ // This call site is the one that matters on RE-INIT: re-init is Advanced-only
1140
+ // by construction, so a Recommended-only wiring would be dead there.
1141
+ // emitMessages=true: Advanced has no end-of-wizard summary recap, so the
1142
+ // step's outcome line is this path's ONLY surface — mandatory, not decorative.
1143
+ const advancedTracker = await runTrackerStepAt('advanced', trackerProvider, true);
1144
+ if (advancedTracker !== undefined) {
1145
+ trackerProvider = advancedTracker.provider;
1146
+ }
1147
+ else if (cliTrackerOverride !== undefined) {
1148
+ // --tracker passed explicitly — honour without prompting, and say so.
1149
+ // The gate declined the step, and this path has no summary recap, so
1150
+ // this line is the selection's ONLY surface (D-TRACKER-CLI-SURFACE).
1151
+ trackerProvider = cliTrackerOverride.provider;
1152
+ const overrideLine = trackerOverrideMessage(trackerProvider);
1153
+ if (overrideLine.level === 'success')
1154
+ p.log.success(overrideLine.text);
1155
+ else
1156
+ p.log.info(overrideLine.text);
1157
+ }
800
1158
  // Attribution feature (after compliance, before flags). This is the ONLY call site —
801
1159
  // the attribution question is Advanced-only (D27); the Recommended path never asks and
802
1160
  // silently carries the seeded value. The gate stays an explicit predicate call so the
@@ -993,6 +1351,10 @@ export const initCommand = new Command('init')
993
1351
  }
994
1352
  }
995
1353
  // Detect current deny list state in user settings (read-only; write happens in security step)
1354
+ // Whether the managed settings file holds a Devflow deny entry — the security
1355
+ // step's `none` branch removes it only then (and only then stops the spinner
1356
+ // for a possible sudo prompt).
1357
+ let managedDenyDetected = false;
996
1358
  {
997
1359
  const userSettingsJson = earlySettingsJson;
998
1360
  let managedExists = false;
@@ -1005,6 +1367,7 @@ export const initCommand = new Command('init')
1005
1367
  }
1006
1368
  catch { /* absent or unsupported platform */ }
1007
1369
  const detected = detectDenyState(userSettingsJson, managedExists, managedContentJson);
1370
+ managedDenyDetected = detected.managed;
1008
1371
  const flagValue = options.security;
1009
1372
  const manifestMode = existingManifest?.features.security;
1010
1373
  const resolution = resolveSecurityAction(flagValue, manifestMode, detected, process.stdin.isTTY);
@@ -1068,9 +1431,17 @@ export const initCommand = new Command('init')
1068
1431
  if (ambientEnabled && ambientPlugin && !pluginsToInstall.includes(ambientPlugin)) {
1069
1432
  pluginsToInstall.push(ambientPlugin);
1070
1433
  }
1071
- // Skills: install ALL from ALL plugins (skills are tiny markdown files;
1072
- // commands need skills from other plugins to function)
1073
- const skillsMap = buildFullSkillsMap();
1434
+ // The EFFECTIVE selection — what the manifest will record, resolved here
1435
+ // rather than at manifest-write time because the skills install set is
1436
+ // derived from it. On a full install it is `pluginsToInstall`; on a partial
1437
+ // install (`--plugin=X`) it merges the prior manifest's plugins with X, so a
1438
+ // previously-installed plugin's skills survive an add-one run (AC-22).
1439
+ const installedPluginNames = pluginsToInstall.map(pl => pl.name);
1440
+ const effectivePluginNames = resolvePluginList(installedPluginNames, existingManifest, !!options.plugin);
1441
+ const effectivePlugins = DEVFLOW_PLUGINS.filter(pl => effectivePluginNames.includes(pl.name));
1442
+ // Skills: the effective selection's closure — every plugin's own skills plus
1443
+ // the ones it requires. Scoped like rules, agents and commands already are.
1444
+ const skillsMap = buildScopedSkillsMap(effectivePlugins);
1074
1445
  // Agents: install only from selected plugins
1075
1446
  const { agentsMap } = buildAssetMaps(pluginsToInstall);
1076
1447
  // Rules: install only from selected plugins (plugin-scoped, not universal)
@@ -1107,16 +1478,23 @@ export const initCommand = new Command('init')
1107
1478
  catch { /* absent — no legacy artifact */ }
1108
1479
  // Install via file copy
1109
1480
  let installReport;
1481
+ const installWarnings = [];
1110
1482
  try {
1111
1483
  installReport = await installViaFileCopy({
1112
1484
  plugins: pluginsToInstall,
1485
+ effectivePlugins,
1113
1486
  claudeDir,
1114
1487
  devflowDir,
1115
1488
  skillsMap,
1116
1489
  agentsMap,
1117
1490
  rulesMap,
1491
+ trackerProvider,
1118
1492
  isPartialInstall: !!options.plugin,
1119
1493
  spinner: s,
1494
+ // Non-fatal install notices with no other channel (skipped symlinks in the
1495
+ // generated reference tree, mode-normalisation failures) reach the user rather
1496
+ // than the void. Collected now, emitted after the spinner stops.
1497
+ warn: (msg) => { installWarnings.push(msg); },
1120
1498
  });
1121
1499
  }
1122
1500
  catch (error) {
@@ -1400,18 +1778,18 @@ export const initCommand = new Command('init')
1400
1778
  content = ambientEnabled ? await addAmbientHook(cleanedForAmbient, devflowDir) : cleanedForAmbient;
1401
1779
  // Capture hooks — always-on (like the context hook below), remove-then-add for
1402
1780
  // upgrade safety. Queue-append only (capture-prompt/capture-turn/capture-question);
1403
- // each script gates its own per-queue write internally via feature config, so there
1404
- // is no CLI-level enable/disable toggle here. MUST run before addMemoryHooks below
1781
+ // each script gates its own per-queue write on the machine-wide switch, so there
1782
+ // is no CLI-level enable/disable toggle here. MUST run before convergeMemoryHooks below
1405
1783
  // so capture-turn lands before memory-worker in the Stop array (AC-C2 ordering:
1406
1784
  // append-before-spawn).
1407
1785
  const cleanedForCapture = removeCaptureHooks(content);
1408
1786
  content = addCaptureHooks(cleanedForCapture, devflowDir);
1409
- // Memory hooks — always remove-then-add to upgrade hook format (e.g., .sh → run-hook).
1410
- // Three hooks: Stop (memory-worker), SessionStart (session-start-memory), PreCompact.
1411
- // Learning agent (spawned via session-start-context directive) handles decision/pitfall
1412
- // detection. Knowledge is handled in-command via write-through (knowledge_writeback MDS partial).
1413
- const cleaned = removeMemoryHooks(content);
1414
- content = memoryEnabled ? addMemoryHooks(cleaned, devflowDir) : cleaned;
1787
+ // Memory hooks — Stop (memory-worker), SessionStart (session-start-memory),
1788
+ // PreCompact — through the same transform `devflow memory --enable/--disable`
1789
+ // uses (D-FEATURES-MACHINE-WIDE). Learning agent (spawned via
1790
+ // session-start-context directive) handles decision/pitfall detection.
1791
+ // Knowledge is handled in-command via write-through (knowledge_writeback MDS partial).
1792
+ content = convergeMemoryHooks(content, memoryEnabled, devflowDir);
1415
1793
  // HUD statusLine
1416
1794
  content = hudEnabled
1417
1795
  ? addHudStatusLine(content, devflowDir)
@@ -1480,32 +1858,21 @@ export const initCommand = new Command('init')
1480
1858
  p.log.warn(`Could not configure settings.json: ${err instanceof Error ? err.message : err}. ` +
1481
1859
  'Manifest records intended state; run devflow init again to retry.');
1482
1860
  }
1483
- // Write .devflow/config.json to manage per-feature enable/disable at runtime.
1484
- // Uses writeConfig (full atomic write) rather than three updateFeature calls because
1485
- // init always sets all three features at once and is never concurrent with toggle
1486
- // commands — it is a one-time setup action. See D1 in feature-config.ts for the
1487
- // concurrency assumption shared by both write strategies.
1861
+ // Write .devflow/config.json — facts about this repo, never a feature switch
1862
+ // (memory/learning/knowledge are the manifest's alone, D-FEATURES-MACHINE-WIDE;
1863
+ // the managed write drops their retired per-repo keys). A managed
1864
+ // read-modify-write, not a whole-file write: init owns only reviewPublication,
1865
+ // and every other key in the file — the hand-written per-repo `tracker`
1866
+ // override first among them — is carried from disk, under --reset too
1867
+ // (D-CONFIG-PRESERVE-UNMANAGED in feature-config.ts, avoids PF-071).
1488
1868
  if (gitRoot) {
1489
- await writeConfig(gitRoot, {
1490
- memory: memoryEnabled,
1491
- learning: learningEnabled,
1492
- knowledge: knowledgeEnabled,
1869
+ await writeManagedConfig(gitRoot, {
1493
1870
  // reviewPublication has no prompt, so it is carried over from the
1494
1871
  // reset-gated snapshot rather than re-read from disk: seedConfig is null
1495
1872
  // under --reset, which is what collapses the field back to 'auto' with
1496
1873
  // every other feature (PF-015 — read the post-gate binding, not the file).
1497
- reviewPublication: seedConfig?.reviewPublication ?? 'auto',
1874
+ reviewPublication: seedConfig?.reviewPublication ?? DEFAULT_CONFIG.reviewPublication,
1498
1875
  });
1499
- // Drain orphaned queue files when memory is disabled so stale turns
1500
- // don't process on a future re-enable. Mirrors memory.ts --disable drain.
1501
- if (!memoryEnabled) {
1502
- await Promise.all([
1503
- fs.unlink(getPendingTurnsPath(gitRoot)).catch((e) => { if (e.code !== 'ENOENT')
1504
- throw e; }),
1505
- fs.unlink(getPendingTurnsProcessingPath(gitRoot)).catch((e) => { if (e.code !== 'ENOENT')
1506
- throw e; }),
1507
- ]);
1508
- }
1509
1876
  }
1510
1877
  // Configure HUD
1511
1878
  const existingHud = loadHudConfig();
@@ -1626,11 +1993,26 @@ export const initCommand = new Command('init')
1626
1993
  }
1627
1994
  }
1628
1995
  else if (securityMode === 'none') {
1629
- // None: strip Devflow deny entries from user settings.
1630
- // Uses the canonical helper (atomic temp+rename; ENOENT-safe; only-write-if-changed).
1996
+ // None: strip Devflow deny entries from EVERY location, as
1997
+ // `devflow security --disable` does. User settings first, via the
1998
+ // canonical helper (atomic temp+rename; ENOENT-safe; only-write-if-changed).
1631
1999
  const stripResult = await stripUserSecurityDenyList(userSettingsPath);
1632
2000
  if (stripResult && verbose)
1633
2001
  p.log.info(`Security deny list removed (${stripResult.removed.length} entries stripped)`);
2002
+ // Then managed settings, through the one removal security.ts also uses.
2003
+ // Before this, `none` left the managed file in place: the deny list kept
2004
+ // applying — at the highest precedence — while the manifest said `none`.
2005
+ // A permission failure is reported, never thrown (the install already
2006
+ // succeeded; the manifest records the choice and a re-run can retry).
2007
+ if (managedDenyDetected) {
2008
+ s.stop('Removing managed security settings (may prompt for sudo password)...');
2009
+ const managedMsg = describeManagedDenyRemoval(await removeManagedDenyList(rootDir, verbose));
2010
+ if (managedMsg.level === 'warn')
2011
+ p.log.warn(managedMsg.text);
2012
+ else if (verbose)
2013
+ p.log.info(managedMsg.text);
2014
+ s.start('Finalizing installation...');
2015
+ }
1634
2016
  }
1635
2017
  else {
1636
2018
  // Exhaustive guard — if TypeScript reaches here, a new SecurityMode variant was added
@@ -1683,12 +2065,21 @@ export const initCommand = new Command('init')
1683
2065
  // failed removal leaves a retired asset live. Both must surface.
1684
2066
  // After I09, the installer's knownNames set unions FEATURE_OWNED_SKILLS, so
1685
2067
  // devflow:compliance is never swept here — no suppression predicate is needed.
1686
- for (const line of formatSweepSummary(installReport)) {
1687
- if (line.level === 'warn')
1688
- p.log.warn(line.message);
1689
- else
1690
- p.log.info(line.message);
1691
- }
2068
+ logSummaryLines(formatSweepSummary(installReport));
2069
+ // Reference-overlay reporting: the overlay rewrites files inside an installed skill
2070
+ // the user may have shadowed, and reports any unit it had to leave alone (PF-015).
2071
+ logSummaryLines(formatOverlaySummary(installReport, trackerProvider));
2072
+ // Skill-scoping reporting: a deselected skill is deleted and a dormant shadow
2073
+ // is inert, and neither is distinguishable from "never installed" on disk.
2074
+ //
2075
+ // L2: "the plugin list is unchanged" means a prior manifest EXISTS and its
2076
+ // plugin set equals this run's. A first install had nothing to remove, so
2077
+ // there is no upgrade to explain — the removal notice would be addressed to
2078
+ // a user who never had the skills.
2079
+ const pluginListUnchanged = isPluginListUnchanged(existingManifest?.plugins ?? null, effectivePluginNames);
2080
+ logSummaryLines(formatSkillScopeSummary(installReport, pluginListUnchanged));
2081
+ for (const warning of installWarnings)
2082
+ p.log.warn(warning);
1692
2083
  const installedSet = new Set(pluginsToInstall.flatMap(p => p.commands).filter(c => c.length > 0));
1693
2084
  const orderedCommands = WORKFLOW_ORDER.filter(cmd => installedSet.has(cmd));
1694
2085
  if (orderedCommands.length > 0) {
@@ -1741,11 +2132,13 @@ export const initCommand = new Command('init')
1741
2132
  p.log.info(`Deduplication: ${agentsMap.size} unique agents (from ${totalAgentDeclarations} declarations)`);
1742
2133
  }
1743
2134
  // Write installation manifest for upgrade tracking (non-fatal — install already succeeded)
1744
- const installedPluginNames = pluginsToInstall.map(pl => pl.name);
1745
2135
  const now = new Date().toISOString();
1746
2136
  const manifestData = {
1747
2137
  version,
1748
- plugins: resolvePluginList(installedPluginNames, existingManifest, !!options.plugin),
2138
+ // Resolved above, before the install, because the skills install set is
2139
+ // derived from it — one binding, so the manifest can never record a
2140
+ // selection other than the one the assets were installed for.
2141
+ plugins: effectivePluginNames,
1749
2142
  scope,
1750
2143
  // Snapshot of known plugin names at this install — used by resolveSeedPlugins on next init
1751
2144
  // to detect new non-optional plugins and auto-adopt them.
@@ -1767,16 +2160,54 @@ export const initCommand = new Command('init')
1767
2160
  // and Advanced wizard selection. convergeComplianceArtifacts was called above.
1768
2161
  // normalizeFrameworks: dedup + filter unknowns before persisting.
1769
2162
  compliance: { enabled: complianceEnabled, frameworks: normalizeFrameworks(complianceFrameworks) },
2163
+ // Resolved tracker selection. Already a validated TrackerProvider — it came
2164
+ // through parseTrackerId (CLI), the typed wizard select, or the seed, which
2165
+ // itself came through normalizeTrackerFeature on read.
2166
+ tracker: { provider: trackerProvider },
1770
2167
  },
1771
2168
  installedAt: existingManifest?.installedAt ?? now,
1772
2169
  updatedAt: now,
1773
2170
  };
1774
- try {
1775
- await writeManifest(devflowDir, manifestData);
1776
- }
1777
- catch (error) {
1778
- p.log.warn(`Failed to write installation manifest (install succeeded): ${error instanceof Error ? error.message : error}`);
1779
- }
2171
+ // ── Manifest write + tracker selection lifecycle (the ONE call site) ──────
2172
+ // persistManifestThenConvergeTracker owns the ordering invariant: the three
2173
+ // tracker file-lifecycle owners in src/core/tracker.ts converge only against
2174
+ // a provider the manifest actually persisted (D-TRACKER-CONVERGE, PF-015).
2175
+ const trackerLifecycle = await persistManifestThenConvergeTracker({
2176
+ devflowDir,
2177
+ claudeDir,
2178
+ manifestData,
2179
+ // The REAL manifest, not the --reset-gated seed: under --reset the resolved
2180
+ // provider collapses to github while the prior provider is still jira/linear,
2181
+ // and that IS a transition the stale-file rename has to fire on.
2182
+ previousProvider: existingManifest?.features.tracker.provider,
2183
+ io: buildTrackerLifecycleIO(),
2184
+ });
2185
+ for (const msg of trackerLifecycle.messages) {
2186
+ if (msg.level === 'warn')
2187
+ p.log.warn(msg.text);
2188
+ else
2189
+ p.log.info(msg.text);
2190
+ }
2191
+ // Only now that the machine-wide switch is on disk (D-INIT-DRAIN-AFTER-SWITCH).
2192
+ await drainDisabledFeatureQueues({
2193
+ gitRoot,
2194
+ memoryEnabled,
2195
+ learningEnabled,
2196
+ manifestWritten: trackerLifecycle.manifestWritten,
2197
+ });
2198
+ // Name the active provider and what the selection moved. The reference
2199
+ // counts come from the install report rather than being recomputed: the
2200
+ // overlay is what actually installed and pruned them, so a second count
2201
+ // here could only ever disagree with it.
2202
+ const trackerLines = formatTrackerAssetSummary({
2203
+ provider: trackerProvider,
2204
+ previous: existingManifest?.features.tracker.provider,
2205
+ isDefault: trackerProvider === DEFAULT_TRACKER_PROVIDER,
2206
+ installedRefs: installReport.overlaidRefs.length,
2207
+ removedRefs: installReport.sweptOrphans.filter(o => o.kind === 'reference').length,
2208
+ agent: trackerLifecycle.agent,
2209
+ });
2210
+ logSummaryLines(trackerLines);
1780
2211
  // External model routing status line (Advanced path / explicit --proxy flag only)
1781
2212
  if (proxyEnabled) {
1782
2213
  p.log.info(`External model routing: ${color.green('enabled')} — takes effect in new Claude Code sessions`);