devflow-kit 2.5.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 (158) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +44 -19
  3. package/dist/agents/git.md +13 -15
  4. package/dist/cli/commands/ambient.js +160 -145
  5. package/dist/cli/commands/capture.js +29 -55
  6. package/dist/cli/commands/compliance.js +32 -61
  7. package/dist/cli/commands/context.js +17 -32
  8. package/dist/cli/commands/debug.js +65 -26
  9. package/dist/cli/commands/flags.js +3 -3
  10. package/dist/cli/commands/hud.js +34 -10
  11. package/dist/cli/commands/init-seed.js +40 -4
  12. package/dist/cli/commands/init.js +249 -271
  13. package/dist/cli/commands/install-report.js +10 -15
  14. package/dist/cli/commands/knowledge/index.js +1 -1
  15. package/dist/cli/commands/knowledge/toggle.js +11 -3
  16. package/dist/cli/commands/learning.js +52 -37
  17. package/dist/cli/commands/legacy-hooks.js +11 -14
  18. package/dist/cli/commands/memory.js +67 -78
  19. package/dist/cli/commands/proxy.js +23 -41
  20. package/dist/cli/commands/security.js +5 -13
  21. package/dist/cli/commands/skills.js +21 -3
  22. package/dist/cli/commands/tracker.js +100 -228
  23. package/dist/cli/commands/uninstall.js +343 -138
  24. package/dist/commands/bug-analysis.md +38 -12
  25. package/dist/commands/code-review.md +70 -21
  26. package/dist/commands/debug.md +37 -7
  27. package/dist/commands/dynamic-build.md +66 -17
  28. package/dist/commands/dynamic-plan.md +19 -8
  29. package/dist/commands/dynamic-profile.md +24 -10
  30. package/dist/commands/dynamic-tickets.md +22 -11
  31. package/dist/commands/explore.md +37 -7
  32. package/dist/commands/implement.md +96 -32
  33. package/dist/commands/plan.md +62 -19
  34. package/dist/commands/release.md +2 -2
  35. package/dist/commands/research.md +34 -8
  36. package/dist/commands/resolve.md +65 -17
  37. package/dist/commands/self-review.md +45 -9
  38. package/dist/core/compliance-compose.js +27 -27
  39. package/dist/core/evidence-policy.js +240 -24
  40. package/dist/core/feature-config.js +94 -25
  41. package/dist/core/feature-switch.js +1 -1
  42. package/dist/core/flags.js +30 -2
  43. package/dist/core/fs-atomic.js +27 -0
  44. package/dist/core/hook-log-dirs.js +104 -0
  45. package/dist/core/learning-tuning-config.js +5 -3
  46. package/dist/core/ledger-root.js +102 -0
  47. package/dist/core/manifest.js +6 -4
  48. package/dist/core/mds-variants.js +34 -97
  49. package/dist/core/migrations.js +49 -23
  50. package/dist/core/plugins.js +5 -4
  51. package/dist/core/project-paths.js +0 -17
  52. package/dist/core/same-location.js +25 -0
  53. package/dist/core/tracker.js +226 -139
  54. package/dist/hud/components/config-counts.js +15 -4
  55. package/dist/hud/components/learning-counts.js +14 -0
  56. package/dist/hud/config.js +2 -1
  57. package/dist/hud/cost-history.js +2 -4
  58. package/dist/hud/git.js +52 -7
  59. package/dist/hud/index.js +7 -9
  60. package/dist/skills/git/references/pr/check-merge-readiness.md +1 -1
  61. package/dist/skills/git/references/pr/ensure-pr-ready.md +1 -1
  62. package/dist/skills/git/references/pr/update-pr-evidence.md +1 -1
  63. package/dist/skills/git/references/tracker/_mcp.md +1 -1
  64. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +1 -1
  65. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +1 -1
  66. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +2 -2
  67. package/dist/skills/git/references/tracker/github/manage-debt.md +3 -3
  68. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +1 -1
  69. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +1 -1
  70. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +2 -2
  71. package/dist/skills/git/references/tracker/jira/manage-debt.md +1 -1
  72. package/dist/skills/git/references/tracker/jira/post-wave-report.md +1 -1
  73. package/dist/skills/git/references/tracker/jira/setup-task.md +1 -1
  74. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +1 -1
  75. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +1 -1
  76. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +2 -2
  77. package/dist/skills/git/references/tracker/linear/manage-debt.md +1 -1
  78. package/dist/skills/git/references/tracker/linear/post-wave-report.md +1 -1
  79. package/dist/skills/git/references/tracker/linear/setup-task.md +1 -1
  80. package/dist/targets/claude-code/claude-paths.js +59 -57
  81. package/dist/targets/claude-code/compliance-install.js +49 -65
  82. package/dist/targets/claude-code/hooks.js +108 -3
  83. package/dist/targets/claude-code/installer.js +30 -57
  84. package/dist/targets/claude-code/post-install.js +232 -139
  85. package/dist/targets/claude-code/tracker-install.js +38 -65
  86. package/package.json +5 -4
  87. package/src/assets/agents/code.md +4 -3
  88. package/src/assets/agents/design.md +1 -0
  89. package/src/assets/agents/git.mds +55 -57
  90. package/src/assets/agents/knowledge.md +2 -2
  91. package/src/assets/agents/review.md +3 -1
  92. package/src/assets/agents/tracker.md +37 -30
  93. package/src/assets/commands/_partials/_compliance.mds +19 -1
  94. package/src/assets/commands/_partials/_decisions.mds +15 -3
  95. package/src/assets/commands/_partials/_docs_root.mds +35 -0
  96. package/src/assets/commands/_partials/_engine.mds +2 -2
  97. package/src/assets/commands/_partials/_evidence_policy.mds +3 -3
  98. package/src/assets/commands/_partials/_factory.mds +1 -1
  99. package/src/assets/commands/_partials/_knowledge.mds +27 -9
  100. package/src/assets/commands/_partials/_plan_contract.mds +2 -2
  101. package/src/assets/commands/_partials/_preamble.mds +1 -1
  102. package/src/assets/commands/_partials/_publication.mds +6 -2
  103. package/src/assets/commands/_partials/_settings.mds +28 -0
  104. package/src/assets/commands/_partials/_ticket_template.mds +3 -3
  105. package/src/assets/commands/_partials/_tracker.mds +4 -4
  106. package/src/assets/commands/_partials/_wave.mds +4 -4
  107. package/src/assets/commands/bug-analysis.mds +19 -17
  108. package/src/assets/commands/code-review.mds +39 -33
  109. package/src/assets/commands/debug.mds +4 -5
  110. package/src/assets/commands/dynamic-build.mds +75 -53
  111. package/src/assets/commands/dynamic-plan.mds +20 -15
  112. package/src/assets/commands/dynamic-profile.mds +24 -11
  113. package/src/assets/commands/dynamic-tickets.mds +25 -20
  114. package/src/assets/commands/explore.mds +4 -5
  115. package/src/assets/commands/implement.mds +58 -45
  116. package/src/assets/commands/plan.mds +34 -29
  117. package/src/assets/commands/release.md +2 -2
  118. package/src/assets/commands/research.mds +11 -9
  119. package/src/assets/commands/resolve.mds +41 -39
  120. package/src/assets/commands/self-review.mds +24 -25
  121. package/src/assets/mds/git/_pr.mds +61 -61
  122. package/src/assets/mds/git/_references.mds +19 -19
  123. package/src/assets/mds/tracker/_common.mds +8 -8
  124. package/src/assets/mds/tracker/_github.mds +71 -71
  125. package/src/assets/mds/tracker/_jira.mds +74 -74
  126. package/src/assets/mds/tracker/_linear.mds +75 -75
  127. package/src/assets/mds/tracker/_mcp.mds +23 -17
  128. package/src/assets/scripts/hooks/background-memory-update +35 -19
  129. package/src/assets/scripts/hooks/capture-prompt +18 -12
  130. package/src/assets/scripts/hooks/capture-question +18 -12
  131. package/src/assets/scripts/hooks/capture-turn +27 -17
  132. package/src/assets/scripts/hooks/debug-trace +11 -6
  133. package/src/assets/scripts/hooks/ensure-devflow-init +33 -6
  134. package/src/assets/scripts/hooks/ensure-proxy +9 -8
  135. package/src/assets/scripts/hooks/ensure-root-gitignore +111 -36
  136. package/src/assets/scripts/hooks/git-marker +48 -0
  137. package/src/assets/scripts/hooks/json-helper.cjs +6 -1
  138. package/src/assets/scripts/hooks/lib/project-paths.cjs +0 -19
  139. package/src/assets/scripts/hooks/log-paths +80 -0
  140. package/src/assets/scripts/hooks/memory-worker +17 -15
  141. package/src/assets/scripts/hooks/pre-compact-memory +41 -16
  142. package/src/assets/scripts/hooks/queue-append +104 -30
  143. package/src/assets/scripts/hooks/resolve-project-root +101 -7
  144. package/src/assets/scripts/hooks/session-start-context +289 -122
  145. package/src/assets/scripts/hooks/session-start-memory +35 -16
  146. package/src/assets/scripts/lib/project-config.cjs +633 -0
  147. package/src/assets/scripts/resolve-evidence-policy.cjs +300 -220
  148. package/src/assets/scripts/resolve-settings.cjs +1054 -0
  149. package/src/assets/scripts/verify-evidence.cjs +1 -1
  150. package/src/assets/skills/compliance/SKILL.md +2 -2
  151. package/src/assets/skills/docs-framework/SKILL.md +6 -7
  152. package/src/assets/skills/docs-framework/references/patterns.md +10 -17
  153. package/src/assets/skills/gap-analysis/SKILL.md +2 -2
  154. package/src/assets/skills/git/references/github-api.md +9 -9
  155. package/src/assets/skills/git/references/patterns.md +1 -1
  156. package/src/assets/skills/worktree-support/SKILL.md +1 -1
  157. package/src/assets/skills/worktree-support/references/roots.md +29 -0
  158. package/src/targets/claude-code/templates/managed-settings.json +25 -9
