devflow-kit 2.4.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/README.md +86 -18
  3. package/dist/agents/git.md +824 -0
  4. package/dist/cli/commands/agents.js +6 -1
  5. package/dist/cli/commands/attribution-prompts.js +1 -1
  6. package/dist/cli/commands/compliance-prompts.js +1 -1
  7. package/dist/cli/commands/compliance.js +23 -1
  8. package/dist/cli/commands/init-seed.js +24 -26
  9. package/dist/cli/commands/init.js +502 -71
  10. package/dist/cli/commands/install-report.js +205 -0
  11. package/dist/cli/commands/knowledge/index.js +2 -2
  12. package/dist/cli/commands/knowledge/toggle.js +27 -37
  13. package/dist/cli/commands/learning.js +37 -30
  14. package/dist/cli/commands/memory.js +79 -69
  15. package/dist/cli/commands/prompt-io.js +4 -4
  16. package/dist/cli/commands/security.js +76 -16
  17. package/dist/cli/commands/skills.js +53 -7
  18. package/dist/cli/commands/tracker-prompts.js +145 -0
  19. package/dist/cli/commands/tracker.js +405 -0
  20. package/dist/cli/commands/uninstall.js +211 -65
  21. package/dist/cli.js +2 -0
  22. package/dist/commands/bug-analysis.md +22 -4
  23. package/dist/commands/code-review.md +44 -15
  24. package/dist/commands/debug.md +20 -6
  25. package/dist/commands/dynamic-build.md +289 -67
  26. package/dist/commands/dynamic-plan.md +60 -21
  27. package/dist/commands/dynamic-profile.md +1 -1
  28. package/dist/commands/dynamic-tickets.md +58 -8
  29. package/dist/commands/explore.md +2 -2
  30. package/dist/commands/implement.md +241 -53
  31. package/dist/commands/plan.md +88 -17
  32. package/dist/commands/release.md +64 -17
  33. package/dist/commands/resolve.md +138 -58
  34. package/dist/commands/self-review.md +2 -2
  35. package/dist/core/agent-models.js +55 -12
  36. package/dist/core/assets.js +58 -2
  37. package/dist/core/evidence-policy.js +147 -0
  38. package/dist/core/feature-config.js +130 -64
  39. package/dist/core/feature-switch.js +112 -0
  40. package/dist/core/flags.js +4 -4
  41. package/dist/core/manifest.js +33 -7
  42. package/dist/core/mds-variants.js +861 -0
  43. package/dist/core/model-discovery.js +12 -1
  44. package/dist/core/plugins.js +357 -9
  45. package/dist/core/project-paths.js +1 -1
  46. package/dist/core/proxy-log.js +8 -6
  47. package/dist/core/proxy-state.js +11 -8
  48. package/dist/core/reference-sweep.js +136 -0
  49. package/dist/core/tracker.js +407 -0
  50. package/dist/skills/git/references/decision-markers.md +19 -0
  51. package/dist/skills/git/references/learn-conventions.md +56 -0
  52. package/dist/skills/git/references/pr/check-ci-status.md +14 -0
  53. package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
  54. package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
  55. package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
  56. package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
  57. package/dist/skills/git/references/pr/post-review-summary.md +42 -0
  58. package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
  59. package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
  60. package/dist/skills/git/references/pr/validate-branch.md +18 -0
  61. package/dist/skills/git/references/publication-gate.md +13 -0
  62. package/dist/skills/git/references/tracker/_mcp.md +153 -0
  63. package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
  64. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
  65. package/dist/skills/git/references/tracker/github/create-release.md +11 -0
  66. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
  67. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
  68. package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
  69. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
  70. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
  71. package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
  72. package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
  73. package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
  74. package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
  75. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
  76. package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
  77. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
  78. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
  79. package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
  80. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
  81. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
  82. package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
  83. package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
  84. package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
  85. package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
  86. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
  87. package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
  88. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
  89. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
  90. package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
  91. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
  92. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
  93. package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
  94. package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
  95. package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
  96. package/dist/skills/git/references/trust-rule.md +7 -0
  97. package/dist/targets/claude-code/installer.js +1213 -31
  98. package/dist/targets/claude-code/legacy.js +5 -0
  99. package/dist/targets/claude-code/post-install.js +196 -74
  100. package/dist/targets/claude-code/tracker-install.js +161 -0
  101. package/package.json +4 -3
  102. package/src/assets/agents/code.md +42 -4
  103. package/src/assets/agents/design.md +1 -1
  104. package/src/assets/agents/git.mds +827 -0
  105. package/src/assets/agents/knowledge.md +1 -1
  106. package/src/assets/agents/learning.md +11 -0
  107. package/src/assets/agents/synthesize.md +1 -1
  108. package/src/assets/agents/test.md +16 -5
  109. package/src/assets/agents/tracker.md +467 -0
  110. package/src/assets/agents/validate.md +7 -5
  111. package/src/assets/commands/_partials/_engine.mds +11 -9
  112. package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
  113. package/src/assets/commands/_partials/_knowledge.mds +2 -2
  114. package/src/assets/commands/_partials/_plan_contract.mds +22 -7
  115. package/src/assets/commands/_partials/_preamble.mds +1 -1
  116. package/src/assets/commands/_partials/_publication.mds +3 -1
  117. package/src/assets/commands/_partials/_ticket_template.mds +3 -2
  118. package/src/assets/commands/_partials/_tracker.mds +18 -0
  119. package/src/assets/commands/_partials/_wave.mds +16 -10
  120. package/src/assets/commands/bug-analysis.mds +15 -5
  121. package/src/assets/commands/code-review.mds +34 -14
  122. package/src/assets/commands/debug.mds +11 -4
  123. package/src/assets/commands/dynamic-build.mds +227 -41
  124. package/src/assets/commands/dynamic-plan.mds +35 -13
  125. package/src/assets/commands/dynamic-tickets.mds +47 -5
  126. package/src/assets/commands/implement.mds +206 -52
  127. package/src/assets/commands/plan.mds +70 -17
  128. package/src/assets/commands/release.md +64 -17
  129. package/src/assets/commands/resolve.mds +126 -56
  130. package/src/assets/mds/git/_pr.mds +331 -0
  131. package/src/assets/mds/git/_references.mds +135 -0
  132. package/src/assets/mds/tracker/_common.mds +156 -0
  133. package/src/assets/mds/tracker/_github.mds +472 -0
  134. package/src/assets/mds/tracker/_jira.mds +407 -0
  135. package/src/assets/mds/tracker/_linear.mds +449 -0
  136. package/src/assets/mds/tracker/_mcp.mds +299 -0
  137. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
  138. package/src/assets/scripts/hooks/background-memory-update +14 -9
  139. package/src/assets/scripts/hooks/capture-prompt +6 -2
  140. package/src/assets/scripts/hooks/capture-question +6 -2
  141. package/src/assets/scripts/hooks/capture-turn +6 -2
  142. package/src/assets/scripts/hooks/ensure-devflow-init +1 -1
  143. package/src/assets/scripts/hooks/ensure-root-gitignore +161 -60
  144. package/src/assets/scripts/hooks/hook-log-init +3 -1
  145. package/src/assets/scripts/hooks/json-helper.cjs +223 -5
  146. package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -1
  147. package/src/assets/scripts/hooks/memory-worker +15 -8
  148. package/src/assets/scripts/hooks/pre-compact-memory +12 -8
  149. package/src/assets/scripts/hooks/preamble +1 -4
  150. package/src/assets/scripts/hooks/queue-append +68 -24
  151. package/src/assets/scripts/hooks/session-start-context +355 -8
  152. package/src/assets/scripts/hooks/session-start-memory +12 -8
  153. package/src/assets/scripts/pr-evidence.cjs +1961 -0
  154. package/src/assets/scripts/redact-secrets.cjs +490 -62
  155. package/src/assets/scripts/release-trace.cjs +1143 -0
  156. package/src/assets/scripts/resolve-evidence-policy.cjs +1065 -0
  157. package/src/assets/scripts/verify-evidence.cjs +1822 -0
  158. package/src/assets/skills/compliance/SKILL.md +2 -0
  159. package/src/assets/skills/docs-framework/SKILL.md +5 -3
  160. package/src/assets/skills/git/SKILL.md +8 -78
  161. package/src/assets/skills/git/references/github-api.md +179 -141
  162. package/src/assets/skills/git/references/patterns.md +11 -6
  163. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  164. package/src/assets/skills/review-methodology/references/patterns.md +6 -61
  165. package/src/assets/skills/review-methodology/references/violations.md +14 -22
  166. package/src/assets/agents/git.md +0 -938
