devflow-kit 2.3.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 (169) hide show
  1. package/CHANGELOG.md +177 -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 +144 -0
  6. package/dist/cli/commands/compliance-prompts.js +7 -11
  7. package/dist/cli/commands/compliance.js +23 -1
  8. package/dist/cli/commands/init-seed.js +69 -30
  9. package/dist/cli/commands/init.js +551 -77
  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 +38 -0
  16. package/dist/cli/commands/proxy.js +91 -31
  17. package/dist/cli/commands/security.js +76 -16
  18. package/dist/cli/commands/skills.js +53 -7
  19. package/dist/cli/commands/tracker-prompts.js +145 -0
  20. package/dist/cli/commands/tracker.js +405 -0
  21. package/dist/cli/commands/uninstall.js +233 -77
  22. package/dist/cli.js +2 -0
  23. package/dist/commands/bug-analysis.md +22 -4
  24. package/dist/commands/code-review.md +44 -15
  25. package/dist/commands/debug.md +20 -6
  26. package/dist/commands/dynamic-build.md +289 -67
  27. package/dist/commands/dynamic-plan.md +60 -21
  28. package/dist/commands/dynamic-profile.md +1 -1
  29. package/dist/commands/dynamic-tickets.md +58 -8
  30. package/dist/commands/explore.md +2 -2
  31. package/dist/commands/implement.md +241 -53
  32. package/dist/commands/plan.md +88 -17
  33. package/dist/commands/release.md +64 -17
  34. package/dist/commands/resolve.md +138 -58
  35. package/dist/commands/self-review.md +2 -2
  36. package/dist/core/agent-models.js +55 -12
  37. package/dist/core/assets.js +58 -2
  38. package/dist/core/evidence-policy.js +147 -0
  39. package/dist/core/feature-config.js +130 -64
  40. package/dist/core/feature-switch.js +112 -0
  41. package/dist/core/flags.js +149 -8
  42. package/dist/core/manifest.js +33 -7
  43. package/dist/core/mds-variants.js +861 -0
  44. package/dist/core/model-discovery.js +12 -1
  45. package/dist/core/plugins.js +357 -9
  46. package/dist/core/project-paths.js +1 -1
  47. package/dist/core/proxy-log.js +8 -4
  48. package/dist/core/proxy-state.js +100 -43
  49. package/dist/core/reference-sweep.js +136 -0
  50. package/dist/core/tracker.js +407 -0
  51. package/dist/skills/git/references/decision-markers.md +19 -0
  52. package/dist/skills/git/references/learn-conventions.md +56 -0
  53. package/dist/skills/git/references/pr/check-ci-status.md +14 -0
  54. package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
  55. package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
  56. package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
  57. package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
  58. package/dist/skills/git/references/pr/post-review-summary.md +42 -0
  59. package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
  60. package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
  61. package/dist/skills/git/references/pr/validate-branch.md +18 -0
  62. package/dist/skills/git/references/publication-gate.md +13 -0
  63. package/dist/skills/git/references/tracker/_mcp.md +153 -0
  64. package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
  65. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
  66. package/dist/skills/git/references/tracker/github/create-release.md +11 -0
  67. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
  68. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
  69. package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
  70. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
  71. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
  72. package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
  73. package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
  74. package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
  75. package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
  76. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
  77. package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
  78. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
  79. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
  80. package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
  81. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
  82. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
  83. package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
  84. package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
  85. package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
  86. package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
  87. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
  88. package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
  89. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
  90. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
  91. package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
  92. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
  93. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
  94. package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
  95. package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
  96. package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
  97. package/dist/skills/git/references/trust-rule.md +7 -0
  98. package/dist/targets/claude-code/installer.js +1213 -31
  99. package/dist/targets/claude-code/legacy.js +5 -0
  100. package/dist/targets/claude-code/post-install.js +303 -104
  101. package/dist/targets/claude-code/tracker-install.js +161 -0
  102. package/package.json +4 -3
  103. package/src/assets/agents/code.md +42 -4
  104. package/src/assets/agents/design.md +1 -1
  105. package/src/assets/agents/git.mds +827 -0
  106. package/src/assets/agents/knowledge.md +1 -1
  107. package/src/assets/agents/learning.md +11 -0
  108. package/src/assets/agents/synthesize.md +1 -1
  109. package/src/assets/agents/test.md +16 -5
  110. package/src/assets/agents/tracker.md +467 -0
  111. package/src/assets/agents/validate.md +7 -5
  112. package/src/assets/commands/_partials/_engine.mds +11 -9
  113. package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
  114. package/src/assets/commands/_partials/_knowledge.mds +2 -2
  115. package/src/assets/commands/_partials/_plan_contract.mds +22 -7
  116. package/src/assets/commands/_partials/_preamble.mds +1 -1
  117. package/src/assets/commands/_partials/_publication.mds +3 -1
  118. package/src/assets/commands/_partials/_ticket_template.mds +3 -2
  119. package/src/assets/commands/_partials/_tracker.mds +18 -0
  120. package/src/assets/commands/_partials/_wave.mds +16 -10
  121. package/src/assets/commands/bug-analysis.mds +15 -5
  122. package/src/assets/commands/code-review.mds +34 -14
  123. package/src/assets/commands/debug.mds +11 -4
  124. package/src/assets/commands/dynamic-build.mds +227 -41
  125. package/src/assets/commands/dynamic-plan.mds +35 -13
  126. package/src/assets/commands/dynamic-tickets.mds +47 -5
  127. package/src/assets/commands/implement.mds +206 -52
  128. package/src/assets/commands/plan.mds +70 -17
  129. package/src/assets/commands/release.md +64 -17
  130. package/src/assets/commands/resolve.mds +126 -56
  131. package/src/assets/mds/git/_pr.mds +331 -0
  132. package/src/assets/mds/git/_references.mds +135 -0
  133. package/src/assets/mds/tracker/_common.mds +156 -0
  134. package/src/assets/mds/tracker/_github.mds +472 -0
  135. package/src/assets/mds/tracker/_jira.mds +407 -0
  136. package/src/assets/mds/tracker/_linear.mds +449 -0
  137. package/src/assets/mds/tracker/_mcp.mds +299 -0
  138. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
  139. package/src/assets/scripts/hooks/background-memory-update +14 -9
  140. package/src/assets/scripts/hooks/capture-prompt +6 -2
  141. package/src/assets/scripts/hooks/capture-question +6 -2
  142. package/src/assets/scripts/hooks/capture-turn +6 -2
  143. package/src/assets/scripts/hooks/ensure-devflow-init +1 -1
  144. package/src/assets/scripts/hooks/ensure-proxy +64 -5
  145. package/src/assets/scripts/hooks/ensure-root-gitignore +161 -60
  146. package/src/assets/scripts/hooks/hook-log-init +3 -1
  147. package/src/assets/scripts/hooks/json-helper.cjs +223 -5
  148. package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -1
  149. package/src/assets/scripts/hooks/memory-worker +15 -8
  150. package/src/assets/scripts/hooks/pre-compact-memory +12 -8
  151. package/src/assets/scripts/hooks/preamble +1 -4
  152. package/src/assets/scripts/hooks/queue-append +68 -24
  153. package/src/assets/scripts/hooks/session-start-context +355 -8
  154. package/src/assets/scripts/hooks/session-start-memory +12 -8
  155. package/src/assets/scripts/pr-evidence.cjs +1961 -0
  156. package/src/assets/scripts/redact-secrets.cjs +490 -62
  157. package/src/assets/scripts/release-trace.cjs +1143 -0
  158. package/src/assets/scripts/resolve-evidence-policy.cjs +1065 -0
  159. package/src/assets/scripts/verify-evidence.cjs +1822 -0
  160. package/src/assets/skills/compliance/SKILL.md +2 -0
  161. package/src/assets/skills/docs-framework/SKILL.md +5 -3
  162. package/src/assets/skills/git/SKILL.md +8 -82
  163. package/src/assets/skills/git/references/github-api.md +179 -141
  164. package/src/assets/skills/git/references/patterns.md +11 -12
  165. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  166. package/src/assets/skills/review-methodology/references/patterns.md +6 -61
  167. package/src/assets/skills/review-methodology/references/violations.md +14 -22
  168. package/src/targets/claude-code/templates/settings.json +0 -4
  169. package/src/assets/agents/git.md +0 -938