@@ -1,5 +1,5 @@
1
1
  /**
2
- * devflow tracker — Show or set the issue tracker provider.
2
+ * devflow tracker — Show or set the machine's issue tracker provider.
3
3
  *
4
4
  * D-TRACKER-PAIR [DR-25]: `src/core/tracker.ts` (domain) +
5
5
  * `src/cli/commands/tracker.ts` (CLI) mirrors the `compliance.ts` pair
@@ -9,103 +9,55 @@
9
9
  *
10
10
  * Applies ADR-013: CLI-layer module; the provider domain, the strict parser and
11
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.
12
+ * The manifest holds the MACHINE default. A repository may select its own
13
+ * provider in its committed `.devflow/project.json` (resolved by
14
+ * resolve-settings.cjs); `--status` names it on an `Effective:` line when one
15
+ * does, and `--set` never touches it.
15
16
  * Avoids PF-015: --set converges the sentinel in BOTH directions, so flipping
16
17
  * back to github removes what flipping away wrote.
17
- * Avoids PF-009: a failed rename/rearm/sentinel step warns, it never aborts.
18
+ * Avoids PF-009: a failed re-arm or sentinel step warns, it never aborts.
18
19
  */
19
20
  import { Command } from 'commander';
20
21
  import { promises as fs } from 'fs';