@@ -0,0 +1,405 @@
1
+ /**
2
+ * devflow tracker — Show or set the issue tracker provider.
3
+ *
4
+ * D-TRACKER-PAIR [DR-25]: `src/core/tracker.ts` (domain) +
5
+ * `src/cli/commands/tracker.ts` (CLI) mirrors the `compliance.ts` pair
6
+ * exactly; ADR-013's pure-core / I/O-target split is the reason both names
7
+ * exist. A reviewer meeting several `tracker*` files in one commit otherwise
8
+ * has no signal that the duplication is deliberate.
9
+ *
10
+ * Applies ADR-013: CLI-layer module; the provider domain, the strict parser and
11
+ * the ~/.devflow file lifecycle all live in src/core/tracker.ts.
12
+ * The manifest is the source of truth for the selected provider: tracker is a
13
+ * manifest-group feature like proxy and compliance, not
14
+ * .devflow/config.json-gated, so the selection is machine-wide, not per-repo.
15
+ * Avoids PF-015: --set converges the sentinel in BOTH directions, so flipping
16
+ * back to github removes what flipping away wrote.
17
+ * Avoids PF-009: a failed rename/rearm/sentinel step warns, it never aborts.
18
+ */
19
+ import { Command } from 'commander';
20
+ import { promises as fs } from 'fs';
21
+ import * as path from 'path';
22
+ import * as p from '@clack/prompts';
23
+ import color from 'picocolors';
24
+ import { DEFAULT_TRACKER_PROVIDER, TRACKER_ATTEMPTS_MAX, TRACKER_PROVIDERS, applyTrackerSentinel, describeTrackerValue, parseTrackerId, rearmTrackerInference, renameStaleTrackerConventions, trackerConventionsPath, } from '../../core/tracker.js';
25
+ import { readManifest, syncManifestFeature } from '../../core/manifest.js';
26
+ import { getClaudeDirectory, getDevFlowDirectory } from '../../targets/claude-code/claude-paths.js';
27
+ import { overlayInstalledReferences, overlayUnitLabel } from '../../targets/claude-code/installer.js';
28
+ import { convergeTrackerArtifacts } from '../../targets/claude-code/tracker-install.js';
29
+ import { describeOverlayFailureState } from './install-report.js';
30
+ import { SKILL_REFS_SKILL_NAME, installedReferenceManifest } from '../../core/mds-variants.js';
31
+ import { prefixSkillName } from '../../core/plugins.js';
32
+ /** How many leading lines of tracker.md are scanned for frontmatter. */
33
+ const PROVENANCE_SCAN_LINES = 40;
34
+ /**
35
+ * How many leading BYTES of tracker.md are read.
36
+ *
37
+ * The line cap above bounds the SCAN, not the read: `String.prototype.split`'s
38
+ * limit truncates the resulting array once the whole file is already in memory.
39
+ * tracker.md is hand-editable and machine-wide, so its size is not devflow's to
40
+ * assume — this is the same bound the Tracker agent writes to and the Git agent
41
+ * loads, enforced at the one TypeScript reader (avoids PF-023: a bound is only
42
+ * real at the sink, and a file round-trip launders the writer's promise).
43
+ */
44
+ const PROVENANCE_READ_BYTES = 8000;
45
+ /**
46
+ * Read at most `limit` bytes from the head of a file.
47
+ *
48
+ * `undefined` for an absent, unreadable or non-file path — the caller reports
49
+ * that as `absent` (PF-014: never throws).
50
+ */
51
+ async function readBoundedHead(filePath, limit) {
52
+ let handle;
53
+ try {
54
+ handle = await fs.open(filePath, 'r');
55
+ const buffer = Buffer.alloc(limit);
56
+ const { bytesRead } = await handle.read(buffer, 0, limit, 0);
57
+ return buffer.subarray(0, bytesRead).toString('utf-8');
58
+ }
59
+ catch {
60
+ return undefined;
61
+ }
62
+ finally {
63
+ await handle?.close().catch(() => undefined);
64
+ }
65
+ }
66
+ /**
67
+ * Read the provenance header of `~/.devflow/tracker.md`.
68
+ *
69
+ * Never throws (PF-014): an absent, unreadable, or directory path is `absent`.
70
+ * Only the bounded head of the file is read and only the leading frontmatter
71
+ * block is scanned, and nothing read here is trusted — every value is rendered
72
+ * through `describeTrackerValue`, because tracker.md is hand-editable and
73
+ * machine-wide, so its content is third-party input at every sink.
74
+ */
75
+ export async function readTrackerProvenance(devflowDir) {
76
+ const head = await readBoundedHead(trackerConventionsPath(devflowDir), PROVENANCE_READ_BYTES);
77
+ if (head === undefined)
78
+ return { kind: 'absent' };
79
+ const lines = head.split('\n', PROVENANCE_SCAN_LINES);
80
+ if (lines[0]?.trim() !== '---')
81
+ return { kind: 'present' };
82
+ let provider;
83
+ let inferredFrom;
84
+ for (const line of lines.slice(1)) {
85
+ if (line.trim() === '---')
86
+ break;
87
+ const match = /^([A-Za-z-]+):\s*(.*)$/.exec(line);
88
+ if (match === null)
89
+ continue;
90
+ if (match[1] === 'provider' && provider === undefined)
91
+ provider = match[2].trim();
92
+ if (match[1] === 'inferred-from' && inferredFrom === undefined)
93
+ inferredFrom = match[2].trim();
94
+ }
95
+ return { kind: 'present', provider, inferredFrom };
96
+ }
97
+ /**
98
+ * Count the installed tracker mechanics for the resolved provider.
99
+ *
100
+ * Counts what is PRESENT against the manifest for that provider rather than
101
+ * listing the directory: the question a user asks `--status` is "can the agent
102
+ * load what it is told to load?", and a stray file in the tree is not an answer
103
+ * to it. Never throws (PF-014).
104
+ */
105
+ export async function readTrackerMechanics(claudeDir, provider) {
106
+ const root = path.join(claudeDir, 'skills', prefixSkillName(SKILL_REFS_SKILL_NAME), 'references');
107
+ let present = 0;
108
+ for (const rel of installedReferenceManifest({ provider })) {
109
+ try {
110
+ await fs.access(path.join(root, ...rel.split('/')));
111
+ present++;
112
+ }
113
+ catch (err) {
114
+ const code = err.code;
115
+ // ENOENT is the file simply not being there — that is a count of zero for
116
+ // this entry, not a failure to look. Anything else IS a failure to look.
117
+ if (code !== undefined && code !== 'ENOENT')
118
+ return { kind: 'unreadable', errno: code };
119
+ }
120
+ }
121
+ return present === 0 ? { kind: 'missing' } : { kind: 'installed', count: present };
122
+ }
123
+ /** Render the mechanics state for the `--status` note. Pure. */
124
+ export function formatTrackerMechanics(state) {
125
+ switch (state.kind) {
126
+ case 'installed':
127
+ return `installed (${state.count} file(s))`;
128
+ case 'missing':
129
+ return 'MISSING — run devflow init';
130
+ case 'unreadable':
131
+ return `unreadable (${state.errno})`;
132
+ default: {
133
+ const _exhaustive = state;
134
+ void _exhaustive;
135
+ return 'unknown';
136
+ }
137
+ }
138
+ }
139
+ /** Render a provenance value for the `--status` note. Pure; bounded; sanitised. */
140
+ export function formatTrackerProvenance(provenance) {
141
+ if (provenance.kind === 'absent') {
142
+ return 'not present (learned in the background at a session start)';
143
+ }
144
+ const parts = ['present'];
145
+ if (provenance.provider !== undefined) {
146
+ parts.push(`provider: ${describeTrackerValue(provenance.provider)}`);
147
+ }
148
+ if (provenance.inferredFrom !== undefined) {
149
+ parts.push(`inferred from: ${describeTrackerValue(provenance.inferredFrom)}`);
150
+ }
151
+ return parts.join(' — ');
152
+ }
153
+ /** The real adapter — the ONE binding of each operation into the `--set` path. */
154
+ export function buildTrackerSetIO() {
155
+ return {
156
+ gitSkillInstalled: async (claudeDir) => {
157
+ try {
158
+ await fs.access(path.join(claudeDir, 'skills', prefixSkillName(SKILL_REFS_SKILL_NAME), 'SKILL.md'));
159
+ return true;
160
+ }
161
+ catch {
162
+ return false;
163
+ }
164
+ },
165
+ overlayReferences: (claudeDir, provider, warn) => overlayInstalledReferences({ claudeDir, provider, warn }),
166
+ renameStaleConventions: renameStaleTrackerConventions,
167
+ syncManifest: (devflowDir, state) => syncManifestFeature(devflowDir, 'tracker', state),
168
+ convergeArtifacts: (claudeDir, provider, warn) => convergeTrackerArtifacts({ claudeDir, provider, warn }),
169
+ rearmInference: rearmTrackerInference,
170
+ applySentinel: applyTrackerSentinel,
171
+ };
172
+ }
173
+ /**
174
+ * Converge every tracker artifact onto a newly selected provider.
175
+ *
176
+ * D-TRACKER-CONVERGE-SET: the step order is the invariant, and it is NOT the
177
+ * same as `devflow init`'s — the two are different call paths obeying one
178
+ * principle, and forcing them through a shared signature would hide the
179
+ * reordering rather than document it.
180
+ *
181
+ * 1. probe `devflow:git` — the mechanics have nowhere to land without it
182
+ * 2. OVERLAY the references (abort on failure, exit 1)
183
+ * 3. rename stale conventions
184
+ * 4. persist the manifest
185
+ * 5. converge the Tracker AGENT file
186
+ * 6. re-arm the attempt counter
187
+ * 7. converge the sentinel (write gated on 5; removal unconditional)
188
+ *
189
+ * The overlay precedes the manifest write, and the agent file follows it. That
190
+ * asymmetry is the point: the reference subtree is INERT — nothing loads it
191
+ * until a Git spawn resolves a provider, and resolving a provider reads the
192
+ * manifest — so installing it early costs nothing and lets a failure abort
193
+ * cleanly with the previous provider still whole. The agent file and the
194
+ * sentinel are ADVERTISING artifacts: they announce a provider, so they must
195
+ * never get ahead of the manifest that records it.
196
+ *
197
+ * Both abort branches — an absent `devflow:git` and a failed overlay — leave the
198
+ * SAME end state and exit 1 (design review C3): manifest, sentinel and
199
+ * conventions unchanged. What the overlay branch cannot claim is that nothing
200
+ * moved at all: the overlay is atomic PER UNIT, so units that succeeded are
201
+ * converged and only the failed ones are reported (design review H1). Saying
202
+ * "nothing else changed" would be the more comfortable sentence and the false
203
+ * one.
204
+ *
205
+ * Never throws, except where its callee does: an absent generated tree is a
206
+ * build artifact that was never produced, and the CLI turns that into exit 1
207
+ * with the build command named — asymmetric against the warn-only rename,
208
+ * re-arm and sentinel steps, because those degrade a working install while an
209
+ * absent tree means there is nothing to install at all.
210
+ */
211
+ export async function runTrackerSet(opts) {
212
+ const { devflowDir, claudeDir, current, requested, io } = opts;
213
+ const messages = [];
214
+ const abort = (text) => {
215
+ messages.push({ level: 'error', text });
216
+ return { exitCode: 1, provider: current.provider, messages };
217
+ };
218
+ // 1. Without devflow:git there is no references/ directory to converge into,
219
+ // and creating one would leave an invisible husk under a skill that does
220
+ // not exist — a directory no sweep looks inside because no skill claims it.
221
+ if (!await io.gitSkillInstalled(claudeDir)) {
222
+ return abort(`devflow:git is not installed — run devflow init --tracker ${requested}. ` +
223
+ `The manifest, sentinel and conventions file are unchanged.`);
224
+ }
225
+ // 2. The overlay. Runs even when the provider is unchanged, so a `--set` that
226
+ // repeats the current selection self-heals a damaged subtree instead of
227
+ // early-returning on an equality check that proves nothing about disk.
228
+ const overlayWarnings = [];
229
+ let overlay;
230
+ try {
231
+ overlay = await io.overlayReferences(claudeDir, requested, (msg) => overlayWarnings.push(msg));
232
+ }
233
+ catch (error) {
234
+ return abort(`Tracker: not changed — ${requested} assets could not be installed. ` +
235
+ `${error instanceof Error ? error.message : String(error)}`);
236
+ }
237
+ for (const text of overlayWarnings)
238
+ messages.push({ level: 'warn', text });
239
+ if (overlay.overlayFailures.length > 0) {
240
+ for (const failure of overlay.overlayFailures) {
241
+ messages.push({
242
+ level: 'warn',
243
+ text: `${overlayUnitLabel(failure.unit)}: ${failure.error} — ` +
244
+ `${describeOverlayFailureState(failure.state)}`,
245
+ });
246
+ }
247
+ return abort(`Tracker: not changed — ${requested} assets could not be installed. The manifest, ` +
248
+ `sentinel and conventions file are unchanged; the overlay is atomic per unit, so the ` +
249
+ `units that succeeded are converged and the ${overlay.overlayFailures.length} that failed ` +
250
+ `are reported above.`);
251
+ }
252
+ // 3. A conventions file inferred for the previous provider is stale the moment
253
+ // the provider changes — move it aside so it can never be silently
254
+ // authoritative, and so the reader-side mismatch guard has nothing to fight.
255
+ const transition = await io.renameStaleConventions(devflowDir, current.provider, requested);
256
+ if (transition.kind === 'renamed') {
257
+ messages.push({
258
+ level: 'info',
259
+ text: `Moved the previous ${transition.previous} conventions aside: ${transition.to}`,
260
+ });
261
+ }
262
+ else if (transition.kind === 'failed') {
263
+ messages.push({ level: 'warn', text: transition.error });
264
+ }
265
+ // 4. Persist. Everything after this point advertises the persisted value.
266
+ await io.syncManifest(devflowDir, { provider: requested });
267
+ // 5. The Tracker agent file.
268
+ const agentWarnings = [];
269
+ const artifacts = await io.convergeArtifacts(claudeDir, requested, (msg) => agentWarnings.push(msg));
270
+ for (const text of agentWarnings)
271
+ messages.push({ level: 'warn', text });
272
+ // 6 + 7. [DR-22] a selection change re-arms the attempt counter; [DR-10] the
273
+ // sentinel converges in both directions. A removal is always attempted, because
274
+ // a stale sentinel costs every future session a fork for a provider the user
275
+ // has left; the WRITE is gated on the agent being SPAWNABLE (design review C2).
276
+ //
277
+ // The gate is `agentPresent`, not `converged`. Reading `converged` alone is
278
+ // wrong in both directions: a re-copy that fails over an already-installed
279
+ // agent would disable a provider that still works, and merely SUPPRESSING the
280
+ // write leaves the previous provider's sentinel in place — so a jira → linear
281
+ // switch whose agent copy failed keeps advertising jira, which is the very
282
+ // state the suppression exists to prevent. Not-spawnable therefore REMOVES,
283
+ // through the one sentinel owner in src/core/tracker.ts (D-TRACKER-OWNER) —
284
+ // never an inline fs.rm here.
285
+ const rearm = await io.rearmInference(devflowDir);
286
+ if (!rearm.ok)
287
+ messages.push({ level: 'warn', text: rearm.error });
288
+ const advertisable = requested === DEFAULT_TRACKER_PROVIDER || artifacts.agentPresent;
289
+ const sentinel = await io.applySentinel(devflowDir, advertisable ? requested : DEFAULT_TRACKER_PROVIDER);
290
+ if (!sentinel.ok)
291
+ messages.push({ level: 'warn', text: sentinel.error });
292
+ if (!advertisable) {
293
+ messages.push({
294
+ level: 'warn',
295
+ text: `Tracker sentinel removed — no ${requested} agent is installed, so nothing advertises a ` +
296
+ `provider whose agent is missing and no session will try to spawn it. ` +
297
+ `Re-run devflow tracker --set ${requested}.`,
298
+ });
299
+ }
300
+ const removed = overlay.pruned.removed.length;
301
+ const agentNote = artifacts.agent === 'unchanged' ? '' : `, tracker agent ${artifacts.agent}`;
302
+ const moved = overlay.overlaidRefs.length > 0 || removed > 0 || agentNote !== '';
303
+ messages.push({
304
+ level: requested === current.provider ? 'info' : 'success',
305
+ text: moved
306
+ ? `Tracker: ${requested} — ${overlay.overlaidRefs.length} installed, ${removed} removed${agentNote}`
307
+ : `Tracker: ${requested} (unchanged)`,
308
+ });
309
+ return { exitCode: 0, provider: requested, messages };
310
+ }
311
+ export const trackerCommand = new Command('tracker')
312
+ .description('Show or set the issue tracker provider')
313
+ .option('--status', 'Show the selected provider and the learned conventions file, and re-arm background inference')
314
+ .option('--set <id>', 'Set the issue tracker provider: github, jira, or linear')
315
+ .action(async (options) => {
316
+ const devflowDir = getDevFlowDirectory();
317
+ const hasFlag = options.status || options.set !== undefined;
318
+ if (!hasFlag) {
319
+ p.intro(color.bgCyan(color.white(' Tracker ')));
320
+ const validIds = TRACKER_PROVIDERS.map(t => `${t.id} — ${t.hint}`).join('\n ');
321
+ p.note(`${color.cyan('devflow tracker --status')} Show the provider and learned conventions\n` +
322
+ `${color.cyan('devflow tracker --set <id>')} Select the issue tracker provider\n\n` +
323
+ `Valid provider IDs:\n ${validIds}`, 'Usage');
324
+ p.outro(color.dim('github is the default — devflow tracker --set github turns the rest off'));
325
+ return;
326
+ }
327
+ // Validate --set input before any I/O (parse-don't-validate at the boundary).
328
+ let setProvider;
329
+ if (options.set !== undefined) {
330
+ const parsed = parseTrackerId(options.set);
331
+ if (!parsed.ok) {
332
+ p.log.error(parsed.error);
333
+ process.exit(1);
334
+ }
335
+ setProvider = parsed.value;
336
+ }
337
+ const manifest = await readManifest(devflowDir);
338
+ if (!manifest) {
339
+ p.log.error('No manifest found. Run devflow init first.');
340
+ process.exit(1);
341
+ }
342
+ const current = manifest.features.tracker;
343
+ // ── Status ─────────────────────────────────────────────────────────────────
344
+ // --status wins when both flags are passed, mirroring `devflow compliance`.
345
+ if (options.status) {
346
+ const provenance = await readTrackerProvenance(devflowDir);
347
+ const mechanics = await readTrackerMechanics(getClaudeDirectory(), current.provider);
348
+ // [D-F] Inspecting the status re-arms the attempt counter. --status is
349
+ // the command a capped user reaches for to find out why nothing is being
350
+ // learned, so it is the command that has to hand back another five
351
+ // tries; the alternative leaves the only escape a hand deletion of an
352
+ // undocumented dotfile. Every other --status in this CLI is a pure read,
353
+ // so this one reports the write it makes as a line of the note below — a
354
+ // machine-state change the output does not mention is a change the user
355
+ // cannot audit. Non-fatal exactly as on the --set path (avoids PF-009): a
356
+ // failed re-arm warns, it never aborts the report the user asked for.
357
+ const statusRearm = await rearmTrackerInference(devflowDir);
358
+ const inference = statusRearm.ok
359
+ ? `re-armed (${TRACKER_ATTEMPTS_MAX} attempts available)`
360
+ : 're-arm failed — see the warning below';
361
+ const providerLabel = current.provider === DEFAULT_TRACKER_PROVIDER
362
+ ? `${color.green(current.provider)} ${color.dim('(default)')}`
363
+ : color.green(current.provider);
364
+ p.note([
365
+ `Provider: ${providerLabel}`,
366
+ `Conventions: ${formatTrackerProvenance(provenance)}`,
367
+ `File: ${trackerConventionsPath(devflowDir)}`,
368
+ `Mechanics: ${formatTrackerMechanics(mechanics)}`,
369
+ `Inference: ${inference}`,
370
+ ].join('\n'), 'Tracker Status');
371
+ if (!statusRearm.ok)
372
+ p.log.warn(statusRearm.error);
373
+ return;
374
+ }
375
+ // ── Set ────────────────────────────────────────────────────────────────────
376
+ const outcome = await runTrackerSet({
377
+ devflowDir,
378
+ claudeDir: getClaudeDirectory(),
379
+ current,
380
+ requested: setProvider ?? current.provider,
381
+ io: buildTrackerSetIO(),
382
+ });
383
+ for (const msg of outcome.messages) {
384
+ switch (msg.level) {
385
+ case 'success':
386
+ p.log.success(msg.text);
387
+ break;
388
+ case 'warn':
389
+ p.log.warn(msg.text);
390
+ break;
391
+ case 'error':
392
+ p.log.error(msg.text);
393
+ break;
394
+ default:
395
+ p.log.info(msg.text);
396
+ break;
397
+ }
398
+ }
399
+ if (outcome.exitCode !== 0)
400
+ process.exit(outcome.exitCode);
401
+ if (outcome.provider !== DEFAULT_TRACKER_PROVIDER) {
402
+ p.log.info(color.dim('Your issue conventions are learned in the background at the next session start'));
403
+ }
404
+ });
405
+ //# sourceMappingURL=tracker.js.map