@@ -7,18 +7,20 @@ 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';
20
22
  import { reapplyAgentMapping, readAgentMapping } from '../../core/agent-models.js';
21
- import { readProxyState, writeProxyState, buildProxyState, buildRoutingConfigJson, DEFAULT_PROXY_PORT } from '../../core/proxy-state.js';
23
+ import { readProxyState, writeProxyState, buildProxyState, buildRoutingConfigJson, DEFAULT_PROXY_PORT, proxyJsonExists } from '../../core/proxy-state.js';
22
24
  import { stripDevflowTeammateModeFromJson } from '../../core/teammate-mode-cleanup.js';
23
25
  // Settings/HookMatcher types used by hook utilities — each in their own module
24
26
  import { addHudStatusLine, removeHudStatusLine } from './hud.js';
@@ -26,12 +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';
39
+ import { shouldRunAttributionStep, runAttributionStep, buildClackAttributionPrompts, applyAttributionAnswer, attributionSeedFrom, } from './attribution-prompts.js';
33
40
  import { convergeFromManifest } from '../../targets/claude-code/compliance-install.js';
34
- import { getPendingTurnsPath, getPendingTurnsProcessingPath } from '../../core/project-paths.js';
35
41
  import * as os from 'os';
36
42
  // Re-export pure functions for tests (canonical source is post-install.ts)