21
22
  import * as path from 'path';
22
23
  import * as p from '@clack/prompts';
23
24
  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 { DEFAULT_TRACKER_PROVIDER, TRACKER_ATTEMPTS_MAX, TRACKER_CONVENTIONS_READ_BYTES, TRACKER_PROVIDERS, applyTrackerSentinel, describeTrackerValue, parseTrackerFrontmatter, parseTrackerId, readBoundedHead, rearmTrackerInference, trackerConventionsPath, } from '../../core/tracker.js';
25
26
  import { readManifest, syncManifestFeature } from '../../core/manifest.js';
27
+ import { loadSettingsModule, personalConfigTrackedWarning, repoTrackerSelection } from '../../core/evidence-policy.js';
26
28
  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
29
  import { SKILL_REFS_SKILL_NAME, installedReferenceManifest } from '../../core/mds-variants.js';
31
30
  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
31
  /**
35
- * How many leading BYTES of tracker.md are read.
32
+ * Read the provenance header of `~/.devflow/tracker/{provider}.md`.
36
33
  *
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`.
34
+ * Never throws (PF-014): an absent, unreadable, or non-regular path is `absent`.
70
35
  * 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.
36
+ * block is scanned — through the one frontmatter parser the migration also uses —
37
+ * and nothing read here is trusted: every value is rendered through
38
+ * `describeTrackerValue`, because the file is hand-editable and machine-wide, so
39
+ * its content is third-party input at every sink.
74
40
  */
