devflow-kit 3.0.1 → 3.2.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 +49 -0
  2. package/README.md +1 -1
  3. package/dist/agents/git.md +2 -2
  4. package/dist/cli/agents-view/index.js +1 -1
  5. package/dist/cli/agents-view/render.js +71 -17
  6. package/dist/cli/agents-view/state.js +42 -16
  7. package/dist/cli/agents-view/terminal.js +5 -5
  8. package/dist/cli/commands/agents.js +142 -51
  9. package/dist/cli/commands/ambient.js +1 -1
  10. package/dist/cli/commands/attribution-prompts.js +8 -8
  11. package/dist/cli/commands/capture.js +1 -1
  12. package/dist/cli/commands/compliance-prompts.js +8 -8
  13. package/dist/cli/commands/compliance.js +8 -7
  14. package/dist/cli/commands/flags.js +33 -31
  15. package/dist/cli/commands/hud.js +1 -1
  16. package/dist/cli/commands/init-seed.js +9 -9
  17. package/dist/cli/commands/init.js +162 -85
  18. package/dist/cli/commands/install-report.js +10 -10
  19. package/dist/cli/commands/learning.js +302 -136
  20. package/dist/cli/commands/memory.js +36 -15
  21. package/dist/cli/commands/proxy.js +23 -23
  22. package/dist/cli/commands/rules.js +6 -5
  23. package/dist/cli/commands/tracker-prompts.js +6 -6
  24. package/dist/cli/commands/tracker.js +9 -9
  25. package/dist/cli/commands/uninstall.js +183 -59
  26. package/dist/cli/flags-view/render.js +5 -5
  27. package/dist/cli/flags-view/state.js +9 -9
  28. package/dist/cli/flags-view/terminal.js +4 -4
  29. package/dist/cli/tui/cells.js +1 -1
  30. package/dist/cli/tui/terminal.js +6 -6
  31. package/dist/commands/code-review.md +0 -2
  32. package/dist/commands/debug.md +14 -11
  33. package/dist/commands/dynamic-build.md +51 -47
  34. package/dist/commands/dynamic-plan.md +27 -7
  35. package/dist/commands/dynamic-profile.md +17 -3
  36. package/dist/commands/dynamic-tickets.md +18 -4
  37. package/dist/commands/explore.md +9 -3
  38. package/dist/commands/implement.md +20 -16
  39. package/dist/commands/plan.md +13 -9
  40. package/dist/commands/release.md +23 -3
  41. package/dist/commands/research.md +9 -3
  42. package/dist/commands/resolve.md +9 -12
  43. package/dist/commands/self-review.md +0 -2
  44. package/dist/core/agent-frontmatter.js +28 -3
  45. package/dist/core/agent-models.js +204 -42
  46. package/dist/core/agent-state.js +28 -6
  47. package/dist/core/ansi.js +2 -2
  48. package/dist/core/assets.js +1 -1
  49. package/dist/core/cache.js +7 -8
  50. package/dist/core/codex-auth-inspect.js +4 -4
  51. package/dist/core/compliance-compose.js +3 -3
  52. package/dist/core/compliance.js +3 -4
  53. package/dist/core/evidence-policy.js +14 -13
  54. package/dist/core/external-models.js +1 -1
  55. package/dist/core/feature-config.js +71 -13
  56. package/dist/core/feature-switch.js +3 -3
  57. package/dist/core/flags.js +49 -25
  58. package/dist/core/fs-atomic.js +6 -7
  59. package/dist/core/learning-queue-cleanup.js +16 -81
  60. package/dist/core/learning-store.js +61 -0
  61. package/dist/core/linked-path.js +46 -0
  62. package/dist/core/manifest.js +5 -5
  63. package/dist/core/mds-variants.js +13 -13
  64. package/dist/core/model-discovery.js +8 -8
  65. package/dist/core/observations.js +17 -101
  66. package/dist/core/orphan-sweep.js +4 -4
  67. package/dist/core/plugins.js +13 -8
  68. package/dist/core/project-paths.js +9 -13
  69. package/dist/core/proxy-log.js +8 -8
  70. package/dist/core/proxy-state.js +3 -3
  71. package/dist/core/queue-drain.js +31 -0
  72. package/dist/core/reference-sweep.js +6 -6
  73. package/dist/core/teammate-mode-cleanup.js +1 -1
  74. package/dist/core/tracker.js +14 -14
  75. package/dist/hud/colors.js +2 -2
  76. package/dist/hud/components/learning-counts.js +54 -22
  77. package/dist/hud/components/version-badge.js +1 -1
  78. package/dist/skills/git/references/pr/resolve-review-threads.md +2 -2
  79. package/dist/skills/git/references/tracker/github/create-release.md +2 -2
  80. package/dist/skills/git/references/tracker/jira/create-release.md +2 -2
  81. package/dist/skills/git/references/tracker/linear/create-release.md +2 -2
  82. package/dist/targets/claude-code/compliance-install.js +17 -15
  83. package/dist/targets/claude-code/hooks.js +2 -2
  84. package/dist/targets/claude-code/installer.js +59 -32
  85. package/dist/targets/claude-code/legacy.js +1 -1
  86. package/dist/targets/claude-code/post-install.js +135 -45
  87. package/dist/targets/claude-code/tracker-install.js +2 -2
  88. package/package.json +1 -1
  89. package/src/assets/agents/code.md +15 -21
  90. package/src/assets/agents/design.md +4 -2
  91. package/src/assets/agents/diagnose.md +3 -1
  92. package/src/assets/agents/evaluate.md +4 -0
  93. package/src/assets/agents/git.mds +2 -2
  94. package/src/assets/agents/knowledge.md +5 -3
  95. package/src/assets/agents/learning.md +281 -196
  96. package/src/assets/agents/research.md +3 -1
  97. package/src/assets/agents/review.md +5 -3
  98. package/src/assets/agents/scrutinize.md +5 -1
  99. package/src/assets/agents/simplify.md +4 -0
  100. package/src/assets/agents/skim.md +4 -2
  101. package/src/assets/agents/synthesize.md +6 -0
  102. package/src/assets/agents/test.md +18 -10
  103. package/src/assets/agents/triage.md +11 -9
  104. package/src/assets/agents/validate.md +14 -10
  105. package/src/assets/commands/_partials/_decisions.mds +8 -3
  106. package/src/assets/commands/_partials/_docs_root.mds +3 -3
  107. package/src/assets/commands/_partials/_engine.mds +16 -32
  108. package/src/assets/commands/_partials/_knowledge.mds +0 -2
  109. package/src/assets/commands/_partials/_preamble.mds +6 -2
  110. package/src/assets/commands/_partials/_settings.mds +2 -2
  111. package/src/assets/commands/_partials/_tracker.mds +1 -1
  112. package/src/assets/commands/code-review.mds +0 -2
  113. package/src/assets/commands/debug.mds +13 -8
  114. package/src/assets/commands/dynamic-build.mds +18 -12
  115. package/src/assets/commands/dynamic-plan.mds +10 -4
  116. package/src/assets/commands/dynamic-profile.mds +1 -1
  117. package/src/assets/commands/dynamic-tickets.mds +2 -2
  118. package/src/assets/commands/explore.mds +9 -1
  119. package/src/assets/commands/implement.mds +19 -13
  120. package/src/assets/commands/plan.mds +12 -8
  121. package/src/assets/commands/release.md +23 -3
  122. package/src/assets/commands/research.mds +9 -3
  123. package/src/assets/commands/resolve.mds +9 -10
  124. package/src/assets/mds/git/_pr.mds +3 -3
  125. package/src/assets/mds/tracker/_common.mds +1 -1
  126. package/src/assets/mds/tracker/_github.mds +3 -3
  127. package/src/assets/mds/tracker/_jira.mds +3 -3
  128. package/src/assets/mds/tracker/_linear.mds +3 -3
  129. package/src/assets/mds/tracker/_mcp.mds +6 -5
  130. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +4 -2
  131. package/src/assets/scripts/hooks/background-memory-update +97 -33
  132. package/src/assets/scripts/hooks/capture-prompt +4 -3
  133. package/src/assets/scripts/hooks/capture-question +4 -3
  134. package/src/assets/scripts/hooks/capture-turn +5 -20
  135. package/src/assets/scripts/hooks/ensure-devflow-init +14 -2
  136. package/src/assets/scripts/hooks/ensure-proxy +5 -6
  137. package/src/assets/scripts/hooks/ensure-root-gitignore +123 -11
  138. package/src/assets/scripts/hooks/git-marker +71 -0
  139. package/src/assets/scripts/hooks/is-hex-sha +1 -1
  140. package/src/assets/scripts/hooks/json-helper.cjs +345 -944
  141. package/src/assets/scripts/hooks/json-parse +25 -129
  142. package/src/assets/scripts/hooks/lib/decisions-format.cjs +205 -156
  143. package/src/assets/scripts/hooks/lib/learning-store.cjs +3207 -0
  144. package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +7 -5
  145. package/src/assets/scripts/hooks/lib/project-paths.cjs +13 -19
  146. package/src/assets/scripts/hooks/lib/render-decisions.cjs +253 -226
  147. package/src/assets/scripts/hooks/memory-worker +10 -0
  148. package/src/assets/scripts/hooks/pre-compact-memory +66 -14
  149. package/src/assets/scripts/hooks/preamble +9 -1
  150. package/src/assets/scripts/hooks/queue-append +55 -23
  151. package/src/assets/scripts/hooks/resolve-project-root +3 -4
  152. package/src/assets/scripts/hooks/session-start-context +146 -45
  153. package/src/assets/scripts/hooks/session-start-memory +33 -11
  154. package/src/assets/scripts/lib/project-config.cjs +2 -2
  155. package/src/assets/scripts/pr-evidence.cjs +3 -3
  156. package/src/assets/scripts/redact-secrets.cjs +20 -20
  157. package/src/assets/scripts/release-trace.cjs +1 -1
  158. package/src/assets/scripts/resolve-evidence-policy.cjs +3 -3
  159. package/src/assets/scripts/resolve-settings.cjs +3 -3
  160. package/src/assets/scripts/verify-evidence.cjs +2 -2
  161. package/src/assets/skills/apply-decisions/SKILL.md +37 -17
  162. package/src/assets/skills/docs-framework/SKILL.md +2 -2
  163. package/src/assets/skills/feature-knowledge/SKILL.md +6 -5
  164. package/src/assets/skills/test-driven-development/SKILL.md +6 -4
  165. package/dist/core/observation-io.js +0 -50
  166. package/src/assets/scripts/hooks/decisions-usage-scan.cjs +0 -131