37
43
  export { substituteSettingsTemplate, computeGitignoreAppend, mergeDenyList, discoverProjectGitRoots } from '../../targets/claude-code/post-install.js';
@@ -98,6 +104,31 @@ export function formatSweepSummary(report) {
98
104
  }
99
105
  return lines;
100
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
+ }
101
132
  /**
102
133
  * Classify the safe-delete installation state based on the installed version
103
134
  * in the user's shell profile.
@@ -161,6 +192,250 @@ export function resolveComplianceInitState(complianceOption, seedFrameworks) {
161
192
  // Re-export formatComplianceSummary from compliance-prompts.ts so existing test imports
162
193
  // (tests/init-logic.test.ts:1615 — imports from '../src/cli/commands/init.js') keep resolving.
163
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
+ }
164
439
  export const initCommand = new Command('init')
165
440
  .description('Initialize Devflow for Claude Code')
166
441
  .option('--scope <type>', 'Installation scope: user or local (project-only)', /^(user|local)$/i)
@@ -182,6 +457,7 @@ export const initCommand = new Command('init')
182
457
  .option('--no-proxy', 'Disable external model routing')
183
458
  .option('--compliance <list>', 'Enable compliance with comma-separated framework IDs (e.g., gdpr,hipaa)')
184
459
  .option('--no-compliance', 'Disable compliance (artifacts removed; frameworks remembered for re-enable)')
460
+ .option('--tracker <id>', 'Issue tracker provider: github, jira, or linear')
185
461
  .option('--security <mode>', 'Security deny list location: user, managed, or none', /^(user|managed|none)$/i)
186
462
  .option('--hud-only', 'Install only the HUD (no plugins, hooks, or extras)')
187
463
  .option('--recommended', 'Apply recommended defaults after plugin selection (skip advanced prompts)')
@@ -260,28 +536,15 @@ export const initCommand = new Command('init')
260
536
  p.log.error(`Failed to install HUD scripts: ${error instanceof Error ? error.message : error}`);
261
537
  process.exit(1);
262
538
  }
263
- // Read existing manifest to preserve user-set compliance state (disable-keeps-frameworks
264
- // 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).
265
541
  let existingHudManifest = null;
266
542
  try {
267
543
  existingHudManifest = await readManifest(devflowDir);
268
544
  }
269
545
  catch { /* absent on fresh install — existingHudManifest stays null */ }
