devflow-kit 2.4.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/CHANGELOG.md +229 -0
  2. package/README.md +111 -18
  3. package/dist/agents/git.md +822 -0
  4. package/dist/cli/commands/agents.js +6 -1
  5. package/dist/cli/commands/ambient.js +160 -145
  6. package/dist/cli/commands/attribution-prompts.js +1 -1
  7. package/dist/cli/commands/capture.js +29 -55
  8. package/dist/cli/commands/compliance-prompts.js +1 -1
  9. package/dist/cli/commands/compliance.js +48 -55
  10. package/dist/cli/commands/context.js +17 -32
  11. package/dist/cli/commands/debug.js +65 -26
  12. package/dist/cli/commands/flags.js +3 -3
  13. package/dist/cli/commands/hud.js +34 -10
  14. package/dist/cli/commands/init-seed.js +61 -27
  15. package/dist/cli/commands/init.js +649 -240
  16. package/dist/cli/commands/install-report.js +200 -0
  17. package/dist/cli/commands/knowledge/index.js +2 -2
  18. package/dist/cli/commands/knowledge/toggle.js +35 -37
  19. package/dist/cli/commands/learning.js +79 -57
  20. package/dist/cli/commands/legacy-hooks.js +11 -14
  21. package/dist/cli/commands/memory.js +134 -135
  22. package/dist/cli/commands/prompt-io.js +4 -4
  23. package/dist/cli/commands/proxy.js +23 -41
  24. package/dist/cli/commands/security.js +81 -29
  25. package/dist/cli/commands/skills.js +71 -7
  26. package/dist/cli/commands/tracker-prompts.js +145 -0
  27. package/dist/cli/commands/tracker.js +277 -0
  28. package/dist/cli/commands/uninstall.js +520 -169
  29. package/dist/cli.js +2 -0
  30. package/dist/commands/bug-analysis.md +58 -14
  31. package/dist/commands/code-review.md +110 -32
  32. package/dist/commands/debug.md +55 -11
  33. package/dist/commands/dynamic-build.md +344 -73
  34. package/dist/commands/dynamic-plan.md +77 -27
  35. package/dist/commands/dynamic-profile.md +25 -11
  36. package/dist/commands/dynamic-tickets.md +76 -15
  37. package/dist/commands/explore.md +37 -7
  38. package/dist/commands/implement.md +314 -62
  39. package/dist/commands/plan.md +146 -32
  40. package/dist/commands/release.md +64 -17
  41. package/dist/commands/research.md +34 -8
  42. package/dist/commands/resolve.md +196 -68
  43. package/dist/commands/self-review.md +45 -9
  44. package/dist/core/agent-models.js +55 -12
  45. package/dist/core/assets.js +58 -2
  46. package/dist/core/compliance-compose.js +27 -27
  47. package/dist/core/evidence-policy.js +363 -0
  48. package/dist/core/feature-config.js +200 -65
  49. package/dist/core/feature-switch.js +112 -0
  50. package/dist/core/flags.js +34 -6
  51. package/dist/core/fs-atomic.js +27 -0
  52. package/dist/core/hook-log-dirs.js +104 -0
  53. package/dist/core/learning-tuning-config.js +5 -3
  54. package/dist/core/ledger-root.js +102 -0
  55. package/dist/core/manifest.js +38 -10
  56. package/dist/core/mds-variants.js +798 -0
  57. package/dist/core/migrations.js +49 -23
  58. package/dist/core/model-discovery.js +12 -1
  59. package/dist/core/plugins.js +361 -12
  60. package/dist/core/project-paths.js +1 -18
  61. package/dist/core/proxy-log.js +8 -6
  62. package/dist/core/proxy-state.js +11 -8
  63. package/dist/core/reference-sweep.js +136 -0
  64. package/dist/core/same-location.js +25 -0
  65. package/dist/core/tracker.js +494 -0
  66. package/dist/hud/components/config-counts.js +15 -4
  67. package/dist/hud/components/learning-counts.js +14 -0
  68. package/dist/hud/config.js +2 -1
  69. package/dist/hud/cost-history.js +2 -4
  70. package/dist/hud/git.js +52 -7
  71. package/dist/hud/index.js +7 -9
  72. package/dist/skills/git/references/decision-markers.md +19 -0
  73. package/dist/skills/git/references/learn-conventions.md +56 -0
  74. package/dist/skills/git/references/pr/check-ci-status.md +14 -0
  75. package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
  76. package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
  77. package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
  78. package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
  79. package/dist/skills/git/references/pr/post-review-summary.md +42 -0
  80. package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
  81. package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
  82. package/dist/skills/git/references/pr/validate-branch.md +18 -0
  83. package/dist/skills/git/references/publication-gate.md +13 -0
  84. package/dist/skills/git/references/tracker/_mcp.md +153 -0
  85. package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
  86. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
  87. package/dist/skills/git/references/tracker/github/create-release.md +11 -0
  88. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
  89. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
  90. package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
  91. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
  92. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
  93. package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
  94. package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
  95. package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
  96. package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
  97. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
  98. package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
  99. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
  100. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
  101. package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
  102. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
  103. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
  104. package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
  105. package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
  106. package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
  107. package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
  108. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
  109. package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
  110. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
  111. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
  112. package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
  113. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
  114. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
  115. package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
  116. package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
  117. package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
  118. package/dist/skills/git/references/trust-rule.md +7 -0
  119. package/dist/targets/claude-code/claude-paths.js +59 -57
  120. package/dist/targets/claude-code/compliance-install.js +49 -65
  121. package/dist/targets/claude-code/hooks.js +108 -3
  122. package/dist/targets/claude-code/installer.js +1187 -32
  123. package/dist/targets/claude-code/legacy.js +5 -0
  124. package/dist/targets/claude-code/post-install.js +366 -151
  125. package/dist/targets/claude-code/tracker-install.js +134 -0
  126. package/package.json +8 -6
  127. package/src/assets/agents/code.md +45 -6
  128. package/src/assets/agents/design.md +2 -1
  129. package/src/assets/agents/git.mds +825 -0
  130. package/src/assets/agents/knowledge.md +3 -3
  131. package/src/assets/agents/learning.md +11 -0
  132. package/src/assets/agents/review.md +3 -1
  133. package/src/assets/agents/synthesize.md +1 -1
  134. package/src/assets/agents/test.md +16 -5
  135. package/src/assets/agents/tracker.md +474 -0
  136. package/src/assets/agents/validate.md +7 -5
  137. package/src/assets/commands/_partials/_compliance.mds +19 -1
  138. package/src/assets/commands/_partials/_decisions.mds +15 -3
  139. package/src/assets/commands/_partials/_docs_root.mds +35 -0
  140. package/src/assets/commands/_partials/_engine.mds +13 -11
  141. package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
  142. package/src/assets/commands/_partials/_factory.mds +1 -1
  143. package/src/assets/commands/_partials/_knowledge.mds +27 -9
  144. package/src/assets/commands/_partials/_plan_contract.mds +22 -7
  145. package/src/assets/commands/_partials/_preamble.mds +2 -2
  146. package/src/assets/commands/_partials/_publication.mds +8 -2
  147. package/src/assets/commands/_partials/_settings.mds +28 -0
  148. package/src/assets/commands/_partials/_ticket_template.mds +3 -2
  149. package/src/assets/commands/_partials/_tracker.mds +18 -0
  150. package/src/assets/commands/_partials/_wave.mds +16 -10
  151. package/src/assets/commands/bug-analysis.mds +31 -19
  152. package/src/assets/commands/code-review.mds +67 -41
  153. package/src/assets/commands/debug.mds +13 -7
  154. package/src/assets/commands/dynamic-build.mds +274 -66
  155. package/src/assets/commands/dynamic-plan.mds +50 -23
  156. package/src/assets/commands/dynamic-profile.mds +24 -11
  157. package/src/assets/commands/dynamic-tickets.mds +63 -16
  158. package/src/assets/commands/explore.mds +4 -5
  159. package/src/assets/commands/implement.mds +234 -67
  160. package/src/assets/commands/plan.mds +91 -33
  161. package/src/assets/commands/release.md +64 -17
  162. package/src/assets/commands/research.mds +11 -9
  163. package/src/assets/commands/resolve.mds +150 -78
  164. package/src/assets/commands/self-review.mds +24 -25
  165. package/src/assets/mds/git/_pr.mds +331 -0
  166. package/src/assets/mds/git/_references.mds +135 -0
  167. package/src/assets/mds/tracker/_common.mds +156 -0
  168. package/src/assets/mds/tracker/_github.mds +472 -0
  169. package/src/assets/mds/tracker/_jira.mds +407 -0
  170. package/src/assets/mds/tracker/_linear.mds +449 -0
  171. package/src/assets/mds/tracker/_mcp.mds +305 -0
  172. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
  173. package/src/assets/scripts/hooks/background-memory-update +40 -19
  174. package/src/assets/scripts/hooks/capture-prompt +18 -8
  175. package/src/assets/scripts/hooks/capture-question +18 -8
  176. package/src/assets/scripts/hooks/capture-turn +27 -13
  177. package/src/assets/scripts/hooks/debug-trace +11 -6
  178. package/src/assets/scripts/hooks/ensure-devflow-init +33 -6
  179. package/src/assets/scripts/hooks/ensure-proxy +9 -8
  180. package/src/assets/scripts/hooks/ensure-root-gitignore +236 -60
  181. package/src/assets/scripts/hooks/git-marker +48 -0
  182. package/src/assets/scripts/hooks/hook-log-init +3 -1
  183. package/src/assets/scripts/hooks/json-helper.cjs +228 -5
  184. package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -20
  185. package/src/assets/scripts/hooks/log-paths +80 -0
  186. package/src/assets/scripts/hooks/memory-worker +22 -13
  187. package/src/assets/scripts/hooks/pre-compact-memory +44 -15
  188. package/src/assets/scripts/hooks/preamble +1 -4
  189. package/src/assets/scripts/hooks/queue-append +146 -28
  190. package/src/assets/scripts/hooks/resolve-project-root +101 -7
  191. package/src/assets/scripts/hooks/session-start-context +534 -20
  192. package/src/assets/scripts/hooks/session-start-memory +38 -15
  193. package/src/assets/scripts/lib/project-config.cjs +633 -0
  194. package/src/assets/scripts/pr-evidence.cjs +1961 -0
  195. package/src/assets/scripts/redact-secrets.cjs +490 -62
  196. package/src/assets/scripts/release-trace.cjs +1143 -0
  197. package/src/assets/scripts/resolve-evidence-policy.cjs +1145 -0
  198. package/src/assets/scripts/resolve-settings.cjs +1054 -0
  199. package/src/assets/scripts/verify-evidence.cjs +1822 -0
  200. package/src/assets/skills/compliance/SKILL.md +4 -2
  201. package/src/assets/skills/docs-framework/SKILL.md +11 -10
  202. package/src/assets/skills/docs-framework/references/patterns.md +10 -17
  203. package/src/assets/skills/gap-analysis/SKILL.md +2 -2
  204. package/src/assets/skills/git/SKILL.md +8 -78
  205. package/src/assets/skills/git/references/github-api.md +179 -141
  206. package/src/assets/skills/git/references/patterns.md +11 -6
  207. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  208. package/src/assets/skills/review-methodology/references/patterns.md +6 -61
  209. package/src/assets/skills/review-methodology/references/violations.md +14 -22
  210. package/src/assets/skills/worktree-support/SKILL.md +1 -1
  211. package/src/assets/skills/worktree-support/references/roots.md +29 -0
  212. package/src/targets/claude-code/templates/managed-settings.json +25 -9
  213. package/src/assets/agents/git.md +0 -938