@@ -29,6 +29,7 @@ import { writeSettingsFileAtomic } from '../../core/fs-atomic.js';
29
29
  import { stripFlags } from '../../core/flags.js';
30
30
  import { stripDevflowTeammateModeFromJson } from '../../core/teammate-mode-cleanup.js';
31
31
  import { getPackageRoot, isContainedIn } from '../../core/paths.js';
32
+ import { firstSymbolicLink } from '../../core/linked-path.js';
32
33
  /**
33
34
  * Where a retired repo-local install lives: `<gitRoot>/.claude` and `<gitRoot>/.devflow`.
34
35
  *
@@ -68,6 +69,77 @@ async function scopeInstallPaths(scope, gitRoot) {
68
69
  return getInstallationPaths();
69
70
  return gitRoot === null ? null : legacyLocalInstallPaths(gitRoot);
70
71
  }
72
+ /**
73
+ * The user scope's guard, which passes everything: `~/.claude` and `~/.devflow` are
74
+ * the user's own, so a symbolic link there is the user's choice.
75
+ */
76
+ const changeAnything = async () => true;
77
+ /**
78
+ * `folder` and every path below it down to `target`, the target included, outermost
79
+ * first, for whichever of the two folders holds `target`; null for a target in
80
+ * neither. PURE.
81
+ */
82
+ function pathsDownTo(folders, target) {
83
+ for (const folder of [folders.claudeDir, folders.devflowDir]) {
84
+ const rel = path.relative(folder, target);
85
+ if (rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel))
86
+ continue;
87
+ const chain = [folder];
88
+ for (const part of rel === '' ? [] : rel.split(path.sep)) {
89
+ chain.push(path.join(chain[chain.length - 1], part));
90
+ }
91
+ return chain;
92
+ }
93
+ return null;
94
+ }
95
+ /**
96
+ * The guard for a legacy local install's removals and settings rewrites.
97
+ *
98
+ * D-CLI-NO-SYMLINK (firstSymbolicLink): a repository can commit `.claude`, `.devflow`
99
+ * or any folder or file in them as a symbolic link, and enough of `.claude` for a
100
+ * plain `uninstall` to detect a legacy install there. `fs.rm` and the settings
101
+ * rewrite act inside whatever folder a link on the way names, so a target is acted
102
+ * on only where neither `<gitRoot>/.claude` or `<gitRoot>/.devflow` nor anything
103
+ * below it on the way to the target, the target included, is a link. Otherwise the
104
+ * target is skipped, the link is left as it was, and one warning per link, opened
105
+ * by `skipped`, names it. A target that cannot be checked, or lies in neither
106
+ * folder, is skipped and reported the same way. The rest of the install is still
107
+ * removed, so a real, unlinked legacy install comes out whole.
108
+ */
109
+ function legacyLocalChangeGuard(folders, skipped) {
110
+ const reported = new Set();
111
+ const report = (subject, reason) => {
112
+ if (reported.has(subject))
113
+ return;
114
+ reported.add(subject);
115
+ p.log.warn(`${skipped}: ${reason}`);
116
+ };
117
+ return async (target) => {
118
+ const chain = pathsDownTo(folders, target);
119
+ if (chain === null) {
120
+ report(target, `${target} is outside the legacy install`);
121
+ return false;
122
+ }
123
+ let link;
124
+ try {
125
+ link = await firstSymbolicLink(chain);
126
+ }
127
+ catch (error) {
128
+ report(target, `${target} could not be checked for a symbolic link (${error instanceof Error ? error.message : String(error)})`);
129
+ return false;
130
+ }
131
+ if (link === null)
132
+ return true;
133
+ report(link, `${link} is a symbolic link, and devflow removes or changes nothing through one`);
134
+ return false;
135
+ };
136
+ }
137
+ /** The guard for one scope's full or selective phase: checked for a legacy local install, open for the user's. */
138
+ function scopeChangeGuard(scope, folders) {
139
+ return scope === 'local'
140
+ ? legacyLocalChangeGuard(folders, 'Legacy local install not fully removed')
141
+ : changeAnything;
142
+ }
71
143
  /**
72
144
  * The plugins the manifest records as installed, as registry definitions.
73
145
  *
@@ -178,7 +250,7 @@ export function formatDryRunPlan(assets) {
178
250
  * - "prompt" — interactive (isTTY=true); caller should ask the user
179
251
  *
180
252
  * @D1 Non-interactive-preserve invariant: when isTTY is false the result is
181
- * always "preserve", never "prompt" — avoids PF-004 half-applied-state hazard.
253
+ * always "preserve", never "prompt" — avoids a half-applied-state hazard.
182
254
  */