75
- export async function readTrackerProvenance(devflowDir) {
76
- const head = await readBoundedHead(trackerConventionsPath(devflowDir), PROVENANCE_READ_BYTES);
41
+ export async function readTrackerProvenance(devflowDir, provider) {
42
+ const head = await readBoundedHead(trackerConventionsPath(devflowDir, provider), TRACKER_CONVENTIONS_READ_BYTES);
77
43
  if (head === undefined)
78
44
  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 };
45
+ const { provider: named, inferredFrom } = parseTrackerFrontmatter(head);
46
+ return { kind: 'present', provider: named, inferredFrom };
96
47
  }
97
48
  /**
98
- * Count the installed tracker mechanics for the resolved provider.
49
+ * Count the installed tracker mechanics.
99
50
  *
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).
51
+ * Counts what is PRESENT against the install manifest — every provider's, since
52
+ * every install carries them all (D-INSTALL-ALL-PROVIDERS) — rather than listing
53
+ * the directory: the question a user asks `--status` is "can the agent load what
54
+ * it is told to load?", and a stray file in the tree is not an answer to it.
55
+ * Never throws (PF-014).
104
56
  */
105
- export async function readTrackerMechanics(claudeDir, provider) {
57
+ export async function readTrackerMechanics(claudeDir) {
106
58
  const root = path.join(claudeDir, 'skills', prefixSkillName(SKILL_REFS_SKILL_NAME), 'references');
107
59
  let present = 0;
108
- for (const rel of installedReferenceManifest({ provider })) {
60
+ for (const rel of installedReferenceManifest()) {
109
61
  try {
110
62
  await fs.access(path.join(root, ...rel.split('/')));
111
63
  present++;
@@ -150,168 +102,88 @@ export function formatTrackerProvenance(provenance) {
150
102
  }
151
103
  return parts.join(' — ');
152
104
  }
105
+ /** Read the conventions `--status` reports for `provider`. Never throws: see readTrackerProvenance. */
106
+ export async function readTrackerStatusConventions(devflowDir, provider) {
107
+ if (provider === DEFAULT_TRACKER_PROVIDER)
108
+ return { kind: 'none' };
109
+ return {
110
+ kind: 'learned',
111
+ file: trackerConventionsPath(devflowDir, provider),
112
+ provenance: await readTrackerProvenance(devflowDir, provider),
113
+ };
114
+ }
115
+ /**
116
+ * The `--status` note's lines. Pure.
117
+ *
118
+ * The `Effective:` line appears ONLY when a repository layer selects the tracker
119
+ * (`selection` non-null) — `Effective: jira (project)` — so a machine whose own
120
+ * provider decides prints the same five labels it always has. The conventions
121
+ * lines describe the provider in effect HERE, which is the one whose file a
122
+ * session in this repository learns and a Git spawn in it reads.
123
+ */
124
+ export function formatTrackerStatus(input) {
125
+ const providerLabel = input.machine === DEFAULT_TRACKER_PROVIDER
126
+ ? `${color.green(input.machine)} ${color.dim('(default)')}`
127
+ : color.green(input.machine);
128
+ const lines = [`Provider: ${providerLabel}`];
129
+ if (input.selection !== null) {
130
+ lines.push(`Effective: ${color.green(input.selection.provider)} (${input.selection.source})`);
131
+ }
132
+ lines.push(...(input.conventions.kind === 'none'
133
+ ? ['Conventions: none (GitHub needs no learned conventions)', 'File: none']
134
+ : [`Conventions: ${formatTrackerProvenance(input.conventions.provenance)}`, `File: ${input.conventions.file}`]), `Mechanics: ${formatTrackerMechanics(input.mechanics)}`, `Inference: ${input.inference}`);
135
+ return lines.join('\n');
136
+ }
153
137
  /** The real adapter — the ONE binding of each operation into the `--set` path. */
154
138
  export function buildTrackerSetIO() {
155
139
  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
140
  syncManifest: (devflowDir, state) => syncManifestFeature(devflowDir, 'tracker', state),
168
- convergeArtifacts: (claudeDir, provider, warn) => convergeTrackerArtifacts({ claudeDir, provider, warn }),
169
141
  rearmInference: rearmTrackerInference,
170
142
  applySentinel: applyTrackerSentinel,
171
143
  };
172
144
  }
173
145
  /**
174
- * Converge every tracker artifact onto a newly selected provider.
146
+ * Select the machine's default tracker provider.
175
147
  *
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.
148
+ * D-TRACKER-CONVERGE-SET: `--set` writes exactly three things, in this order —
180
149
  *
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)
150
+ * 1. the manifest (the machine default; what every other step names)
151
+ * 2. the attempt counters (re-armed, so the new provider gets its five tries)
152
+ * 3. the sentinel (the provider's name, or removed for github)
188
153
  *
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.
154
+ * and nothing else. Every provider's mechanics and the Tracker agent are already
155
+ * installed (D-INSTALL-ALL-PROVIDERS), so a selection change has no reference tree
156
+ * to swap and no agent to add or remove; and each provider keeps its own
157
+ * conventions file (D-TRACKER-PER-PROVIDER-CONVENTIONS), so there is nothing to
158
+ * move aside. The sentinel follows the manifest because it ADVERTISES the value
159
+ * the manifest records, and must never get ahead of it.
196
160
  *
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.
161
+ * Never throws, except where the manifest writer does. The re-arm and sentinel
162
+ * steps warn rather than abort: each degrades a working install, never breaks it.
210
163
  */
211
164
  export async function runTrackerSet(opts) {
212
- const { devflowDir, claudeDir, current, requested, io } = opts;
165
+ const { devflowDir, current, requested, io } = opts;
213
166
  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
167
  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.
168
+ // [DR-22] a selection change re-arms the attempt counters; [DR-10] the
169
+ // sentinel converges in both directions.
285
170
  const rearm = await io.rearmInference(devflowDir);
286
171
  if (!rearm.ok)
287
172
  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);
173
+ const sentinel = await io.applySentinel(devflowDir, requested);
290
174
  if (!sentinel.ok)
291
175
  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 !== '';
176
+ const unchanged = requested === current.provider;
303
177
  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)`,
178
+ level: unchanged ? 'info' : 'success',
179
+ text: unchanged ? `Tracker: ${requested} (unchanged)` : `Tracker: ${requested}`,
308
180
  });
309
- return { exitCode: 0, provider: requested, messages };
181
+ return { provider: requested, messages };
310
182
  }
311
183
  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')
184
+ .description('Show or set the machine\'s issue tracker provider')
185
+ .option('--status', 'Show the provider in effect and its learned conventions file, and re-arm background inference')
186
+ .option('--set <id>', 'Set the machine\'s default issue tracker provider: github, jira, or linear')
315
187
  .action(async (options) => {
316
188
  const devflowDir = getDevFlowDirectory();
317
189
  const hasFlag = options.status || options.set !== undefined;
@@ -319,9 +191,9 @@ export const trackerCommand = new Command('tracker')
319
191
  p.intro(color.bgCyan(color.white(' Tracker ')));
320
192
  const validIds = TRACKER_PROVIDERS.map(t => `${t.id} — ${t.hint}`).join('\n ');
321
193
  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` +
194
+ `${color.cyan('devflow tracker --set <id>')} Set the machine's default provider\n\n` +
323
195
  `Valid provider IDs:\n ${validIds}`, 'Usage');
324
- p.outro(color.dim('github is the default — devflow tracker --set github turns the rest off'));
196
+ p.outro(color.dim('github is the default — devflow tracker --set github returns to it'));
325
197
  return;
326
198
  }