270
- // Write minimal manifest
271
- const now = new Date().toISOString();
272
546
  try {
273
- await writeManifest(devflowDir, {
274
- version,
275
- plugins: [],
276
- scope,
277
- features: {
278
- ambient: false, memory: false, hud: true, knowledge: false,
279
- learning: false, rules: false, flags: {}, proxy: false,
280
- compliance: existingHudManifest?.features.compliance ?? { enabled: false, frameworks: [] },
281
- },
282
- installedAt: now,
283
- updatedAt: now,
284
- });
547
+ await writeManifest(devflowDir, buildHudOnlyManifest(existingHudManifest, version, scope, new Date().toISOString()));
285
548
  }
286
549
  catch { /* non-fatal */ }
287
550
  p.log.success('HUD installed');
@@ -318,7 +581,7 @@ export const initCommand = new Command('init')
318
581
  // registry defaults — including viewMode 'default' (an externally-set /focus in settings.json
319
582
  // must not survive a factory reset).
320
583
  const { seedManifest, seedConfig, seedSettings } = resolveResetGatedInputs(!!options.reset, existingManifest, earlyProjectConfig, earlySettingsJson ?? '');
321
- const seed = resolveInitSeed(seedManifest, seedConfig, seedSettings, DEVFLOW_PLUGINS);
584
+ const seed = resolveInitSeed(seedManifest, seedSettings, DEVFLOW_PLUGINS);
322
585
  // Early validation: parse --compliance <list> at the boundary before any prompts (PF-parse-at-boundary).
323
586
  // options.compliance: string → --compliance <list>; false → --no-compliance; undefined → not passed
324
587
  let cliComplianceOverride;
@@ -332,6 +595,20 @@ export const initCommand = new Command('init')
332
595
  cliComplianceOverride = complianceStateResult.value;
333
596
  }
334
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
+ }
335
612
  // Select plugins to install
336
613
  let selectedPlugins = [];
337
614
  if (options.plugin) {
@@ -505,6 +782,9 @@ export const initCommand = new Command('init')
505
782
  // CLI override applied below in both Recommended and Advanced paths.
506
783
  let complianceEnabled = seed.features.compliance.enabled;
507
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;
508
788
  let enabledFlags = { ...seed.flags };
509
789
  // viewModeExplicit: true when --reset is passed; signals resolveFinalViewMode to let the
510
790
  // seed-time view-mode win over an externally-set value in settings.json.
@@ -519,6 +799,47 @@ export const initCommand = new Command('init')
519
799
  // The final value is written to the manifest and consumed by the dedicated security step.
520
800
  let securityMode = 'user'; // placeholder; overwritten below by resolve
521
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
+ };
522
843
  // Safe-delete detection (both paths need this)
523
844
  const platform = detectPlatform();
524
845
  const shell = detectShell();
@@ -551,6 +872,16 @@ export const initCommand = new Command('init')
551
872
  // Step messages not emitted here — the Recommended summary note (below) already
552
873
  // prints the Compliance line from complianceSummary via formatComplianceSummary.
553
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);
882
+ // No attribution step here: the suppress-attribution question is Advanced-only (D27).
883
+ // Recommended silently carries the seeded value in enabledFlags — fresh installs get
884
+ // the registry default (off), re-inits get prior state. See shouldRunAttributionStep.
554
885
  // Apply explicit CLI toggles on top of the seed.
555
886
  // Precedence: explicit CLI flag > wizard result > seed value (prior state > registry default).
556
887
  // proxy is included: --proxy/--no-proxy CLI flags override the seed in non-interactive mode.
@@ -563,6 +894,8 @@ export const initCommand = new Command('init')
563
894
  rules: options.rules,
564
895
  proxy: options.proxy,
565
896
  compliance: cliComplianceOverride ?? wizardCompliance,
897
+ // Precedence: cliOverride ?? wizardResult ?? seed (applyCliToggles supplies the seed arm).
898
+ tracker: cliTrackerOverride ?? wizardTracker,
566
899
  });