@@ -0,0 +1,136 @@
1
+ import { promises as fs } from 'fs';
2
+ import * as path from 'path';
3
+ /**
4
+ * @file reference-sweep.ts
5
+ *
6
+ * Path-keyed registry-diff sweep for the generated `devflow:git` reference tree.
7
+ *
8
+ * Sibling of {@link sweepOrphanedAssets} in orphan-sweep.ts and deliberately the same
9
+ * {@link SweepResult} shape — `scanned` is the non-vacuity counter, removals and
10
+ * per-item failures are reported rather than thrown (avoids PF-009).
11
+ *
12
+ * What is genuinely new is the KEY. `sweepOrphanedAssets` keys a flat directory by
13
+ * registry name through `mdEntryName`, which cannot express `tracker/{provider}/{op}.md`:
14
+ * two providers may legitimately both carry a `comment.md`, so the registry name has to
15
+ * be the relative PATH, and the walk has to descend.
16
+ *
17
+ * Never writes, only removes (avoids PF-011).
18
+ */
19
+ /**
20
+ * Descent bound for every walk over the generated reference tree — this sweep and the
21
+ * build's own prune, which imports it (`pruneOrphans` in scripts/build-mds.ts). One
22
+ * tree, one bound: two walkers each carrying their own literal is how they come to
23
+ * disagree on both the number of levels and what happens at the last one.
24
+ *
25
+ * `depth` counts the walked root as 0 and the bound is the deepest directory a walk may
26
+ * descend INTO, so `depth > MAX_REFERENCE_SWEEP_DEPTH` is the breach — the comparison
27
+ * the build's walks already use. The installed tree is two levels deep
28
+ * (`tracker/{provider}/{op}.md`), so 8 is generous. The bound exists because an
29
+ * unbounded recursion over a directory neither walker owns would spin on a symlink loop
30
+ * rather than fail — every loop has an explicit upper bound.
31
+ *
32
+ * The two walkers answer a breach differently by design, and both answer out loud: the
33
+ * build throws (a generated tree that deep is a build bug, and dist/ is still the
34
+ * build's own to fail), while this sweep records the unvisited subtree in `failed`
35
+ * (avoids PF-009 — an install is not abandoned over one subtree). Neither returns
36
+ * quietly: a subtree the walk never entered must not be summarised as converged.
37
+ */
38
+ export const MAX_REFERENCE_SWEEP_DEPTH = 8;
39
+ /**
40
+ * Remove everything under `root` that the manifest does not name.
41
+ *
42
+ * @param root - Directory to converge (the installed `references/tracker/` tree).
43
+ * @param knownRelPaths - POSIX paths relative to `root` that must survive. Derived from
44
+ * the build's own module registries by the caller — never hand-listed.
45
+ *
46
+ * @returns A {@link SweepResult} whose `removed` entries are POSIX relative paths. A
47
+ * directory into which no manifest path descends is removed WHOLE and reported by its
48
+ * own relative path — leaving it empty would be a convergence that stops one step
49
+ * short, and an empty provider directory is indistinguishable from a provider whose
50
+ * references failed to install. A subtree left unswept because it breached
51
+ * {@link MAX_REFERENCE_SWEEP_DEPTH} is reported in `failed` under its own relative
52
+ * path, for the same reason: everything this sweep did not converge is named.
53
+ *
54
+ * A missing or unreadable `root` is a no-op, not an error: the overlay creates the tree
55
+ * it converges, so an absent one simply means there is nothing to prune yet.
56
+ */
57
+ export async function sweepOrphanedReferences(root, knownRelPaths) {
58
+ const acc = { scanned: 0, removed: [], failed: [] };
59
+ await sweepDirectory(root, '', 0, knownRelPaths, directoryPrefixes(knownRelPaths), acc);
60
+ return { scanned: acc.scanned, removed: acc.removed, failed: acc.failed };
61
+ }
62
+ async function sweepDirectory(dir, prefix, depth, known, knownDirPrefixes, acc) {
63
+ if (depth > MAX_REFERENCE_SWEEP_DEPTH) {
64
+ // A breached bound means this subtree is never visited, so any orphan inside it
65
+ // survives. Report it through the same channel as a failed removal: a silent return
66
+ // would let the install summary claim convergence over ground never covered.
67
+ acc.failed.push({
68
+ name: prefix || dir,
69
+ error: new Error(`${prefix || dir}: sweep descent exceeds the bound of ${MAX_REFERENCE_SWEEP_DEPTH} ` +
70
+ `levels — this subtree was not swept and any orphans under it survive.`),
71
+ });
72
+ return;
73
+ }
74
+ let entries;
75
+ try {
76
+ entries = await fs.readdir(dir, { withFileTypes: true });
77
+ }
78
+ catch {
79
+ return; /* absent or unreadable — not an error (avoids PF-009) */
80
+ }
81
+ for (const entry of entries) {
82
+ const relPath = prefix === '' ? entry.name : `${prefix}/${entry.name}`;
83
+ const fullPath = path.join(dir, entry.name);
84
+ // A real subdirectory is either an ancestor of something the manifest names — in
85
+ // which case descend — or dead weight, in which case take the whole subtree.
86
+ // isDirectory() is false for a symlink-to-dir, so a planted link is treated as a
87
+ // leaf and removed rather than followed.
88
+ if (entry.isDirectory()) {
89
+ if (knownDirPrefixes.has(`${relPath}/`)) {
90
+ await sweepDirectory(fullPath, relPath, depth + 1, known, knownDirPrefixes, acc);
91
+ continue;
92
+ }
93
+ acc.scanned++;
94
+ try {
95
+ await fs.rm(fullPath, { recursive: true, force: true });
96
+ acc.removed.push(relPath);
97
+ }
98
+ catch (err) {
99
+ acc.failed.push({ name: relPath, error: err }); /* per-item isolation (avoids PF-009) */
100
+ }
101
+ continue;
102
+ }
103
+ acc.scanned++;
104
+ if (known.has(relPath))
105
+ continue;
106
+ try {
107
+ await fs.rm(fullPath, { force: true });
108
+ acc.removed.push(relPath);
109
+ }
110
+ catch (err) {
111
+ acc.failed.push({ name: relPath, error: err }); /* per-item isolation (avoids PF-009) */
112
+ }
113
+ }
114
+ }
115
+ /**
116
+ * Every directory prefix some manifest path sits under, trailing slash included:
117
+ * `tracker/github/comment.md` contributes `tracker/` and `tracker/github/`.
118
+ *
119
+ * Built once per sweep so "does anything the manifest names live under this directory?"
120
+ * costs one `has` per entry instead of a scan of the whole manifest per entry. The
121
+ * answer is identical to that scan: `dir/` is a member exactly when some manifest path
122
+ * starts with `dir/`.
123
+ *
124
+ * Both loops are bounded by their own input — the inner one walks separators from a
125
+ * strictly increasing offset, so it terminates at the last one in the path.
126
+ */
127
+ function directoryPrefixes(known) {
128
+ const prefixes = new Set();
129
+ for (const p of known) {
130
+ for (let cut = p.indexOf('/'); cut !== -1; cut = p.indexOf('/', cut + 1)) {
131
+ prefixes.add(p.slice(0, cut + 1));
132
+ }
133
+ }
134
+ return prefixes;
135
+ }
136
+ //# sourceMappingURL=reference-sweep.js.map
@@ -0,0 +1,25 @@
1
+ import { promises as fs } from 'fs';
2
+ import * as path from 'path';
3
+ /**
4
+ * Whether two paths name one location — realpaths where they exist, so a symlinked
5
+ * HOME, or macOS's /var → /private/var temp tree, still matches. The shell hooks make
6
+ * the same physical comparison in git-marker's df_is_project_root (D-HOOKS-GIT-ONLY).
7
+ */
8
+ export async function isSameLocation(a, b) {
9
+ const canonical = (target) => fs.realpath(target).catch(() => path.resolve(target));
10
+ const [left, right] = await Promise.all([canonical(a), canonical(b)]);
11
+ return left === right;
12
+ }
13
+ /**
14
+ * D-INIT-NOT-HOME: the CLI half of D-HOOKS-GIT-ONLY. A git repository rooted at
15
+ * HOME (a dotfiles repo) is not a project: its `<root>/.devflow` is the machine
16
+ * root ~/.devflow, and its `.gitignore` and `.claudeignore` are the user's own
17
+ * home-directory files. So no command writes per-repository files there — the
18
+ * same rule the hooks' df_is_project_root applies — and `roots` is returned
19
+ * without any entry that is HOME.
20
+ */
21
+ export async function withoutHomeRoots(roots, homeDir) {
22
+ const verdicts = await Promise.all(roots.map(root => isSameLocation(root, homeDir)));
23
+ return roots.filter((_, i) => !verdicts[i]);
24
+ }
25
+ //# sourceMappingURL=same-location.js.map
@@ -0,0 +1,494 @@
1
+ /**
2
+ * Core issue-tracker provider registry, boundary parser, and file lifecycle.
3
+ *
4
+ * Two halves, deliberately in one module:
5
+ * - The DOMAIN half (registry, TrackerProvider, parseTrackerId,
6
+ * normalizeTrackerFeature, path derivation, the frontmatter parser) is
7
+ * pure — zero I/O.
8
+ * - The LIFECYCLE half (rearmTrackerInference, applyTrackerSentinel,
9
+ * migrateLegacyTrackerConventions) owns the ~/.devflow tracker files.
10
+ * It sits here rather than in a target adapter because ~/.devflow is
11
+ * devflow-global, not Claude-Code-specific — the same reason manifest.ts's
12
+ * read/write live in src/core/ (applies ADR-013).
13
+ *
14
+ * avoids PF-014: nothing here calls process.exit() and nothing throws; every
15
+ * fallible path returns a Result, so callers own their own error rendering
16
+ * and a try/finally in a caller is never skipped.
17
+ *
18
+ * D-TRACKER-OWNER [DR-22][DR-10]: the attempt counters and the machine provider
19
+ * sentinel have exactly ONE owner each — `rearmTrackerInference` and
20
+ * `applyTrackerSentinel` — and every command that touches one goes through it.
21
+ * The sentinel is converged by `devflow init` and `devflow tracker --set`; the
22
+ * counters are re-armed by those two and by `devflow tracker --status`, which
23
+ * is the command a capped user reaches for (D-F). The conventions files are the
24
+ * Tracker agent's to write and the user's to edit: no command moves, renames or
25
+ * deletes one, and the single legacy file is moved once, by the migration that
26
+ * calls `migrateLegacyTrackerConventions`. Never inline an `fs.rm` at a call
27
+ * site.
28
+ */
29
+ import { promises as fs } from 'fs';
30
+ import * as path from 'path';
31
+ /**
32
+ * Canonical issue-tracker provider registry.
33
+ *
34
+ * D-TRACKER-ONE-DOMAIN [PF-049]: this table is the SINGLE authority on the closed
35
+ * provider set, and `TrackerProvider` below is a projection of it. A provider
36
+ * therefore exists for the type system exactly when it has a row here: there is no
37
+ * hand-listed union that can admit an id `parseTrackerId` rejects and the wizard
38
+ * never offers. `as const satisfies` is what buys both halves — `satisfies` checks
39
+ * every row against `TrackerProviderDefinition` while `as const` keeps the ids as
40
+ * literals rather than widening them to `string` (the same pattern
41
+ * `VARIANT_MODULES` uses in src/core/mds-variants.ts).
42
+ *
43
+ * Labels and hints are stamped verbatim into rendered prompts — no user input is
44
+ * ever written. The validated ID selects a hardcoded path prefix from a static
45
+ * map at every consumer; the input string is never concatenated into a path.
46
+ *
47
+ * Hints deliberately avoid the token "MCP": transport must not leak into
48
+ * user-facing text (standing prohibition).
49
+ */
50
+ export const TRACKER_PROVIDERS = [
51
+ { id: 'github', label: 'GitHub', hint: 'GitHub Issues through the gh CLI' },
52
+ { id: 'jira', label: 'Jira', hint: 'Atlassian Jira issues and projects' },
53
+ { id: 'linear', label: 'Linear', hint: 'Linear issues and projects' },
54
+ ];
55
+ /** Registry IDs in registry order. */
56
+ export const TRACKER_PROVIDER_IDS = TRACKER_PROVIDERS.map(p => p.id);
57
+ /**
58
+ * The off position and the value every malformed input self-heals to.
59
+ * Existing installs and every GitHub user land here, silently.
60
+ */
61
+ export const DEFAULT_TRACKER_PROVIDER = 'github';
62
+ // ---------------------------------------------------------------------------
63
+ // Artifact basenames — one spelling for every TypeScript reader
64
+ //
65
+ // NOT the only spelling in the repository, and a rename that assumes it is will
66
+ // miss the places these names are hardcoded (PF-013): the SessionStart hook's
67
+ // Section 3 (shell) and the Tracker agent's prompt (prose), neither of which can
68
+ // import from here. Each is cross-pinned against these constants by tests —
69
+ // shell-hooks-tracker, tracker-agent, uninstall-logic and core/tracker — so the
70
+ // spellings cannot drift silently, but they do have to move together.
71
+ //
72
+ // Two families are one-per-provider and are DERIVED from the registry rather
73
+ // than spelled out — the conventions files under TRACKER_CONVENTIONS_DIR and the
74
+ // attempt counters (TRACKER_ATTEMPTS_NAMES) — so a fourth provider is covered the
75
+ // day it joins the registry. The staging files are one-per-invocation under a
76
+ // mktemp name, so uninstall imports TRACKER_STAGED_PREFIX and resolves it against
77
+ // disk.
78
+ // ---------------------------------------------------------------------------
79
+ /**
80
+ * `~/.devflow/tracker/` — one inferred conventions file per provider
81
+ * (USER CONTENT on uninstall).
82
+ *
83
+ * D-TRACKER-PER-PROVIDER-CONVENTIONS: conventions are learned per PROVIDER, not
84
+ * per machine. A repository may select its own tracker in its committed
85
+ * `.devflow/project.json`, so one machine can meet more than one provider, and a
86
+ * single machine-wide file would be silently authoritative for whichever provider
87
+ * did not write it. One file per provider means a provider change moves nothing
88
+ * aside: the other provider's conventions stay where they are, correct for the
89
+ * repositories that use it.
90
+ */
91
+ export const TRACKER_CONVENTIONS_DIR = 'tracker';
92
+ /**
93
+ * `~/.devflow/tracker.md` — the single machine-wide conventions file releases
94
+ * before per-provider conventions wrote. The `tracker-conventions-per-provider-v1`
95
+ * migration moves it to its provider's file; one it cannot place (no provider in
96
+ * its frontmatter, or that provider's file already exists) stays, as USER CONTENT.
97
+ */
98
+ export const TRACKER_LEGACY_CONVENTIONS_FILE = 'tracker.md';
99
+ /** `~/.devflow/.tracker.attempts` — the single counter those releases kept; the migration removes it. */
100
+ export const TRACKER_LEGACY_ATTEMPTS_FILE = '.tracker.attempts';
101
+ /**
102
+ * `~/.devflow/.tracker.enabled` — the machine provider sentinel (install artifact).
103
+ * Holds the provider NAME on one line; absent for github (see applyTrackerSentinel).
104
+ */
105
+ export const TRACKER_ENABLED_FILE = '.tracker.enabled';
106
+ /** `~/.devflow/.tracker.processing` — the Tracker agent's atomic claim (install artifact). */
107
+ export const TRACKER_CLAIM_FILE = '.tracker.processing';
108
+ /**
109
+ * `~/.devflow/.tracker-staged.XXXXXX` — the Tracker agent's scrubbed staging
110
+ * file (install artifact). A basename PREFIX, not a basename.
111
+ *
112
+ * The agent takes its stage with `mktemp` inside `~/.devflow`, one per
113
+ * invocation so two concurrent runs never share a path, and removes it from a
114
+ * `trap` on EXIT INT TERM. A SIGKILL outruns the trap, so a stage can outlive
115
+ * the run it belongs to — and an artifacts-only uninstall that removes exact
116
+ * paths walks straight past it while reporting the directory swept. It carries
117
+ * no user-authored content (it is a scrubbed, unplaced copy of what the agent
118
+ * was about to write), so it is an install artifact, never user content.
119
+ *
120
+ * Spelled twice for the reason the basenames above are (PF-013): the agent's
121
+ * prompt cannot import from here, so the mktemp template is also a literal in
122
+ * src/assets/agents/tracker.md, and tests/core/tracker.test.ts pins the two
123
+ * spellings together.
124
+ */
125
+ export const TRACKER_STAGED_PREFIX = '.tracker-staged.';
126
+ /**
127
+ * `.tracker.{provider}.attempts` — one provider's inference attempt counter
128
+ * (install artifact).
129
+ *
130
+ * Per provider, so one provider whose tracker connection is broken spends only
131
+ * its own attempts: a machine that meets jira in one repository and linear in
132
+ * another must not have the broken one cap the working one. The claim file stays
133
+ * global — one Tracker agent runs at a time on a machine, whichever provider it
134
+ * is learning.
135
+ */
136
+ export function trackerAttemptsName(provider) {
137
+ return `.tracker.${provider}.attempts`;
138
+ }
139
+ /** Every provider's attempt-counter basename, in registry order — what a re-arm clears and uninstall removes. */
140
+ export const TRACKER_ATTEMPTS_NAMES = TRACKER_PROVIDER_IDS.map(id => trackerAttemptsName(id));
141
+ /**
142
+ * How many background inference attempts a machine gets before the SessionStart
143
+ * hook stops emitting the setup directive.
144
+ *
145
+ * Spelled twice for the reason the basenames above are (PF-013): the hook is the
146
+ * enforcer and cannot import from here, so `TRACKER_ATTEMPTS_MAX=5` is also a
147
+ * literal in src/assets/scripts/hooks/session-start-context. This constant is the
148
+ * number `devflow tracker --status` quotes back when it re-arms the counter, and
149
+ * tests/core/tracker.test.ts pins the two spellings together so the report cannot
150
+ * promise more tries than the hook grants.
151
+ */
152
+ export const TRACKER_ATTEMPTS_MAX = 5;
153
+ // ---------------------------------------------------------------------------
154
+ // Internal helpers
155
+ // ---------------------------------------------------------------------------
156
+ const REGISTRY_SET = new Set(TRACKER_PROVIDER_IDS);
157
+ const VALID_IDS_LIST = TRACKER_PROVIDER_IDS.join(', ');
158
+ /** Longest rejected value echoed back to the terminal, in CODE POINTS. */
159
+ const MAX_ECHOED_VALUE = 40;
160
+ function errorMessage(err) {
161
+ return err instanceof Error ? err.message : String(err);
162
+ }
163
+ /** The errno of a rejected fs call, or undefined when the failure carries none. */
164
+ function errnoCode(err) {
165
+ return typeof err === 'object' && err !== null ? err.code : undefined;
166
+ }
167
+ // ---------------------------------------------------------------------------
168
+ // Exported functions — domain
169
+ // ---------------------------------------------------------------------------
170
+ /**
171
+ * The one runtime membership test for the provider domain.
172
+ *
173
+ * Byte-exact against the registry: no trim, no case folding, no alias. Both the
174
+ * strict parser and the tolerant normaliser narrow through this, so the domain
175
+ * the compiler enforces and the domain the boundary enforces are the same set by
176
+ * construction rather than by two casts that happen to agree.
177
+ */
178
+ export function isTrackerProvider(value) {
179
+ return typeof value === 'string' && REGISTRY_SET.has(value);
180
+ }
181
+ /**
182
+ * Render an untrusted provider token for a terminal message.
183
+ *
184
+ * Rejected values are echoed so the user can see what they typed, so they are
185
+ * third-party input at a display sink: control characters (terminal escapes,
186
+ * BEL, newlines) are replaced and the value is truncated. Used by
187
+ * `parseTrackerId`'s error text and by `devflow tracker --status`.
188
+ *
189
+ * Takes `unknown`, and a non-string renders as its TYPE: the module's
190
+ * never-throws contract (PF-014) has to hold for what reaches this sink, not
191
+ * only for what the signature says does — `devflow tracker --status` reads
192
+ * a conventions file's hand-editable frontmatter, and a caller-side guard is one edit
193
+ * from being gone. Naming the type also keeps the render total, where `String()`
194
+ * would hand control to a caller-supplied `toString` — itself both a throw path
195
+ * and an echo path this function exists to close.
196
+ */
197
+ export function describeTrackerValue(raw) {
198
+ if (typeof raw !== 'string')
199
+ return `<${raw === null ? 'null' : typeof raw}>`;
200
+ // The class is written with ESCAPES, never literal control bytes. A raw NUL
201
+ // makes grep classify this whole file as binary — it prints "Binary file
202
+ // matches" and skips the lines — so every grep-based sweep over src/core/
203
+ // silently stops covering tracker.ts while still exiting 0. Pinned by
204
+ // tests/guards/no-control-bytes.test.ts.
205
+ // eslint-disable-next-line no-control-regex
206
+ const stripped = raw.replace(/[\x00-\x1f\x7f]/g, '?');
207
+ // Cut by CODE POINT. `slice` cuts UTF-16 code units, so a cut landing inside an
208
+ // astral character (emoji, CJK ext-B) emits the lone surrogate half of it.
209
+ const points = [...stripped];
210
+ return points.length > MAX_ECHOED_VALUE
211
+ ? `${points.slice(0, MAX_ECHOED_VALUE).join('')}…`
212
+ : stripped;
213
+ }
214
+ /**
215
+ * Strict boundary parser for a provider token (`--tracker <id>`,
216
+ * `devflow tracker --set <id>`).
217
+ *
218
+ * D-TRACKER-STRICT: REJECT, NEVER REPAIR. Membership is byte-exact against the
219
+ * registry — no trim, no case folding, no alias, no space-to-dash. `jira-cloud`
220
+ * errors rather than becoming `jira`; `JIRA` and `jira ` error rather than being
221
+ * repaired into `jira`. This is the one place compliance's shape is copied but
222
+ * its `normalizeId` is NOT: repairing a provider silently installs mechanics for
223
+ * a tracker the user did not name, and a repaired token is the echo the
224
+ * static-path-prefix rule exists to prevent. All-invalid input never yields a
225
+ * silent default.
226
+ *
227
+ * The error names every valid ID and the offending value.
228
+ */
229
+ export function parseTrackerId(input) {
230
+ if (input === '') {
231
+ return { ok: false, error: `Missing tracker provider ID. Valid IDs: ${VALID_IDS_LIST}` };
232
+ }
233
+ if (!isTrackerProvider(input)) {
234
+ return {
235
+ ok: false,
236
+ error: `Unknown tracker provider ID: "${describeTrackerValue(input)}". Valid IDs: ${VALID_IDS_LIST}`,
237
+ };
238
+ }
239
+ return { ok: true, value: input };
240
+ }
241
+ /**
242
+ * Tolerant sink normaliser for a raw `manifest.features.tracker` value.
243
+ *
244
+ * Absent, null, malformed, a bare string, or an unknown provider → the default
245
+ * `{provider:'github'}` (applies ADR-014 self-heal). Drop-not-error: a manifest
246
+ * written by a newer devflow, or hand-edited, degrades to what this build
247
+ * understands instead of failing the read.
248
+ *
249
+ * D-TRACKER-SELF-HEAL [DR-26]: self-healing here is SILENT and emits no
250
+ * DEGRADED — that is the correct ADR-014 behaviour, and it is a different
251
+ * condition from a per-repo config value outside the domain (which does emit
252
+ * `unknown tracker provider`). The two must not be conflated.
253
+ */
254
+ export function normalizeTrackerFeature(raw) {
255
+ const DEFAULT = { provider: DEFAULT_TRACKER_PROVIDER };
256
+ if (raw === null || raw === undefined || typeof raw !== 'object' || Array.isArray(raw)) {
257
+ return DEFAULT;
258
+ }
259
+ const obj = raw;
260
+ if (!isTrackerProvider(obj.provider))
261
+ return DEFAULT;
262
+ return { provider: obj.provider };
263
+ }
264
+ // ---------------------------------------------------------------------------
265
+ // Exported functions — path derivation (pure)
266
+ // ---------------------------------------------------------------------------
267
+ /** `{devflowDir}/tracker` — the directory holding one conventions file per provider. */
268
+ export function trackerConventionsDir(devflowDir) {
269
+ return path.join(devflowDir, TRACKER_CONVENTIONS_DIR);
270
+ }
271
+ /**
272
+ * `{devflowDir}/tracker/{provider}.md` — one provider's inferred conventions.
273
+ *
274
+ * The provider is a registry id (the type admits nothing else), so the segment it
275
+ * contributes is one of three fixed words, never user input joined into a path.
276
+ */
277
+ export function trackerConventionsPath(devflowDir, provider) {
278
+ return path.join(trackerConventionsDir(devflowDir), `${provider}.md`);
279
+ }
280
+ /** `{devflowDir}/.tracker.{provider}.attempts` — one provider's inference attempt counter. */
281
+ export function trackerAttemptsPath(devflowDir, provider) {
282
+ return path.join(devflowDir, trackerAttemptsName(provider));
283
+ }
284
+ /** `{devflowDir}/.tracker.enabled` — the machine provider sentinel. */
285
+ export function trackerEnabledSentinelPath(devflowDir) {
286
+ return path.join(devflowDir, TRACKER_ENABLED_FILE);
287
+ }
288
+ /** How many leading lines of a conventions file are scanned for frontmatter. */
289
+ const FRONTMATTER_SCAN_LINES = 40;
290
+ /**
291
+ * Parse the leading frontmatter of a conventions file's head.
292
+ *
293
+ * The one parser, shared by `devflow tracker --status` and the
294
+ * per-provider migration, so the two can never disagree about which provider a
295
+ * file names. Scans at most {@link FRONTMATTER_SCAN_LINES} lines; the first
296
+ * occurrence of each key wins.
297
+ */
298
+ export function parseTrackerFrontmatter(head) {
299
+ const lines = head.split('\n', FRONTMATTER_SCAN_LINES);
300
+ if (lines[0]?.trim() !== '---')
301
+ return { hasFrontmatter: false };
302
+ let provider;
303
+ let inferredFrom;
304
+ for (const line of lines.slice(1)) {
305
+ if (line.trim() === '---')
306
+ break;
307
+ const match = /^([A-Za-z-]+):\s*(.*)$/.exec(line);
308
+ if (match === null)
309
+ continue;
310
+ if (match[1] === 'provider' && provider === undefined)
311
+ provider = match[2].trim();
312
+ if (match[1] === 'inferred-from' && inferredFrom === undefined)
313
+ inferredFrom = match[2].trim();
314
+ }
315
+ return { hasFrontmatter: true, provider, inferredFrom };
316
+ }
317
+ /**
318
+ * How many leading BYTES of a conventions file any reader takes — the bound the
319
+ * Tracker agent writes to and the Git agent loads. A line cap alone bounds the
320
+ * SCAN, not the read: the file's size is not devflow's to assume (avoids PF-023:
321
+ * a bound is only real at the sink).
322
+ */
323
+ export const TRACKER_CONVENTIONS_READ_BYTES = 8000;
324
+ /**
325
+ * Read at most `limit` bytes from the head of a REGULAR file.
326
+ *
327
+ * `undefined` for an absent, unreadable or non-regular path — a FIFO or a device
328
+ * is refused before it is opened, so a hostile entry can never block a read.
329
+ * Follows a symlink to read what it names (a reader cares what the conventions
330
+ * SAY); it never moves or writes through one. Never throws (PF-014).
331
+ */
332
+ export async function readBoundedHead(filePath, limit) {
333
+ let handle;
334
+ try {
335
+ if (!(await fs.stat(filePath)).isFile())
336
+ return undefined;
337
+ handle = await fs.open(filePath, 'r');
338
+ const buffer = Buffer.alloc(limit);
339
+ const { bytesRead } = await handle.read(buffer, 0, limit, 0);
340
+ return buffer.subarray(0, bytesRead).toString('utf-8');
341
+ }
342
+ catch {
343
+ return undefined;
344
+ }
345
+ finally {
346
+ await handle?.close().catch(() => undefined);
347
+ }
348
+ }
349
+ // ---------------------------------------------------------------------------
350
+ // Exported functions — file lifecycle
351
+ // ---------------------------------------------------------------------------
352
+ /**
353
+ * Re-arm background convention inference by removing every provider's attempt
354
+ * counter.
355
+ *
356
+ * The documented re-arm path (OD-14 / D-F): `devflow init` AND
357
+ * `devflow tracker --set/--status` all re-arm, so a user whose tracker server
358
+ * was broken for five sessions is not stuck at the cap forever. Every provider's
359
+ * counter, not only the machine's: a repository's committed project.json can
360
+ * select a provider the machine never did, and its counter is the one a user in
361
+ * that repository is capped on.
362
+ *
363
+ * Idempotent when a counter is absent; never throws (PF-014). `fs.rm` with
364
+ * `force` treats an absent file — and an absent parent directory — as success.
365
+ */
366
+ export async function rearmTrackerInference(devflowDir) {
367
+ try {
368
+ for (const name of TRACKER_ATTEMPTS_NAMES) {
369
+ await fs.rm(path.join(devflowDir, name), { force: true });
370
+ }
371
+ return { ok: true, value: undefined };
372
+ }
373
+ catch (err) {
374
+ return { ok: false, error: `Could not reset the tracker attempt counter: ${errorMessage(err)}` };
375
+ }
376
+ }
377
+ /**
378
+ * Converge the `.tracker.enabled` sentinel to the machine provider.
379
+ *
380
+ * [DR-10] Holds the provider NAME, one line, whenever the machine provider is NOT
381
+ * github, and is removed when it is. The SessionStart hook reads it with the
382
+ * `read` builtin, so the machine provider costs no fork: a GitHub user pays one
383
+ * `stat`, and a jira machine whose conventions are already learned pays a stat,
384
+ * a builtin read and a second stat — zero forks either way. A name rather than a
385
+ * bare presence marker, so the hook never has to open the manifest to learn which
386
+ * provider it is gating.
387
+ *
388
+ * Converges unconditionally in both directions (avoids PF-015): a provider
389
+ * flipped back to github removes the sentinel in the same call shape that wrote
390
+ * it, so there is no "enable wrote it, disable forgot it" asymmetry.
391
+ */
392
+ export async function applyTrackerSentinel(devflowDir, provider) {
393
+ const sentinel = trackerEnabledSentinelPath(devflowDir);
394
+ try {
395
+ if (provider === DEFAULT_TRACKER_PROVIDER) {
396
+ await fs.rm(sentinel, { force: true });
397
+ }
398
+ else {
399
+ await fs.mkdir(devflowDir, { recursive: true });
400
+ await fs.writeFile(sentinel, `${provider}\n`, 'utf-8');
401
+ }
402
+ return { ok: true, value: undefined };
403
+ }
404
+ catch (err) {
405
+ return { ok: false, error: `Could not update the tracker sentinel: ${errorMessage(err)}` };
406
+ }
407
+ }
408
+ /**
409
+ * Move the pre-per-provider `~/.devflow/tracker.md` to the provider file its
410
+ * frontmatter names (D-TRACKER-PER-PROVIDER-CONVENTIONS), once.
411
+ *
412
+ * - no legacy file → `none`
413
+ * - frontmatter names a registry provider,
414
+ * and that provider's file does not exist → `moved`, by `rename(2)`
415
+ * - no provider it can name, or the target
416
+ * already exists → `kept`: the file stays where it is,
417
+ * user content, with one reason
418
+ * - any I/O failure → `failed`
419
+ *
420
+ * `rename(2)` moves a SYMLINK itself, never the file it points at, so a
421
+ * conventions file kept in a dotfiles repository stays there and only the link
422
+ * moves. A link's target is resolved relative to the link's directory, so a
423
+ * RELATIVE link would resolve from one level deeper and name a different path —
424
+ * the conventions would silently vanish behind a dangling link that also blocks
425
+ * the Tracker agent's create-exclusive write. A relative link is therefore `kept`,
426
+ * with its reason; an absolute one moves unaffected. rename also replaces an
427
+ * existing destination without a word, which is why the target is probed first
428
+ * and a present one is never overwritten: that file holds conventions a user may
429
+ * have corrected by hand, and it is the provider's own.
430
+ *
431
+ * D-TRACKER-MIGRATE-WINDOW: the probe and the rename are two calls, so a Tracker
432
+ * agent that writes the same provider's file between them is replaced by the
433
+ * legacy one. Accepted: the window is one syscall wide, only `devflow init` opens
434
+ * it, and both files hold that provider's conventions. link-then-unlink would
435
+ * close it but follows a symlink on macOS, fails where hard links are
436
+ * unsupported, and can leave both names behind.
437
+ *
438
+ * The legacy single attempt counter is removed on every run that reaches the end:
439
+ * it carries no user content, the per-provider counters replace it, and a file
440
+ * nothing reads is one no uninstall list would account for.
441
+ */
442
+ export async function migrateLegacyTrackerConventions(devflowDir) {
443
+ const from = path.join(devflowDir, TRACKER_LEGACY_CONVENTIONS_FILE);
444
+ try {
445
+ await fs.rm(path.join(devflowDir, TRACKER_LEGACY_ATTEMPTS_FILE), { force: true });
446
+ let isLink;
447
+ try {
448
+ isLink = (await fs.lstat(from)).isSymbolicLink();
449
+ }
450
+ catch (err) {
451
+ if (errnoCode(err) === 'ENOENT')
452
+ return { kind: 'none' };
453
+ throw err;
454
+ }
455
+ if (isLink && !path.isAbsolute(await fs.readlink(from))) {
456
+ return {
457
+ kind: 'kept',
458
+ reason: `${from} is a symbolic link with a relative target, which would no longer resolve from ` +
459
+ `${trackerConventionsDir(devflowDir)}, so it was left in place — re-create it there by hand.`,
460
+ };
461
+ }
462
+ const head = await readBoundedHead(from, TRACKER_CONVENTIONS_READ_BYTES);
463
+ if (head === undefined) {
464
+ return { kind: 'kept', reason: `${from} is not a readable regular file, so it was left in place.` };
465
+ }
466
+ const named = parseTrackerFrontmatter(head).provider;
467
+ if (!isTrackerProvider(named)) {
468
+ return {
469
+ kind: 'kept',
470
+ reason: `${from} names no tracker provider in its frontmatter, so it was left in place — ` +
471
+ `move it to ${trackerConventionsDir(devflowDir)}/{provider}.md by hand if it is still wanted.`,
472
+ };
473
+ }
474
+ const to = trackerConventionsPath(devflowDir, named);
475
+ try {
476
+ await fs.lstat(to);
477
+ return {
478
+ kind: 'kept',
479
+ reason: `${from} was left in place — ${to} already exists and is never overwritten.`,
480
+ };
481
+ }
482
+ catch (err) {
483
+ if (errnoCode(err) !== 'ENOENT')
484
+ throw err;
485
+ }
486
+ await fs.mkdir(trackerConventionsDir(devflowDir), { recursive: true });
487
+ await fs.rename(from, to);
488
+ return { kind: 'moved', from, to, provider: named };
489
+ }
490
+ catch (err) {
491
+ return { kind: 'failed', error: `Could not move ${from} to its provider's file: ${errorMessage(err)}` };
492
+ }
493
+ }
494
+ //# sourceMappingURL=tracker.js.map