183
255
  export function resolveSecurityRemovalDecision(opts) {
184
256
  if (!opts.anySecurityPresent || opts.keepDocs)
@@ -193,7 +265,7 @@ export function resolveSecurityRemovalDecision(opts) {
193
265
  *
194
266
  * A cancel (user presses Ctrl-C on this prompt) is treated as decline: the
195
267
  * .devflow/ directory is preserved and the uninstall continues with the remaining
196
- * steps rather than aborting via process.exit() (applies PF-014, applies ADR-003).
268
+ * steps rather than aborting via process.exit().
197
269
  *
198
270
  * PURE — no I/O, fully testable.
199
271
  *
@@ -233,8 +305,8 @@ export function partitionProjectData(entries) {
233
305
  * realpaths, so macOS's `/var` → `/private/var` and a symlinked HOME still match.
234
306
  *
235
307
  * A `.devflow` that is a symbolic link to anywhere else is skipped too, and never
236
- * followed: its target is a directory devflow cannot prove it wrote (applies
237
- * ADR-024), so a confirmed cleanup must not empty it — nor unlink a link the user made.
308
+ * followed: its target is a directory devflow cannot prove it wrote, so a
309
+ * confirmed cleanup must not empty it — nor unlink a link the user made.
238
310
  */
239
311
  export async function resolveProjectDataPlan(opts) {
240
312
  if (opts.gitRoot === null)
@@ -320,9 +392,9 @@ export function formatProjectDataPlan(plan) {
320
392
  * @D5 Precondition guard: any anomalous devflowDir falls back to 'artifacts-only' rather
321
393
  * than throwing — business logic must not throw (engineering rule). The explicit guard
322
394
  * makes the invariant present in production code, not only in tests (reliability rule).
323
- * @D6 avoids PF-014: the caller must NOT process.exit() after a cancel/decline response;
395
+ * @D6 The caller must NOT process.exit() after a cancel/decline response;
324
396
  * removeAllDevFlow has already run by the time the prompt fires, so removeDevFlowInstallArtifacts
325
- * must execute on every non-confirm path to leave a clean end-state (applies ADR-003).
397
+ * must execute on every non-confirm path to leave a clean end-state.
326
398
  * @D7 keepDocs gate: when the caller passes --keep-docs, the entire ~/.devflow dir cleanup
327
399
  * prompt is suppressed — artifacts-only regardless of isTTY or userContent. This prevents
328
400
  * --keep-docs from triggering prompts about skill shadows or preference-profile.md.
@@ -517,7 +589,7 @@ export function installArtifactPaths(devflowDir) {
517
589
  { relPath: 'agent-models.json' },
518
590
  // cost history — auto-generated session telemetry, not user-authored.
519
591
  // The whole costs/ tree is removed so sessions/ and archive.jsonl cannot
520
- // drift from their write sites in src/hud/cost-history.ts (avoids PF-013).
592
+ // drift from their write sites in src/hud/cost-history.ts.
521
593
  { relPath: 'costs', isDir: true },
522
594
  // proxy artifacts
523
595
  { relPath: 'proxy.json' },
@@ -545,7 +617,7 @@ export function installArtifactPaths(devflowDir) {
545
617
  { relPath: 'logs', isDir: true },
546
618
  // Model-discovery + HUD component caches. hudCacheDir() is the authoritative
547
619
  // accessor for the cache/ parent (modelCacheDir() resolves beneath it), so the
548
- // removal site stays byte-locked to the write sites (avoids PF-013).
620
+ // removal site stays byte-locked to the write sites.
549
621
  { relPath: path.relative(devflowDir, hudCacheDir(devflowDir)), isDir: true },
550
622
  ];
551
623
  }
@@ -567,19 +639,24 @@ export function installArtifactPaths(devflowDir) {
567
639
  * agent-models.json is an INSTALL ARTIFACT (stale per-agent overrides silently
568
640
  * re-apply to renamed/deleted agents on reinstall — AC-P1-F4) and therefore
569
641
  * belongs in this list, not in enumerateUserDevFlowContent.
642
+ *
643
+ * Each removal passes `mayChange` first, which skips one a symbolic link would
644
+ * redirect in a legacy local install (D-CLI-NO-SYMLINK, legacyLocalChangeGuard).
570
645
  */
571
- export async function removeDevFlowInstallArtifacts(devflowDir, verbose) {
646
+ export async function removeDevFlowInstallArtifacts(devflowDir, verbose, mayChange = changeAnything) {
572
647
  const manifestPath = path.join(devflowDir, 'manifest.json');
573
- try {
574
- await fs.rm(manifestPath, { force: true });
575
- if (verbose) {
576
- p.log.success('Removed manifest.json');
648
+ if (await mayChange(manifestPath)) {
649
+ try {
650
+ await fs.rm(manifestPath, { force: true });
651
+ if (verbose) {
652
+ p.log.success('Removed manifest.json');
653
+ }
654
+ }
655
+ catch (error) {
656
+ p.log.warn(`Could not remove manifest.json: ${error}`);
577
657
  }
578
658
  }
579
- catch (error) {
580
- p.log.warn(`Could not remove manifest.json: ${error}`);
581
- }
582
- // Proxy install artifacts — remove non-fatally (per-item failure isolation, avoids PF-009)
659
+ // Proxy install artifacts — remove non-fatally (per-item failure isolation)
583
660
  // Inform the user if a proxy relay process is still running (never kill — informational only).
584
661
  try {
585
662
  const pidContent = await fs.readFile(path.join(devflowDir, 'proxy.pid'), 'utf-8');
@@ -594,7 +671,7 @@ export async function removeDevFlowInstallArtifacts(devflowDir, verbose) {
594
671
  }
595
672
  }
596
673
  catch { /* proxy.pid absent or unreadable — non-fatal */ }
597
- // All install artifacts removed non-fatally (avoids PF-009). Resolved against
674
+ // All install artifacts removed non-fatally. Resolved against
598
675
  // disk first, so a per-run staging basename is a real path by the time the
599
676
  // containment guard below sees it.
600
677
  for (const artifact of await resolveInstallArtifactPaths(devflowDir)) {
@@ -607,6 +684,8 @@ export async function removeDevFlowInstallArtifacts(devflowDir, verbose) {
607
684
  if (!isContainedIn(devflowDir, artifact.relPath)) {
608
685
  continue;
609
686
  }
687
+ if (!(await mayChange(fullPath)))
688
+ continue;
610
689
  try {
611
690
  await fs.rm(fullPath, { force: true, recursive: artifact.isDir === true });
612
691
  if (verbose)
@@ -654,7 +733,7 @@ export async function isDevFlowInstalled(claudeDir) {
654
733
  * scope directories, intersected with what actually exists on disk.
655
734
  *
656
735
  * Extracted from the dry-run loop body so the enumeration logic is
657
- * independently testable (avoids PF-018: tests must exercise the production
736
+ * independently testable (tests must exercise the production
658
737
  * path, not only the pure helper functions).
659
738
  *
660
739
  * Coverage matches removeAllDevFlow exactly:
@@ -664,7 +743,7 @@ export async function isDevFlowInstalled(claudeDir) {
664
743
  * - Prefixed (devflow:name): live registry ∪ LEGACY_SKILL_NAMES.
665
744
  * - Bare (name or devflow-name legacy): LEGACY_SKILL_NAMES ONLY.
666
745
  * ~/.claude/skills/ is shared; bare dirs for live-registry skill names
667
- * are by construction foreign to Devflow (avoids PF-012).
746
+ * are by construction foreign to Devflow.
668
747
  * 3. manifest.json (removed separately in removeDevFlowInstallArtifacts).
669
748
  * 4. Every artifact path from installArtifactPaths(devflowDir) — the single
670
749
  * source of truth shared with the real removal loop.
@@ -688,7 +767,7 @@ export async function enumerateDryRunExtras(claudeDir, devflowDir) {
688
767
  catch { /* absent */ }
689
768
  }
690
769
  // 2. Skills: enumerate ALL removal candidates that exist on disk.
691
- // Mirrors removeAllDevFlow's split-pass approach (avoids PF-012 + PF-018):
770
+ // Mirrors removeAllDevFlow's split-pass approach:
692
771
  // Prefixed: live registry ∪ LEGACY_SKILL_NAMES ∪ FEATURE_OWNED_SKILLS
693
772
  // Bare: LEGACY_SKILL_NAMES only — shared skills/ dir; live-registry bare
694
773
  // dirs are by construction foreign to Devflow.
@@ -812,6 +891,8 @@ export async function runSelectivePhaseForScope(opts) {
812
891
  const { claudeDir, devflowDir, selectedPlugins, verbose } = opts;
813
892
  const scope = opts.scope ?? 'user';
814
893
  const installedPlugins = opts.installedPlugins ?? DEVFLOW_PLUGINS;
894
+ // D-CLI-NO-SYMLINK: a legacy local install's removals and rewrites pass this first.
895
+ const mayChange = scopeChangeGuard(scope, { claudeDir, devflowDir });
815
896
  // Revert GPT agent frontmatter BEFORE removing agent files — strips GPT model
816
897
  // lines from installed agent frontmatter while the files are still present.
817
898
  // Non-fatal: tolerate missing agents dir or revert errors.
@@ -819,23 +900,25 @@ export async function runSelectivePhaseForScope(opts) {
819
900
  const agentsInstallDir = path.join(claudeDir, 'agents', 'devflow');
820
901
  try {
821
902
  await fs.access(agentsInstallDir);
822
- await revertExternalAgents({
823
- installDir: agentsInstallDir,
824
- devflowDir,
825
- onWarning: (msg) => { if (verbose)
826
- p.log.warn(msg); },
827
- });
903
+ if (await mayChange(agentsInstallDir)) {
904
+ await revertExternalAgents({
905
+ installDir: agentsInstallDir,
906
+ devflowDir,
907
+ onWarning: (msg) => { if (verbose)
908
+ p.log.warn(msg); },
909
+ });
910
+ }
828
911
  }
829
912
  catch { /* agents dir absent or revert failed — non-fatal */ }
830
913
  }
831
- await removeSelectedPlugins(claudeDir, selectedPlugins, verbose, installedPlugins);
914
+ await removeSelectedPlugins(claudeDir, selectedPlugins, verbose, installedPlugins, mayChange);
832
915
  // Clean up ambient hook if ambient plugin is being removed
833
916
  if (selectedPlugins.some(sp => sp.name === 'devflow-ambient')) {
834
917
  const settingsPath = path.join(claudeDir, 'settings.json');
835
918
  try {
836
919
  const settings = await fs.readFile(settingsPath, 'utf-8');
837
920
  const updated = await removeAmbientHook(settings, { purgeLegacyRule: scope === 'user' });
838
- if (updated !== settings) {
921
+ if (updated !== settings && await mayChange(settingsPath)) {
839
922
  await writeSettingsFileAtomic(settingsPath, updated);
840
923
  if (verbose) {
841
924
  p.log.success('Ambient mode hooks removed from settings.json');
@@ -866,6 +949,8 @@ export async function runSelectivePhaseForScope(opts) {
866
949
  */
867
950
  export async function runFullPhaseForScope(opts) {
868
951
  const { scope, claudeDir, devflowDir, devflowScriptsDir, verbose, keepDocs, isTTY } = opts;
952
+ // D-CLI-NO-SYMLINK: a legacy local install's removals and rewrites pass this first.
953
+ const mayChange = scopeChangeGuard(scope, { claudeDir, devflowDir });
869
954
  // Revert GPT agent frontmatter before removing agents — ensures no orphaned
870
955
  // GPT model lines remain if agents dir is preserved by a later partial flow.
871
956
  // Non-fatal: tolerate missing agents dir or revert errors.
@@ -873,24 +958,26 @@ export async function runFullPhaseForScope(opts) {
873
958
  const agentsInstallDir = path.join(claudeDir, 'agents', 'devflow');
874
959
  try {
875
960
  await fs.access(agentsInstallDir);
876
- await revertExternalAgents({
877
- installDir: agentsInstallDir,
878
- devflowDir,
879
- onWarning: (msg) => { if (verbose)
880
- p.log.warn(msg); },
881
- });
961
+ if (await mayChange(agentsInstallDir)) {
962
+ await revertExternalAgents({
963
+ installDir: agentsInstallDir,
964
+ devflowDir,
965
+ onWarning: (msg) => { if (verbose)
966
+ p.log.warn(msg); },
967
+ });
968
+ }
882
969
  }
883
970
  catch { /* agents dir absent or revert failed — non-fatal */ }
884
971
  }
885
972
  // removeAllDevFlow removes Claude Code assets (commands, agents, rules, skills)
886
973
  // and devflowDir/scripts/. Scope-aware cleanup handles the rest of devflowDir.
887
- await removeAllDevFlow(claudeDir, devflowScriptsDir, verbose);
974
+ await removeAllDevFlow(claudeDir, devflowScriptsDir, verbose, mayChange);
888
975
  if (scope === 'local') {
889
976
  // Local scope: devflowDir is gitRoot/.devflow/ which holds project data
890
977
  // (memory, learning, features, docs, config.json). Never remove those —
891
978
  // only remove install artifacts: scripts/ (done above) + manifest.json and
892
979
  // the other Devflow-generated artifacts (see removeDevFlowInstallArtifacts).
893
- await removeDevFlowInstallArtifacts(devflowDir, verbose);
980
+ await removeDevFlowInstallArtifacts(devflowDir, verbose, mayChange);
894
981
  p.log.info('Local project data (memory, learning, features, docs) preserved');
895
982
  }
896
983
  else {
@@ -923,7 +1010,7 @@ export async function runFullPhaseForScope(opts) {
923
1010
  });
924
1011
  if (p.isCancel(confirmFullCleanup)) {
925
1012
  // removeAllDevFlow already ran — clean up the manifest to leave a consistent
926
- // state. avoids PF-014: process.exit() here would skip removeDevFlowInstallArtifacts,
1013
+ // state. process.exit() here would skip removeDevFlowInstallArtifacts,
927
1014
  // leaving a stale manifest.json that points to assets no longer on disk.
928
1015
  await removeDevFlowInstallArtifacts(devflowDir, verbose);
929
1016
  p.log.info(`${devflowDir} preserved (full removal cancelled; removing scripts and install artifacts only)`);
@@ -945,7 +1032,7 @@ export async function runFullPhaseForScope(opts) {
945
1032
  *
946
1033
  * D-UNINSTALL-CARVE-OUT: DEVFLOW_TRACKED_PATHS are never removed. `--keep-docs`,
947
1034
  * a non-interactive run, a decline and a cancel all leave the directory untouched,
948
- * and a cancel continues the uninstall rather than exiting (avoids PF-014). The
1035
+ * and a cancel continues the uninstall rather than exiting. The
949
1036
  * directory itself is removed only when nothing tracked was in it.
950
1037
  */
951
1038
  async function runProjectDataStep(plan, gates) {
@@ -1109,7 +1196,12 @@ export async function runCleanupPhase(opts) {
1109
1196
  applyProxyTeardownToSettings(parsedSettings, opts.managedProxyPorts?.get(scope));
1110
1197
  settingsContent = JSON.stringify(parsedSettings, null, 2) + '\n';
1111
1198
  }
1112
- if (settingsContent !== originalContent) {
1199
+ // D-CLI-NO-SYMLINK: a legacy local install's settings.json is rewritten only
1200
+ // where no symbolic link leads it out of `<gitRoot>/.claude`.
1201
+ const mayChange = scope === 'local'
1202
+ ? legacyLocalChangeGuard(paths, 'settings.json not edited')
1203
+ : changeAnything;
1204
+ if (settingsContent !== originalContent && await mayChange(settingsPath)) {
1113
1205
  await writeSettingsFileAtomic(settingsPath, settingsContent);
1114
1206
  if (verbose) {
1115
1207
  p.log.success(`Devflow hooks removed from settings.json (${scope})`);
@@ -1391,8 +1483,11 @@ export const uninstallCommand = new Command('uninstall')
1391
1483
  });
1392
1484
  /**
1393
1485
  * Remove all Devflow assets (full uninstall).
1486
+ *
1487
+ * Each removal passes `mayChange` first, which skips one a symbolic link would
1488
+ * redirect in a legacy local install (D-CLI-NO-SYMLINK, legacyLocalChangeGuard).
1394
1489
  */
1395
- export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose) {
1490
+ export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose, mayChange = changeAnything) {
1396
1491
  const devflowDirectories = [
1397
1492
  { path: path.join(claudeDir, 'commands', 'devflow'), name: 'commands' },
1398
1493
  { path: path.join(claudeDir, 'agents', 'devflow'), name: 'agents' },
@@ -1400,6 +1495,8 @@ export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose) {
1400
1495
  { path: devflowScriptsDir, name: 'scripts' }
1401
1496
  ];
1402
1497
  for (const dir of devflowDirectories) {
1498
+ if (!(await mayChange(dir.path)))
1499
+ continue;
1403
1500
  try {
1404
1501
  await fs.rm(dir.path, { recursive: true, force: true });
1405
1502
  if (verbose) {
@@ -1412,7 +1509,7 @@ export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose) {
1412
1509
  }
1413
1510
  // Remove Devflow skill directories.
1414
1511
  // ~/.claude/skills/ is shared with other tools; the two passes use different
1415
- // name sets to avoid deleting foreign dirs (avoids PF-012):
1512
+ // name sets to avoid deleting foreign dirs:
1416
1513
  //
1417
1514
  // Prefixed pass (devflow:name): live registry ∪ LEGACY_SKILL_NAMES ∪ FEATURE_OWNED_SKILLS.
1418
1515
  // prefixSkillName() is idempotent for already-prefixed legacy entries.
@@ -1421,7 +1518,7 @@ export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose) {
1421
1518
  // Bare pass (name or devflow-name legacy): LEGACY_SKILL_NAMES ONLY.
1422
1519
  // A bare dir whose name matches a live-registry skill is by construction
1423
1520
  // foreign to Devflow (the devflow: namespace shipped in dcecda3, 2026-03-30).
1424
- // A bare 'compliance/' dir is also foreign (not in LEGACY_SKILL_NAMES) — PF-012.
1521
+ // A bare 'compliance/' dir is also foreign (not in LEGACY_SKILL_NAMES).
1425
1522
  const prefixedSkillNames = new Set([...getAllSkillNames(), ...LEGACY_SKILL_NAMES, ...FEATURE_OWNED_SKILLS]);
1426
1523
  const skillsDir = path.join(claudeDir, 'skills');
1427
1524
  let skillsRemoved = 0;
@@ -1430,6 +1527,8 @@ export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose) {
1430
1527
  const prefixedPath = path.join(skillsDir, prefixSkillName(skillName));
1431
1528
  try {
1432
1529
  await fs.stat(prefixedPath);
1530
+ if (!(await mayChange(prefixedPath)))
1531
+ continue;
1433
1532
  await fs.rm(prefixedPath, { recursive: true, force: true });
1434
1533
  skillsRemoved++;
1435
1534
  }
@@ -1440,6 +1539,8 @@ export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose) {
1440
1539
  const barePath = path.join(skillsDir, skillName);
1441
1540
  try {
1442
1541
  await fs.stat(barePath);
1542
+ if (!(await mayChange(barePath)))
1543
+ continue;
1443
1544
  await fs.rm(barePath, { recursive: true, force: true });
1444
1545
  skillsRemoved++;
1445
1546
  }
@@ -1449,8 +1550,12 @@ export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose) {
1449
1550
  p.log.success(`Removed ${skillsRemoved} Devflow skill directories`);
1450
1551
  }
1451
1552
  // Also remove old nested skills structure if it exists
1553
+ const nestedSkillsDir = path.join(claudeDir, 'skills', 'devflow');
1452
1554
  try {
1453
- await fs.rm(path.join(claudeDir, 'skills', 'devflow'), { recursive: true, force: true });
1555
+ await fs.stat(nestedSkillsDir);
1556
+ if (await mayChange(nestedSkillsDir)) {
1557
+ await fs.rm(nestedSkillsDir, { recursive: true, force: true });
1558
+ }
1454
1559
  }
1455
1560
  catch {
1456
1561
  // Old structure doesn't exist
@@ -1459,7 +1564,7 @@ export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose) {
1459
1564
  // registry (retired, renamed, or deleted). The loop above walks the static
1460
1565
  // registry + LEGACY list; this sweep walks the actual directory, so orphaned
1461
1566
  // dirs from older versions are also cleaned up. (F9)
1462
- await sweepDevflowNamespaces(claudeDir, verbose);
1567
+ await sweepDevflowNamespaces(claudeDir, verbose, mayChange);
1463
1568
  }
1464
1569
  /**
1465
1570
  * Registry-diff sweep of all Devflow-owned namespaces in `claudeDir`:
@@ -1472,23 +1577,29 @@ export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose) {
1472
1577
  * cleaned up promptly rather than accumulating on disk.
1473
1578
  *
1474
1579
  * knownNames spans ALL plugins for each asset type so assets belonging to
1475
- * plugins NOT being uninstalled are never swept (avoids PF-012).
1580
+ * plugins NOT being uninstalled are never swept.
1476
1581
  *
1477
1582
  * Removals are announced only under `verbose`, but removal FAILURES always warn:
1478
1583
  * a swept-but-not-actually-removed agent or command keeps loading in Claude Code,
1479
1584
  * and the user has no other signal that it is still there.
1585
+ *
1586
+ * A folder `mayChange` refuses is not swept at all (D-CLI-NO-SYMLINK,
1587
+ * legacyLocalChangeGuard). An entry in a swept folder that is itself a link is
1588
+ * only unlinked: `fs.rm` never follows the link it is given.
1480
1589
  */
1481
- export async function sweepDevflowNamespaces(claudeDir, verbose) {
1590
+ export async function sweepDevflowNamespaces(claudeDir, verbose, mayChange = changeAnything) {
1482
1591
  const agentsDir = path.join(claudeDir, 'agents', 'devflow');
1483
1592
  const commandsDir = path.join(claudeDir, 'commands', 'devflow');
1484
1593
  const skillsDir = path.join(claudeDir, 'skills');
1485
- const agentsSweep = await sweepOrphanedAssets(agentsDir, new Set(getAllAgentNames()), mdEntryName);
1486
- const commandsSweep = await sweepOrphanedAssets(commandsDir, new Set(getAllCommandNames()), mdEntryName);
1594
+ const noSweep = { scanned: 0, removed: [], failed: [] };
1595
+ const sweep = async (...args) => (await mayChange(args[0])) ? sweepOrphanedAssets(...args) : noSweep;
1596
+ const agentsSweep = await sweep(agentsDir, new Set(getAllAgentNames()), mdEntryName);
1597
+ const commandsSweep = await sweep(commandsDir, new Set(getAllCommandNames()), mdEntryName);
1487
1598
  // FEATURE_OWNED_SKILLS (compliance) unioned into knownNames so a selective
1488
1599
  // uninstall of another plugin does not sweep devflow:compliance — nothing
1489
1600
  // converges after selective uninstall (only installViaFileCopy + convergeComplianceArtifacts
1490
1601
  // on full/partial init re-materialize it). D-FO-1 applies.
1491
- const skillsSweep = await sweepOrphanedAssets(skillsDir, new Set([...getAllSkillNames(), ...FEATURE_OWNED_SKILLS]), (entry) => entry.startsWith(SKILL_NAMESPACE) ? unprefixSkillName(entry) : null);
1602
+ const skillsSweep = await sweep(skillsDir, new Set([...getAllSkillNames(), ...FEATURE_OWNED_SKILLS]), (entry) => entry.startsWith(SKILL_NAMESPACE) ? unprefixSkillName(entry) : null);
1492
1603
  const byKind = [
1493
1604
  { kind: 'agent', sweep: agentsSweep },
1494
1605
  { kind: 'command', sweep: commandsSweep },
@@ -1511,14 +1622,18 @@ export async function sweepDevflowNamespaces(claudeDir, verbose) {
1511
1622
  * sweep the install directory for any orphaned assets whose names left the
1512
1623
  * registry (retired agents/commands survive indefinitely without the sweep).
1513
1624
  * For skills: only remove skills that are NOT used by any remaining plugin.
1625
+ * Each removal passes `mayChange` first, which skips one a symbolic link would
1626
+ * redirect in a legacy local install (D-CLI-NO-SYMLINK, legacyLocalChangeGuard).
1514
1627
  */
1515
- export async function removeSelectedPlugins(claudeDir, plugins, verbose, installedPlugins = DEVFLOW_PLUGINS) {
1628
+ export async function removeSelectedPlugins(claudeDir, plugins, verbose, installedPlugins = DEVFLOW_PLUGINS, mayChange = changeAnything) {
1516
1629
  const { skills, agents, commands, rules } = computeAssetsToRemove(plugins, installedPlugins);
1517
1630
  const commandsDir = path.join(claudeDir, 'commands', 'devflow');
1518
1631
  for (const cmd of commands) {
1519
- const cmdFileName = mdFileName(cmd.replace(/^\//, ''));
1632
+ const cmdPath = path.join(commandsDir, mdFileName(cmd.replace(/^\//, '')));
1633
+ if (!(await mayChange(cmdPath)))
1634
+ continue;
1520
1635
  try {
1521
- await fs.rm(path.join(commandsDir, cmdFileName), { force: true });
1636
+ await fs.rm(cmdPath, { force: true });
1522
1637
  if (verbose) {
1523
1638
  p.log.success(`Removed command ${cmd}`);
1524
1639
  }
@@ -1529,8 +1644,11 @@ export async function removeSelectedPlugins(claudeDir, plugins, verbose, install
1529
1644
  }
1530
1645
  const agentsDir = path.join(claudeDir, 'agents', 'devflow');
1531
1646
  for (const agent of agents) {
1647
+ const agentPath = path.join(agentsDir, mdFileName(agent));
1648
+ if (!(await mayChange(agentPath)))
1649
+ continue;
1532
1650
  try {
1533
- await fs.rm(path.join(agentsDir, mdFileName(agent)), { force: true });
1651
+ await fs.rm(agentPath, { force: true });
1534
1652
  if (verbose) {
1535
1653
  p.log.success(`Removed agent ${agent}`);
1536
1654
  }
@@ -1546,9 +1664,12 @@ export async function removeSelectedPlugins(claudeDir, plugins, verbose, install
1546
1664
  // exclusively by the frozen LEGACY_SKILL_NAMES pass in removeAllDevFlow and
1547
1665
  // in init.ts; live-registry skills never had bare installs (the devflow:
1548
1666
  // namespace shipped in dcecda3, 2026-03-30), so a bare dir for a current
1549
- // registry name is by construction foreign (avoids PF-012).
1667
+ // registry name is by construction foreign.
1668
+ const skillPath = path.join(skillsDir, prefixSkillName(skill));
1669
+ if (!(await mayChange(skillPath)))
1670
+ continue;
1550
1671
  try {
1551
- await fs.rm(path.join(skillsDir, prefixSkillName(skill)), { recursive: true, force: true });
1672
+ await fs.rm(skillPath, { recursive: true, force: true });
1552
1673
  }
1553
1674
  catch { /* Skill might not exist */ }
1554
1675
  if (verbose) {
@@ -1557,8 +1678,11 @@ export async function removeSelectedPlugins(claudeDir, plugins, verbose, install
1557
1678
  }
1558
1679
  const rulesDir = path.join(claudeDir, 'rules', 'devflow');
1559
1680
  for (const rule of rules) {
1681
+ const rulePath = path.join(rulesDir, mdFileName(rule));
1682
+ if (!(await mayChange(rulePath)))
1683
+ continue;
1560
1684
  try {
1561
- await fs.rm(path.join(rulesDir, mdFileName(rule)), { force: true });
1685
+ await fs.rm(rulePath, { force: true });
1562
1686
  if (verbose) {
1563
1687
  p.log.success(`Removed rule ${rule}`);
1564
1688
  }
@@ -1568,7 +1692,7 @@ export async function removeSelectedPlugins(claudeDir, plugins, verbose, install
1568
1692
  // Registry-diff sweep: agents, commands, AND skills — named step (A6).
1569
1693
  // Removes any orphaned files whose names left the registry (retired, renamed,
1570
1694
  // or deleted from all plugins). Spans ALL plugins so assets belonging to
1571
- // non-selected plugins are never swept (avoids PF-012).
1572
- await sweepDevflowNamespaces(claudeDir, verbose);
1695
+ // non-selected plugins are never swept.
1696
+ await sweepDevflowNamespaces(claudeDir, verbose, mayChange);
1573
1697
  }
1574
1698
  //# sourceMappingURL=uninstall.js.map
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Pure TUI frame renderer for the devflow flags view.
3
3
  *
4
- * applies ADR-013: CLI-layer view module; zero fs/tty imports.
5
- * avoids PF-014: pure function, no process.exit(), no I/O.
4
+ * CLI-layer view module; zero fs/tty imports.
5
+ * Pure function, no process.exit(), no I/O.
6
6
  *
7
7
  * Layout (FIXED_ROWS = 10, viewport = state.viewportHeight — single owner):
8
8
  * 1 Title " Devflow Flags"
@@ -59,14 +59,14 @@ export function computeViewportHeight(termRows) {
59
59
  * non-boolean at devflow default → plain string
60
60
  * non-boolean deviating from devflow default → bold string
61
61
  *
62
- * Colour vocabulary (one colour, one semantic — applies ADR-016's amendment lesson):
62
+ * Colour vocabulary (one colour, one semantic):
63
63
  * cyan = focus indicator (chevron wrapper ‹ › on the cursor row only)
64
64
  * yellow = dirty indicator (unconditional ●) and boolean 'off'
65
65
  * green = boolean 'on'
66
66
  * bold = non-boolean value deviating from devflow default
67
67
  *
68
68
  * disk-sourced values are routed through sanitizeCell to prevent TAB/LF
69
- * layout breaks inside the fixed-width TUI cell (avoids PF-023).
69
+ * layout breaks inside the fixed-width TUI cell.
70
70
  */
71
71
  function formatValue(row) {
72
72
  const v = row.configuredValue;
@@ -151,7 +151,7 @@ function renderRow(row, isCursor, isEditing, editBuffer, editCaret, labelW, valu
151
151
  // so an inner RESET (e.g. from green('on')) does not kill the outer cyan.
152
152
  // cyan('‹ ') + <styled-or-plain content> + cyan(' ›')
153
153
  // rather than cyan(`‹ ${content} ›`), which terminates the outer cyan at the
154
- // inner RESET, leaving the closing chevron unstyled (applies ADR-016 amendment lesson).
154
+ // inner RESET, leaving the closing chevron unstyled.
155
155
  const chevronBudget = valueW - 4;
156
156
  let valueCell;
157
157
  if (isCursor && isEditing) {
@@ -1,20 +1,20 @@
1
1
  /**
2
2
  * Pure keypress reducer for the devflow flags TUI.
3
3
  *
4
- * applies ADR-013: CLI-layer view module; consumes src/core/ imports only.
5
- * applies ADR-016: one syntax, one semantic — value vocabulary.
6
- * avoids PF-014: pure functions only — no process.exit(), no I/O.
7
- * avoids PF-017: generic shell in tui/terminal.ts; this module is pure logic.
4
+ * CLI-layer view module; consumes src/core/ imports only.
5
+ * One syntax, one semantic — value vocabulary.
6
+ * Pure functions only — no process.exit(), no I/O.
7
+ * Generic shell in tui/terminal.ts; this module is pure logic.
8
8
  *
9
9
  * viewMode GLUE RULE: view-mode's neutralValue ('default') maps to null in the TUI.
10
10
  * `buildFlagRows` maps record value 'default' → null via `recordToTui` (core/flags.ts);
11
11
  * `collectFlagRecord` maps null → 'default' via `tuiToRecord` (core/flags.ts).
12
12
  * Number 0 is ACTIVE — null ≠ 0. Both functions live next to neutralValueOf, their
13
- * definition dependency (PF-017 one-shared-definition corollary).
13
+ * definition dependency, so there is one shared definition rather than per-consumer copies.
14
14
  *
15
15
  * Strict number parsing: leading/trailing whitespace and leading zeros are
16
16
  * invalid ('007' → error, ' 8' → error). This rejects pathological inputs
17
- * before they reach coerceFlagValue (applies PF-023).
17
+ * before they reach coerceFlagValue.
18
18
  *
19
19
  * Buffer hard cap: BUFFER_MAX_LEN = 64 chars (paste-flood guard).
20
20
  *
@@ -198,8 +198,8 @@ function enterEdit(state) {
198
198
  * Contract:
199
199
  * - Empty buffer + allowUnset → commit null (unset)
200
200
  * - Empty buffer + !allowUnset → error "Value is required"
201
- * - For number/string flags: delegate to parseFlagValueInput (applies PF-023 —
202
- * strict grammar enforced at the core sink, not per-caller). Error messages
201
+ * - For number/string flags: delegate to parseFlagValueInput (strict grammar
202
+ * enforced at the core sink, not per-caller). Error messages
203
203
  * distinguish format failures (padded/hex/leading-zeros) from bounds failures.
204
204
  * - parseFlagValueInput returns null on invalid input → stay editing + error
205
205
  */
@@ -230,7 +230,7 @@ function commitEdit(state) {
230
230
  };
231
231
  }
232
232
  }
233
- // Number flag: parseFlagValueInput enforces strict decimal grammar (avoids PF-023).
233
+ // Number flag: parseFlagValueInput enforces strict decimal grammar.
234
234
  // Provide specific error messages to distinguish format from bounds failures.
235
235
  if (flagDef.kind === 'number') {
236
236
  if (buf !== buf.trim()) {
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Thin adapter — devflow flags TUI shell over the generic runTui driver.
3
3
  *
4
- * applies ADR-013: impure I/O shell in CLI layer; pure logic lives in state.ts/render.ts.
5
- * avoids PF-014: cleanup wired via Promise resolve — never process.exit() inside
6
- * a finally-guarded scope.
7
- * avoids PF-017: thin adapter over the generic shell (src/cli/tui/terminal.ts).
4
+ * Impure I/O shell in CLI layer; pure logic lives in state.ts/render.ts.
5
+ * Cleanup wired via Promise resolve — never process.exit() inside
6
+ * a finally-guarded scope (it would skip the finally).
7
+ * Thin adapter over the generic shell (src/cli/tui/terminal.ts).
8
8
  *
9
9
  * Public API:
10
10
  * - runFlagsTui(initialRows, io?) → Promise<FlagsTuiResult>
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Shared TUI cell helpers — shared by agents-view and flags-view.
3
3
  *
4
- * Applies PF-017: generified into a shared module rather than copy-adapted per consumer.
4
+ * Generified into a shared module rather than copy-adapted per consumer.
5
5
  * Pure functions, no I/O.
6
6
  */
7
7
  import { stripAnsi, truncate } from '../../core/ansi.js';