567
900
  ambientEnabled = effectiveFeatures.ambient;
568
901
  memoryEnabled = effectiveFeatures.memory;
@@ -573,6 +906,7 @@ export const initCommand = new Command('init')
573
906
  proxyEnabled = effectiveFeatures.proxy;
574
907
  complianceEnabled = effectiveFeatures.compliance.enabled;
575
908
  complianceFrameworks = effectiveFeatures.compliance.frameworks;
909
+ trackerProvider = effectiveFeatures.tracker.provider;
576
910
  // enabledFlags is already initialised to seed.flags above.
577
911
  // Compute safe-delete block synchronously so we know whether to fetch installed version
578
912
  if (profilePath && safeDeleteAvailable) {
@@ -608,6 +942,10 @@ export const initCommand = new Command('init')
608
942
  `Knowledge bases: ${knowledgeEnabled ? 'enabled' : 'disabled'}`,
609
943
  `Ext model routing: ${proxyEnabled ? 'enabled' : 'disabled'}`,
610
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)}`,
611
949
  `View mode: ${readViewMode(enabledFlags)}`,
612
950
  `Claude Code flags: ${defaultFlagCount} configured`,
613
951
  `${claudeignoreEnabled ? '.claudeignore: created' : ''}`,
@@ -653,7 +991,8 @@ export const initCommand = new Command('init')
653
991
  'compaction. Clear your session at any point and resume right\n' +
654
992
  'where you left off.\n\n' +
655
993
  'Runs a background agent on session stop that consumes additional\n' +
656
- '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');
657
996
  const memoryChoice = await p.confirm({
658
997
  message: 'Enable working memory? (Recommended)',
659
998
  initialValue: seed.features.memory,
@@ -686,7 +1025,8 @@ export const initCommand = new Command('init')
686
1025
  else {
687
1026
  p.note('Per-feature knowledge bases capture cross-cutting patterns,\n' +
688
1027
  'conventions, and gotchas. Created and updated automatically\n' +
689
- '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');
690
1030
  const knowledgeChoice = await p.confirm({
691
1031
  message: 'Enable feature knowledge bases? (Recommended)',
692
1032
  initialValue: seed.features.knowledge,
@@ -703,7 +1043,7 @@ export const initCommand = new Command('init')
703
1043
  else {
704
1044
  p.note('Detects architectural decisions and pitfalls from your session\n' +
705
1045
  'dialogs. Runs a background agent on session stop that consumes\n' +
706
- 'additional tokens.', 'Learning (Decision/Pitfall Tracking)');
1046
+ 'additional tokens. Applies to every project.', 'Learning (Decision/Pitfall Tracking)');
707
1047
  const learningChoice = await p.confirm({
708
1048
  message: 'Enable learning? (Recommended)',
709
1049
  initialValue: seed.features.learning,
@@ -793,6 +1133,63 @@ export const initCommand = new Command('init')
793
1133
  // No third case in practice: on this path the predicate only returns false for a
794
1134
  // CLI override (isTTY is guaranteed true by the non-TTY guard above). If it ever
795
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
+ }
1158
+ // Attribution feature (after compliance, before flags). This is the ONLY call site —
1159
+ // the attribution question is Advanced-only (D27); the Recommended path never asks and
1160
+ // silently carries the seeded value. The gate stays an explicit predicate call so the
1161
+ // documented gate table in attribution-prompts.ts remains the single authority.
1162
+ // isTTY is guaranteed true here (the non-TTY guard above exit-1'd); passing it keeps
1163
+ // the promptless contract enforced at the predicate rather than by position (PF-029).
1164
+ if (shouldRunAttributionStep({
1165
+ // D27-GATE: bind to the resolved mode so all four documented gate-table rows
1166
+ // are reachable and the predicate — not lexical placement — enforces Advanced-only.
1167
+ // Using useRecommended ? 'recommended' : 'advanced' makes the gate testable from
1168
+ // both sides and prevents the Recommended path from accidentally running the step
1169
+ // if this block is ever repositioned (applies PF-029).
1170
+ mode: useRecommended ? 'recommended' : 'advanced',
1171
+ isTTY: process.stdin.isTTY,
1172
+ })) {
1173
+ const attributionStep = await runAttributionStep({
1174
+ // attributionSeedFrom keeps the seed a real boolean regardless of the stored
1175
+ // FlagsRecord value type — undefined/null/false all map to false (PF-018).
1176
+ seed: attributionSeedFrom(enabledFlags),
1177
+ prompts: buildClackAttributionPrompts(),
1178
+ });
1179
+ if (attributionStep.kind === 'cancelled') {
1180
+ p.cancel('Installation cancelled.');
1181
+ process.exit(0);
1182
+ }
1183
+ // D27: applyAttributionAnswer is the single merge site for the wizard answer.
1184
+ enabledFlags = applyAttributionAnswer(enabledFlags, attributionStep);
1185
+ // Advanced path emits an outcome line (mirrors compliance step pattern).
1186
+ for (const msg of attributionStep.messages) {
1187
+ if (msg.level === 'success')
1188
+ p.log.success(msg.text);
1189
+ else
1190
+ p.log.info(msg.text);
1191
+ }
1192
+ }
796
1193
  /**
797
1194
  * D40: init applies seeded flag defaults non-interactively. Flags are customized
798
1195
  * exclusively via `devflow flags`; re-init preserves existing values and adopts
@@ -954,6 +1351,10 @@ export const initCommand = new Command('init')
954
1351
  }
955
1352
  }
956
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;
957
1358
  {
958
1359
  const userSettingsJson = earlySettingsJson;
959
1360
  let managedExists = false;
@@ -966,6 +1367,7 @@ export const initCommand = new Command('init')
966
1367
  }
967
1368
  catch { /* absent or unsupported platform */ }
968
1369
  const detected = detectDenyState(userSettingsJson, managedExists, managedContentJson);
1370
+ managedDenyDetected = detected.managed;
969
1371
  const flagValue = options.security;
970
1372
  const manifestMode = existingManifest?.features.security;
971
1373
  const resolution = resolveSecurityAction(flagValue, manifestMode, detected, process.stdin.isTTY);
@@ -1029,9 +1431,17 @@ export const initCommand = new Command('init')
1029
1431
  if (ambientEnabled && ambientPlugin && !pluginsToInstall.includes(ambientPlugin)) {
1030
1432
  pluginsToInstall.push(ambientPlugin);
1031
1433
  }
1032
- // Skills: install ALL from ALL plugins (skills are tiny markdown files;
1033
- // commands need skills from other plugins to function)
1034
- 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);
1035
1445
  // Agents: install only from selected plugins
1036
1446
  const { agentsMap } = buildAssetMaps(pluginsToInstall);
1037
1447
  // Rules: install only from selected plugins (plugin-scoped, not universal)
@@ -1068,16 +1478,23 @@ export const initCommand = new Command('init')
1068
1478
  catch { /* absent — no legacy artifact */ }
1069
1479
  // Install via file copy
1070
1480
  let installReport;
1481
+ const installWarnings = [];
1071
1482
  try {
1072
1483
  installReport = await installViaFileCopy({
1073
1484
  plugins: pluginsToInstall,
1485
+ effectivePlugins,
1074
1486
  claudeDir,
1075
1487
  devflowDir,
1076
1488
  skillsMap,
1077
1489
  agentsMap,
1078
1490
  rulesMap,
1491
+ trackerProvider,
1079
1492
  isPartialInstall: !!options.plugin,
1080
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); },
1081
1498
  });
1082
1499
  }
1083
1500
  catch (error) {
@@ -1361,18 +1778,18 @@ export const initCommand = new Command('init')
1361
1778
  content = ambientEnabled ? await addAmbientHook(cleanedForAmbient, devflowDir) : cleanedForAmbient;
1362
1779
  // Capture hooks — always-on (like the context hook below), remove-then-add for
1363
1780
  // upgrade safety. Queue-append only (capture-prompt/capture-turn/capture-question);
1364
- // each script gates its own per-queue write internally via feature config, so there
1365
- // 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
1366
1783
  // so capture-turn lands before memory-worker in the Stop array (AC-C2 ordering:
1367
1784
  // append-before-spawn).
1368
1785
  const cleanedForCapture = removeCaptureHooks(content);
1369
1786
  content = addCaptureHooks(cleanedForCapture, devflowDir);
1370
- // Memory hooks — always remove-then-add to upgrade hook format (e.g., .sh → run-hook).
1371
- // Three hooks: Stop (memory-worker), SessionStart (session-start-memory), PreCompact.
1372
- // Learning agent (spawned via session-start-context directive) handles decision/pitfall
1373
- // detection. Knowledge is handled in-command via write-through (knowledge_writeback MDS partial).
1374
- const cleaned = removeMemoryHooks(content);
1375
- 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);
1376
1793
  // HUD statusLine
1377
1794
  content = hudEnabled
1378
1795
  ? addHudStatusLine(content, devflowDir)
@@ -1411,14 +1828,18 @@ export const initCommand = new Command('init')
1411
1828
  content = JSON.stringify(parsedSettings, null, 2) + '\n';
1412
1829
  }
1413
1830
  // Proxy env: ANTHROPIC_BASE_URL strip-then-add, scoped to managed port.
1414
- // Read proxy.json to learn which port we own — only that URL is stripped.
1415
- // A user's own localhost gateway on any other port is preserved.
1831
+ // D-STRIP-1: only strip when proxy.json exists — evidence that Devflow previously
1832
+ // wrote ANTHROPIC_BASE_URL. Without this gate, a fresh init on a machine where
1833
+ // DEFAULT_PROXY_PORT (4141) happens to be a user's own gateway (LiteLLM etc.)
1834
+ // would silently delete both their URL and the window-enforcement var.
1416
1835
  // Invariant: proxy.json always reflects the final settled state after the
1417
1836
  // preflight block above — all paths that force proxyEnabled=false also write
1418
1837
  // proxy.json enabled:false (avoids PF-015), so managedPort == effectivePort.
1419
- const proxyStateForStrip = await readProxyState(devflowDir);
1420
- const managedPort = proxyStateForStrip.ok ? proxyStateForStrip.value.port : DEFAULT_PROXY_PORT;
1421
- content = stripProxyEnv(content, managedPort);
1838
+ if (await proxyJsonExists(devflowDir)) {
1839
+ const proxyStateForStrip = await readProxyState(devflowDir);
1840
+ const managedPort = proxyStateForStrip.ok ? proxyStateForStrip.value.port : DEFAULT_PROXY_PORT;
1841
+ content = stripProxyEnv(content, managedPort);
1842
+ }
1422
1843
  if (proxyEnabled)
1423
1844
  content = applyProxyEnv(content, effectivePort);
1424
1845
  if (content !== original) {
@@ -1437,32 +1858,21 @@ export const initCommand = new Command('init')
1437
1858
  p.log.warn(`Could not configure settings.json: ${err instanceof Error ? err.message : err}. ` +
1438
1859
  'Manifest records intended state; run devflow init again to retry.');
1439
1860
  }
1440
- // Write .devflow/config.json to manage per-feature enable/disable at runtime.
1441
- // Uses writeConfig (full atomic write) rather than three updateFeature calls because
1442
- // init always sets all three features at once and is never concurrent with toggle
1443
- // commands — it is a one-time setup action. See D1 in feature-config.ts for the
1444
- // 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).
1445
1868
  if (gitRoot) {
1446
- await writeConfig(gitRoot, {
1447
- memory: memoryEnabled,
1448
- learning: learningEnabled,
1449
- knowledge: knowledgeEnabled,
1869
+ await writeManagedConfig(gitRoot, {
1450
1870
  // reviewPublication has no prompt, so it is carried over from the
1451
1871
  // reset-gated snapshot rather than re-read from disk: seedConfig is null
1452
1872
  // under --reset, which is what collapses the field back to 'auto' with
1453
1873
  // every other feature (PF-015 — read the post-gate binding, not the file).
1454
- reviewPublication: seedConfig?.reviewPublication ?? 'auto',
1874
+ reviewPublication: seedConfig?.reviewPublication ?? DEFAULT_CONFIG.reviewPublication,
1455
1875
  });
1456
- // Drain orphaned queue files when memory is disabled so stale turns
1457
- // don't process on a future re-enable. Mirrors memory.ts --disable drain.
1458
- if (!memoryEnabled) {
1459
- await Promise.all([
1460
- fs.unlink(getPendingTurnsPath(gitRoot)).catch((e) => { if (e.code !== 'ENOENT')
1461
- throw e; }),
1462
- fs.unlink(getPendingTurnsProcessingPath(gitRoot)).catch((e) => { if (e.code !== 'ENOENT')
1463
- throw e; }),
1464
- ]);
1465
- }
1466
1876
  }
1467
1877
  // Configure HUD
1468
1878
  const existingHud = loadHudConfig();
@@ -1583,11 +1993,26 @@ export const initCommand = new Command('init')
1583
1993
  }
1584
1994
  }
1585
1995
  else if (securityMode === 'none') {
1586
- // None: strip Devflow deny entries from user settings.
1587
- // 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).
1588
1999
  const stripResult = await stripUserSecurityDenyList(userSettingsPath);
1589
2000
  if (stripResult && verbose)
1590
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
+ }
1591
2016
  }
1592
2017
  else {
1593
2018
  // Exhaustive guard — if TypeScript reaches here, a new SecurityMode variant was added
@@ -1640,12 +2065,21 @@ export const initCommand = new Command('init')
1640
2065
  // failed removal leaves a retired asset live. Both must surface.
1641
2066
  // After I09, the installer's knownNames set unions FEATURE_OWNED_SKILLS, so
1642
2067
  // devflow:compliance is never swept here — no suppression predicate is needed.
1643
- for (const line of formatSweepSummary(installReport)) {
1644
- if (line.level === 'warn')
1645
- p.log.warn(line.message);
1646
- else
1647
- p.log.info(line.message);
1648
- }
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);
1649
2083
  const installedSet = new Set(pluginsToInstall.flatMap(p => p.commands).filter(c => c.length > 0));
1650
2084
  const orderedCommands = WORKFLOW_ORDER.filter(cmd => installedSet.has(cmd));
1651
2085
  if (orderedCommands.length > 0) {
@@ -1698,11 +2132,13 @@ export const initCommand = new Command('init')
1698
2132
  p.log.info(`Deduplication: ${agentsMap.size} unique agents (from ${totalAgentDeclarations} declarations)`);
1699
2133
  }
1700
2134
  // Write installation manifest for upgrade tracking (non-fatal — install already succeeded)
1701
- const installedPluginNames = pluginsToInstall.map(pl => pl.name);
1702
2135
  const now = new Date().toISOString();
1703
2136
  const manifestData = {
1704
2137
  version,
1705
- 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,
1706
2142
  scope,
1707
2143
  // Snapshot of known plugin names at this install — used by resolveSeedPlugins on next init
1708
2144
  // to detect new non-optional plugins and auto-adopt them.
@@ -1724,16 +2160,54 @@ export const initCommand = new Command('init')
1724
2160
  // and Advanced wizard selection. convergeComplianceArtifacts was called above.
1725
2161
  // normalizeFrameworks: dedup + filter unknowns before persisting.
1726
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 },
1727
2167
  },
1728
2168
  installedAt: existingManifest?.installedAt ?? now,
1729
2169
  updatedAt: now,
1730
2170
  };
1731
- try {
1732
- await writeManifest(devflowDir, manifestData);
1733
- }
1734
- catch (error) {
1735
- p.log.warn(`Failed to write installation manifest (install succeeded): ${error instanceof Error ? error.message : error}`);
1736
- }
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);
1737
2211
  // External model routing status line (Advanced path / explicit --proxy flag only)
1738
2212
  if (proxyEnabled) {
1739
2213
  p.log.info(`External model routing: ${color.green('enabled')} — takes effect in new Claude Code sessions`);