327
199
  // Validate --set input before any I/O (parse-don't-validate at the boundary).
@@ -343,9 +215,12 @@ export const trackerCommand = new Command('tracker')
343
215
  // ── Status ─────────────────────────────────────────────────────────────────
344
216
  // --status wins when both flags are passed, mirroring `devflow compliance`.
345
217
  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
218
+ const settingsModule = loadSettingsModule();
219
+ const selection = repoTrackerSelection(settingsModule, { dir: process.cwd() });
220
+ const effective = selection?.provider ?? current.provider;
221
+ const conventions = await readTrackerStatusConventions(devflowDir, effective);
222
+ const mechanics = await readTrackerMechanics(getClaudeDirectory());
223
+ // [D-F] Inspecting the status re-arms the attempt counters. --status is
349
224
  // the command a capped user reaches for to find out why nothing is being
350
225
  // learned, so it is the command that has to hand back another five
351
226
  // tries; the alternative leaves the only escape a hand deletion of an
@@ -358,24 +233,23 @@ export const trackerCommand = new Command('tracker')
358
233
  const inference = statusRearm.ok
359
234
  ? `re-armed (${TRACKER_ATTEMPTS_MAX} attempts available)`
360
235
  : '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');
236
+ p.note(formatTrackerStatus({
237
+ machine: current.provider,
238
+ selection,
239
+ conventions,
240
+ mechanics,
241
+ inference,
242
+ }), 'Tracker Status');
371
243
  if (!statusRearm.ok)
372
244
  p.log.warn(statusRearm.error);
245
+ const trackedWarning = personalConfigTrackedWarning(settingsModule, { dir: process.cwd() });
246
+ if (trackedWarning !== null)
247
+ p.log.warn(trackedWarning);
373
248
  return;
374
249
  }
375
250
  // ── Set ────────────────────────────────────────────────────────────────────
376
251
  const outcome = await runTrackerSet({
377
252
  devflowDir,
378
- claudeDir: getClaudeDirectory(),
379
253
  current,
380
254
  requested: setProvider ?? current.provider,
381
255
  io: buildTrackerSetIO(),
@@ -396,8 +270,6 @@ export const trackerCommand = new Command('tracker')
396
270
  break;
397
271
  }
398
272
  }
399
- if (outcome.exitCode !== 0)
400
- process.exit(outcome.exitCode);
401
273
  if (outcome.provider !== DEFAULT_TRACKER_PROVIDER) {
402
274
  p.log.info(color.dim('Your issue conventions are learned in the background at the next session start'));
403
275
  }