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
@@ -7,6 +7,11 @@
7
7
  * Users who skip major versions should run uninstall + reinstall.
8
8
  *
9
9
  * Organized by era to make scanning for duplicates tractable.
10
+ *
11
+ * These lists are FROZEN deletion manifests for names that once shipped: the
12
+ * `*_V2` suffix is the frozen spelling of an era, not a version to bump, and a
13
+ * name is removed from a list only when its pruning window has passed — never
14
+ * renamed to match a current registry name (avoids PF-012).
10
15
  */
11
16
  /** Pre-v1.0.0: devflow- prefixed skill names from the original install scheme. */
12
17
  const LEGACY_SKILLS_PRE_V1 = [
@@ -1,5 +1,5 @@
1
1
  import { promises as fs, writeFileSync, unlinkSync } from 'fs';
2
- import { execSync } from 'child_process';
2
+ import { execFileSync } from 'child_process';
3
3
  import * as path from 'path';
4
4
  import * as os from 'os';
5
5
  import * as p from '@clack/prompts';
@@ -25,6 +25,28 @@ export function computeGitignoreAppend(existingContent, entries) {
25
25
  const existingLines = existingContent.split('\n').map(l => l.trim());
26
26
  return entries.filter(entry => !existingLines.includes(entry));
27
27
  }
28
+ /**
29
+ * Sentinel line whose presence means the current (v3-and-later) carve-out block is
30
+ * installed. Devflow-unique: no user writes `!.devflow/conventions.md` by hand.
31
+ */
32
+ const DEVFLOW_GITIGNORE_SENTINEL_V3 = '!.devflow/conventions.md';
33
+ /** Sentinel line whose presence means the v2 carve-out block is installed (no conventions.md line). */
34
+ const DEVFLOW_GITIGNORE_SENTINEL_V2 = '!.devflow/features/*/KNOWLEDGE.md';
35
+ /**
36
+ * The block's final line: ignore the devflow-managed `.claudeignore` file.
37
+ * NOT a sentinel — users legitimately author this line themselves.
38
+ */
39
+ const CLAUDEIGNORE_LINE = '.claudeignore';
40
+ /** A user's explicit un-ignore of `.claudeignore`; never overridden. */
41
+ const CLAUDEIGNORE_NEGATION = '!.claudeignore';
42
+ /**
43
+ * Re-includes the team-owned evidence policy file (D-GITIGNORE-V5). A COMPLETION
44
+ * line, never a presence sentinel: users may author it themselves, so its presence
45
+ * proves nothing about the devflow block (avoids PF-059). It sits after `.devflow/*`
46
+ * (which it overrides under last-match-wins) and before `.claudeignore`, so the
47
+ * block's final line stays `.claudeignore`.
48
+ */
49
+ const DEVFLOW_POLICY_LINE = '!.devflow/policy.json';
28
50
  /**
29
51
  * The shared .devflow/ gitignore block. Everything under .devflow/ is local
30
52
  * (memory, learning, docs, locks) EXCEPT:
@@ -32,6 +54,9 @@ export function computeGitignoreAppend(existingContent, entries) {
32
54
  * + committed (the Knowledge agent commits them at workflow end).
33
55
  * - conventions.md: naming-convention authority written by the Git learn-conventions
34
56
  * operation; GIT-TRACKED so the team shares a single naming source.
57
+ * - policy.json: the team-owned evidence policy, read from the default branch by
58
+ * resolve-evidence-policy.cjs; GIT-TRACKED so a team can commit it without `git add -f`.
59
+ * Devflow never writes it.
35
60
  *
36
61
  * Re-including files under an ignored tree needs a `dir/*` + `!dir/keep` pair at
37
62
  * each level — a bare `.devflow/` excludes the directory so git never descends and
@@ -39,65 +64,121 @@ export function computeGitignoreAppend(existingContent, entries) {
39
64
  *
40
65
  * Kept BYTE-IDENTICAL to the block emitted by src/assets/scripts/hooks/ensure-root-gitignore
41
66
  * so the init-time path and the always-on hook path produce the same file.
42
- *
43
- * D-GITIGNORE-V3: v3 of the carve-out block (adds !.devflow/conventions.md).
44
67
  */
45
- export const DEVFLOW_GITIGNORE_BLOCK = [
68
+ const DEVFLOW_GITIGNORE_BLOCK_LINES = [
46
69
  '# Devflow runtime data — local by default (memory, learning, docs, locks).',
47
- '# Two exceptions are shared via git: feature knowledge bases under .devflow/features/',
48
- '# (index.md and every {slug}/KNOWLEDGE.md) and .devflow/conventions.md (naming',
49
- '# authority). To stop sharing, re-add `.devflow/features/` or `.devflow/conventions.md`',
50
- '# to your own .gitignore.',
70
+ '# Shared via git: feature knowledge bases under .devflow/features/ (index.md and',
71
+ '# every {slug}/KNOWLEDGE.md), .devflow/conventions.md (naming authority) and',
72
+ '# .devflow/policy.json (evidence policy). To stop sharing the first two, re-add',
73
+ '# `.devflow/features/` or `.devflow/conventions.md` to your own .gitignore.',
51
74
  '.devflow/*',
52
75
  '!.devflow/features/',
53
76
  '.devflow/features/*',
54
77
  '!.devflow/features/index.md',
55
78
  '!.devflow/features/*/',
56
79
  '.devflow/features/*/*',
57
- '!.devflow/features/*/KNOWLEDGE.md',
58
- '!.devflow/conventions.md',
59
- ].join('\n');
60
- /** Sentinel line whose presence means the v3 carve-out block is installed. */
61
- const DEVFLOW_GITIGNORE_SENTINEL_V3 = '!.devflow/conventions.md';
62
- /** Sentinel line whose presence means the v2 carve-out block is installed (no conventions.md line). */
63
- const DEVFLOW_GITIGNORE_SENTINEL_V2 = '!.devflow/features/*/KNOWLEDGE.md';
80
+ DEVFLOW_GITIGNORE_SENTINEL_V2,
81
+ DEVFLOW_GITIGNORE_SENTINEL_V3,
82
+ DEVFLOW_POLICY_LINE,
83
+ CLAUDEIGNORE_LINE,
84
+ ];
85
+ /** The full carve-out block, `.claudeignore` line included. */
86
+ export const DEVFLOW_GITIGNORE_BLOCK = DEVFLOW_GITIGNORE_BLOCK_LINES.join('\n');
87
+ /**
88
+ * The carve-out block without its final `.claudeignore` line — emitted instead of
89
+ * the full block when the target .gitignore already carries a `.claudeignore` or
90
+ * `!.claudeignore` entry of the user's own.
91
+ */
92
+ export const DEVFLOW_GITIGNORE_BLOCK_WITHOUT_CLAUDEIGNORE = DEVFLOW_GITIGNORE_BLOCK_LINES.slice(0, -1).join('\n');
64
93
  /** The legacy wholesale comment our pre-carve-out writers emitted. */
65
94
  const LEGACY_DEVFLOW_COMMENT = '# Devflow runtime data (local by default; remove to share via git)';
66
95
  /**
67
96
  * PURE: given existing .gitignore content, return the content that ignores
68
- * `.devflow/` with the feature-knowledge + conventions.md carve-out — or `null`
69
- * when no change is needed. Idempotent: feeding its own output back returns `null`.
97
+ * `.devflow/` with the feature-knowledge + conventions.md + policy.json carve-out —
98
+ * or `null` when no change is needed. Idempotent: feeding its own output back returns `null`.
99
+ *
100
+ * D-GITIGNORE-V5: the block is detected ONLY by its own devflow-unique sentinel
101
+ * (`!.devflow/conventions.md`). `.claudeignore` and `!.devflow/policy.json` are
102
+ * COMPLETION lines — users legitimately author both themselves — so each is topped up
103
+ * when missing and never read as proof the block exists. A presence check on a
104
+ * user-authored line inverts both halves of the contract: projects that already carry
105
+ * that line are told the block is installed when it is not, and a user's
106
+ * `!.claudeignore` un-ignore is silently reversed by re-appending `.claudeignore`
107
+ * under last-match-wins (avoids PF-059).
108
+ *
109
+ * `hasClaudeignoreEntry` is true when some whole line, trimmed, is exactly
110
+ * `.claudeignore` OR `!.claudeignore`. Treating both forms as "present" both honours
111
+ * an un-ignore and makes every branch converge on re-run. `hasPolicyLine` is true when
112
+ * some whole line, trimmed, is exactly `!.devflow/policy.json`. The missing completion
113
+ * lines, in block order, are [policy line, `.claudeignore` (only when
114
+ * `!hasClaudeignoreEntry`)].
70
115
  *
71
- * - v3 sentinel already present → `null` (already at current format).
72
- * - User-authored `/.devflow/` (leading slash) present → `null` (respect manual config).
73
- * - v2 sentinel present but not v3 → UPGRADE: append just `!.devflow/conventions.md`.
74
- * - Legacy bare `.devflow/` present → strip it (+ our old comment), append the full block.
75
- * - Otherwise → append the full block (or the block alone when content is empty).
116
+ * 1. A `/.devflow/` line present → `null` (user opt-out; respect manual config).
117
+ * 2. v3 sentinel present → append the missing completion lines; `null` when none are
118
+ * missing. This is the v4→v5 upgrade: a v4 block gains only the policy line, after
119
+ * its `.claudeignore` line, and keeps its old comment.
120
+ * 3. v2 sentinel present, no v3 → append `!.devflow/conventions.md` followed by the
121
+ * missing completion lines.
122
+ * 4. Legacy bare `.devflow/` present → strip it (+ our old comment), then append the
123
+ * block; no block at all → append the block. The block is emitted MINUS its final
124
+ * `.claudeignore` line when `hasClaudeignoreEntry`. A user's own policy line is
125
+ * duplicated harmlessly here, and the re-run is a no-op.
126
+ * 5. Neither completion line is ever a sentinel. The marker file
127
+ * (`.devflow/.root-gitignore-configured-v5`) is a fast-path claim, never proof.
128
+ *
129
+ * Line matching is whole-line, whitespace-tolerant, exact text — never substring.
130
+ * Both append forms are mirrored byte-for-byte in the shell twin
131
+ * (src/assets/scripts/hooks/ensure-root-gitignore), which is what the cross-implementation
132
+ * parity table in tests/shell-hooks.test.ts pins.
76
133
  */
77
134
  export function computeDevflowGitignore(existingContent) {
78
135
  const lines = existingContent.split('\n');
79
136
  const trimmed = lines.map(l => l.trim());
80
- if (trimmed.includes(DEVFLOW_GITIGNORE_SENTINEL_V3))
81
- return null;
82
- if (trimmed.some(l => l === '/.devflow/'))
137
+ const hasClaudeignoreEntry = trimmed.some(l => l === CLAUDEIGNORE_LINE || l === CLAUDEIGNORE_NEGATION);
138
+ const hasPolicyLine = trimmed.includes(DEVFLOW_POLICY_LINE);
139
+ /** The block-completing lines this file lacks, in block order. */
140
+ const missingCompletionLines = [
141
+ ...(hasPolicyLine ? [] : [DEVFLOW_POLICY_LINE]),
142
+ ...(hasClaudeignoreEntry ? [] : [CLAUDEIGNORE_LINE]),
143
+ ];
144
+ /**
145
+ * Continue an existing devflow block with the lines it is missing. One newline
146
+ * guard, no blank separator — the appended lines belong to the block above them.
147
+ */
148
+ const appendLines = (body, block) => `${body}${body.endsWith('\n') ? '' : '\n'}${block}\n`;
149
+ /**
150
+ * Start a new block after unrelated content: one blank separator line. Existing
151
+ * trailing newlines are preserved verbatim (no trimEnd, no blank-line dedupe) so
152
+ * the shell twin's `tail -c 1` guard produces the identical bytes.
153
+ */
154
+ const appendBlock = (body, block) => body.length === 0
155
+ ? `${block}\n`
156
+ : `${body}${body.endsWith('\n') ? '' : '\n'}\n${block}\n`;
157
+ // 1. User opt-out wins over every sentinel.
158
+ if (trimmed.includes('/.devflow/'))
83
159
  return null;
84
- // v2→v3 upgrade: v2 sentinel present, v3 sentinel absent → append the missing line only.
85
- // Preserve existing trailing newlines byte-for-byte (matches shell twin's tail -c 1 guard).
160
+ // 2. Block installed (v3 and later) — top up only the completion lines it lacks.
161
+ if (trimmed.includes(DEVFLOW_GITIGNORE_SENTINEL_V3)) {
162
+ return missingCompletionLines.length === 0
163
+ ? null
164
+ : appendLines(existingContent, missingCompletionLines.join('\n'));
165
+ }
166
+ // 3. v2 block installed — append the lines it lacks, in block order.
86
167
  if (trimmed.includes(DEVFLOW_GITIGNORE_SENTINEL_V2)) {
87
- const sep = existingContent.endsWith('\n') ? '' : '\n';
88
- return `${existingContent}${sep}${DEVFLOW_GITIGNORE_SENTINEL_V3}\n`;
168
+ return appendLines(existingContent, [DEVFLOW_GITIGNORE_SENTINEL_V3, ...missingCompletionLines].join('\n'));
89
169
  }
90
- const append = (body) => body.trimEnd()
91
- ? `${body.trimEnd()}\n\n${DEVFLOW_GITIGNORE_BLOCK}\n`
92
- : `${DEVFLOW_GITIGNORE_BLOCK}\n`;
93
- if (trimmed.some(l => l === '.devflow/')) {
170
+ // 4. No devflow block — install one, respecting any .claudeignore entry of the user's own.
171
+ const block = hasClaudeignoreEntry
172
+ ? DEVFLOW_GITIGNORE_BLOCK_WITHOUT_CLAUDEIGNORE
173
+ : DEVFLOW_GITIGNORE_BLOCK;
174
+ if (trimmed.includes('.devflow/')) {
94
175
  // Upgrade our legacy wholesale entry: drop the bare line + old comment, append block.
95
176
  const kept = lines
96
177
  .filter(l => l.trim() !== '.devflow/' && l.trim() !== LEGACY_DEVFLOW_COMMENT)
97
178
  .join('\n');
98
- return append(kept);
179
+ return appendBlock(kept, block);
99
180
  }
100
- return append(existingContent);
181
+ return appendBlock(existingContent, block);
101
182
  }
102
183
  /**
103
184
  * Merge Devflow deny entries into an existing settings JSON object.
@@ -531,11 +612,14 @@ export async function installManagedSettings(rootDir, verbose) {
531
612
  return false;
532
613
  }
533
614
  try {
534
- execSync(`sudo mkdir -p '${managedDir}'`, { stdio: 'inherit' });
535
- // Write via sudo tee to avoid shell quoting issues with the JSON content
615
+ execFileSync('sudo', ['mkdir', '-p', managedDir], { stdio: 'inherit' });
616
+ // Stage the JSON in a file and copy it with sudo. Every sudo call takes an
617
+ // argv (execFileSync, no shell), so no path is ever re-parsed by a shell — a
618
+ // package root under a home directory holding a quote cannot break or extend
619
+ // the root command.
536
620
  const tmpFile = path.join(rootDir, '.managed-settings-tmp.json');
537
621
  await fs.writeFile(tmpFile, content, 'utf-8');
538
- execSync(`sudo cp '${tmpFile}' '${managedPath}'`, { stdio: 'inherit' });
622
+ execFileSync('sudo', ['cp', tmpFile, managedPath], { stdio: 'inherit' });
539
623
  await fs.rm(tmpFile, { force: true });
540
624
  if (verbose) {
541
625
  p.log.success(`Managed settings written to ${managedPath} (via sudo)`);
@@ -557,14 +641,26 @@ export async function installManagedSettings(rootDir, verbose) {
557
641
  * 1. Try direct write/delete
558
642
  * 2. If EACCES and TTY, ask user before sudo
559
643
  * 3. Non-TTY: return false (caller logs preservation message)
644
+ *
645
+ * `managedPathOverride` exists so a test can point the removal at a temp file;
646
+ * production callers omit it and get the platform's system path. It is a
647
+ * parameter, deliberately not an environment variable: this function may run
648
+ * `sudo rm` / `sudo cp` on the path, and an env-selectable target would let
649
+ * whoever controls the environment aim a root write the user consented to for
650
+ * "managed settings" at any file.
560
651
  */
561
- export async function removeManagedSettings(rootDir, verbose) {
652
+ export async function removeManagedSettings(rootDir, verbose, managedPathOverride) {
562
653
  let managedPath;
563
- try {
564
- managedPath = getManagedSettingsPath();
654
+ if (managedPathOverride !== undefined) {
655
+ managedPath = managedPathOverride;
565
656
  }
566
- catch {
567
- return false;
657
+ else {
658
+ try {
659
+ managedPath = getManagedSettingsPath();
660
+ }
661
+ catch {
662
+ return false;
663
+ }
568
664
  }
569
665
  let existingContent;
570
666
  try {
@@ -639,12 +735,12 @@ export async function removeManagedSettings(rootDir, verbose) {
639
735
  }
640
736
  try {
641
737
  if (shouldDelete) {
642
- execSync(`sudo rm '${managedPath}'`, { stdio: 'inherit' });
738
+ execFileSync('sudo', ['rm', managedPath], { stdio: 'inherit' });
643
739
  }
644
740
  else {
645
741
  const tmpFile = path.join(rootDir, '.managed-settings-tmp.json');
646
742
  await fs.writeFile(tmpFile, updatedContent, 'utf-8');
647
- execSync(`sudo cp '${tmpFile}' '${managedPath}'`, { stdio: 'inherit' });
743
+ execFileSync('sudo', ['cp', tmpFile, managedPath], { stdio: 'inherit' });
648
744
  await fs.rm(tmpFile, { force: true });
649
745
  }
650
746
  if (verbose) {
@@ -949,45 +1045,73 @@ export async function updateGitignore(gitRoot, verbose) {
949
1045
  }
950
1046
  }
951
1047
  }
952
- /** Current carve-out marker version. Bump when the block format changes. */
953
- const GITIGNORE_MARKER_V3 = '.root-gitignore-configured-v3';
954
- /** Previous marker — removed when upgrading to v3. */
955
- const GITIGNORE_MARKER_V2 = '.root-gitignore-configured-v2';
1048
+ /**
1049
+ * Current carve-out marker version. Bump when the block format changes — together
1050
+ * with the shell twin's stamp (ensure-root-gitignore) and the ensure-devflow-init
1051
+ * fast path, in one commit (D-GITIGNORE-V5).
1052
+ */
1053
+ const GITIGNORE_MARKER_V5 = '.root-gitignore-configured-v5';
1054
+ /**
1055
+ * Earlier markers, the unversioned (v1) one included — every one is removed
1056
+ * whenever the project is v5-stamped, on the fast path too: an older devflow can
1057
+ * re-stamp one beside v5, and the shell twin drops the same four.
1058
+ */
1059
+ const LEGACY_GITIGNORE_MARKERS = [
1060
+ '.root-gitignore-configured-v4',
1061
+ '.root-gitignore-configured-v3',
1062
+ '.root-gitignore-configured-v2',
1063
+ '.root-gitignore-configured',
1064
+ ];
1065
+ /** Remove every legacy marker; an absent one is a no-op. Call only once v5 is stamped. */
1066
+ async function removeLegacyGitignoreMarkers(devflowDir) {
1067
+ for (const legacy of LEGACY_GITIGNORE_MARKERS) {
1068
+ try {
1069
+ await fs.rm(path.join(devflowDir, legacy), { force: true });
1070
+ }
1071
+ catch { /* ok if absent */ }
1072
+ }
1073
+ }
956
1074
  /**
957
1075
  * Deterministically ensure the project root .gitignore applies the `.devflow/`
958
- * carve-out (local by default, feature knowledge + conventions.md shared via git).
1076
+ * carve-out (local by default; feature knowledge, conventions.md and the evidence
1077
+ * policy shared via git).
959
1078
  *
960
1079
  * Manages ONLY `.devflow/` — never `.claude/` — because user-scope installs must
961
1080
  * not gitignore `.claude/`. This is the init-time counterpart to the always-on
962
- * src/assets/scripts/hooks/ensure-root-gitignore shell helper; both write the identical
963
- * DEVFLOW_GITIGNORE_BLOCK, so the two paths are byte-compatible and mutually
964
- * idempotent. Called unconditionally (independent of install scope and every
965
- * feature toggle) whenever a git root is known.
1081
+ * src/assets/scripts/hooks/ensure-root-gitignore shell helper; both resolve the same
1082
+ * shape for a given .gitignore — DEVFLOW_GITIGNORE_BLOCK, or
1083
+ * DEVFLOW_GITIGNORE_BLOCK_WITHOUT_CLAUDEIGNORE when the project owns that entry — and
1084
+ * emit identical bytes, so the two paths are byte-compatible and mutually idempotent.
1085
+ * Called unconditionally (independent of install scope and every feature toggle)
1086
+ * whenever a git root is known.
966
1087
  *
967
- * Uses a versioned marker file (`.devflow/.root-gitignore-configured-v3`) for fast-path
968
- * detection — the same pattern as the shell twin. Bumping the version forces existing
969
- * installs to re-run once and upgrade their block (v2→v3: adds conventions.md line).
1088
+ * Uses a versioned project-local marker file (`.devflow/.root-gitignore-configured-v5`)
1089
+ * for fast-path detection — the same pattern as the shell twin. The marker is a claim,
1090
+ * not proof, so even a marked install re-reads .gitignore and re-runs
1091
+ * computeDevflowGitignore; bumping the version forces a re-run once per install, which
1092
+ * is how a v4-marked project gains the policy line and is re-stamped v5.
970
1093
  *
971
- * Idempotent: already-v3 installs return immediately (marker fast-path). Errors are
972
- * swallowed (verbose-logged) — a gitignore write must never abort init.
1094
+ * Idempotent: computeDevflowGitignore returns null for a converged file, so a
1095
+ * marked install performs one read and no write. Errors are swallowed
1096
+ * (verbose-logged) — a gitignore write must never abort init.
973
1097
  */
974
1098
  export async function ensureDevflowGitignore(gitRoot, verbose) {
975
1099
  try {
976
1100
  const devflowDir = path.join(gitRoot, '.devflow');
977
- const markerV3 = path.join(devflowDir, GITIGNORE_MARKER_V3);
1101
+ const markerV5 = path.join(devflowDir, GITIGNORE_MARKER_V5);
978
1102
  const gitignorePath = path.join(gitRoot, '.gitignore');
979
- // Fast-path with verification: v3 marker normally means the block is installed,
1103
+ // Fast-path with verification: v5 marker normally means the block is installed,
980
1104
  // but the marker is a claim, not proof — a merge-conflict resolution may have
981
1105
  // dropped the block. Even when the marker exists, read .gitignore (one cheap
982
1106
  // read) and run computeDevflowGitignore; write only when it returns non-null.
983
- // Idempotent: sentinel present → computeDevflowGitignore returns null → no write.
984
- let v3Marked = false;
1107
+ // Idempotent: converged file → computeDevflowGitignore returns null → no write.
1108
+ let v5Marked = false;
985
1109
  try {
986
- await fs.access(markerV3);
987
- v3Marked = true;
1110
+ await fs.access(markerV5);
1111
+ v5Marked = true;
988
1112
  }
989
1113
  catch { /* absent */ }
990
- if (v3Marked) {
1114
+ if (v5Marked) {
991
1115
  let existingContent = '';
992
1116
  try {
993
1117
  existingContent = await fs.readFile(gitignorePath, 'utf-8');
@@ -997,9 +1121,10 @@ export async function ensureDevflowGitignore(gitRoot, verbose) {
997
1121
  if (healContent !== null) {
998
1122
  await fs.writeFile(gitignorePath, healContent, 'utf-8');
999
1123
  if (verbose) {
1000
- p.log.success('.gitignore configured (.devflow/ local; feature knowledge + conventions shared)');
1124
+ p.log.success('.gitignore configured (.devflow/ local; feature knowledge + conventions + evidence policy shared)');
1001
1125
  }
1002
1126
  }
1127
+ await removeLegacyGitignoreMarkers(devflowDir);
1003
1128
  return;
1004
1129
  }
1005
1130
  let gitignoreContent = '';
@@ -1011,16 +1136,13 @@ export async function ensureDevflowGitignore(gitRoot, verbose) {
1011
1136
  if (newContent !== null) {
1012
1137
  await fs.writeFile(gitignorePath, newContent, 'utf-8');
1013
1138
  if (verbose) {
1014
- p.log.success('.gitignore configured (.devflow/ local; feature knowledge + conventions shared)');
1139
+ p.log.success('.gitignore configured (.devflow/ local; feature knowledge + conventions + evidence policy shared)');
1015
1140
  }
1016
1141
  }
1017
- // Stamp v3 marker so subsequent runs fast-path; drop the legacy v2 marker.
1142
+ // Stamp v5 marker so subsequent runs fast-path; drop every legacy marker.
1018
1143
  await fs.mkdir(devflowDir, { recursive: true });
1019
- await fs.writeFile(markerV3, '', 'utf-8');
1020
- try {
1021
- await fs.rm(path.join(devflowDir, GITIGNORE_MARKER_V2), { force: true });
1022
- }
1023
- catch { /* ok if absent */ }
1144
+ await fs.writeFile(markerV5, '', 'utf-8');
1145
+ await removeLegacyGitignoreMarkers(devflowDir);
1024
1146
  }
1025
1147
  catch (error) {
1026
1148
  if (verbose) {
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Tracker artifact installer for the Claude Code target.
3
+ *
4
+ * Convergence function for the ONE artifact whose presence advertises the
5
+ * selected tracker provider: the Tracker agent file. The generated reference
6
+ * subtree converges too, but through {@link overlayInstalledReferences} in
7
+ * installer.ts — that is the installer's one overlay spelling, and a mode flag
8
+ * on this function would have made it a second (design review M2).
9
+ *
10
+ * Applies ADR-013: I/O orchestration in src/targets/; pure helpers in src/core/.
11
+ * Applies PF-009: warn-not-throw, so one failing artifact never aborts an install.
12
+ * Applies PF-015: the biconditional converges in BOTH directions — selecting a
13
+ * provider installs the agent, returning to github removes it.
14
+ */
15
+ import { promises as fs } from 'fs';
16
+ import * as path from 'path';
17
+ import { agentSourceDirs } from '../../core/assets.js';
18
+ import { mdFileName } from '../../core/orphan-sweep.js';
19
+ /**
20
+ * The agent whose presence is conditional on the provider.
21
+ *
22
+ * It stays DECLARED in `devflow-core-skills.agents` and is filtered at install
23
+ * time rather than being lifted into a feature-owned set: the compliance
24
+ * precedent does not transfer, because compliance's plugin was deleted while
25
+ * `devflow-core-skills` is a live, non-optional owner. A feature-owned set would
26
+ * cost three new union sites and a rewrite of the pinned agent-roster floor to
27
+ * solve a problem the agent SWEEP does not have — the sweep keys on the full
28
+ * registry, so it never sees this file as an orphan.
29
+ *
30
+ * Exported because the filter has more than one reader and may have only one
31
+ * authority (D-TRACKER-AGENT-OWNER): {@link convergeTrackerArtifacts} below,
32
+ * which decides whether the file exists, and, in installer.ts, both the generic
33
+ * agent copy loop — which has to skip the one agent it does not own — and the
34
+ * full-install pre-clean, which has to empty the agent directory around it. A
35
+ * second literal at any of those sites is the shape this export exists to forbid.
36
+ */
37
+ export const TRACKER_AGENT_NAME = 'tracker';
38
+ // ── Internals ──────────────────────────────────────────────────────────────
39
+ /**
40
+ * The provider whose mechanics need no Tracker agent.
41
+ *
42
+ * GitHub conventions are not inferred: the Git agent runs `gh`, whose issue
43
+ * grammar this repo already speaks. The agent exists to infer conventions from a
44
+ * connected tool-call server, which is a thing only the other providers have.
45
+ */
46
+ const AGENTLESS_PROVIDER = 'github';
47
+ function agentTarget(claudeDir) {
48
+ return path.join(claudeDir, 'agents', 'devflow', mdFileName(TRACKER_AGENT_NAME));
49
+ }
50
+ async function pathExists(p) {
51
+ try {
52
+ await fs.access(p);
53
+ return true;
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ /** First path in `candidates` that exists, or undefined when none do. */
60
+ async function firstExisting(candidates) {
61
+ for (const candidate of candidates) {
62
+ if (await pathExists(candidate))
63
+ return candidate;
64
+ }
65
+ return undefined;
66
+ }
67
+ /**
68
+ * Would copying `source` over `target` change anything?
69
+ *
70
+ * Asked once, between resolving the source and copying it, so a run that would write a
71
+ * byte-identical copy of the installed agent reports `unchanged` instead of `installed`
72
+ * — the same question the reference overlay asks per unit, for the same reason: a
73
+ * summary that can only ever say "installed" says nothing, and a steady-state re-init
74
+ * announcing "tracker agent installed" is the noise this closes (QA S2).
75
+ *
76
+ * It does NOT weaken the self-heal. A hand-edited or truncated agent differs from its
77
+ * source, so it is copied and reported as written; only an identical file is skipped,
78
+ * and skipping a copy of what is already there changes nothing on disk.
79
+ *
80
+ * Any error — an absent target, an unreadable one — answers "no". The fallback is the
81
+ * copy that was going to happen anyway, so a failure to compare costs a write, never
82
+ * correctness.
83
+ */
84
+ async function copyWouldChangeNothing(source, target) {
85
+ try {
86
+ const [from, to] = await Promise.all([fs.readFile(source), fs.readFile(target)]);
87
+ return from.equals(to);
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ }
93
+ // ── Convergence ────────────────────────────────────────────────────────────
94
+ /**
95
+ * Converge the Tracker agent file onto the resolved provider.
96
+ *
97
+ * Convergence matrix:
98
+ * provider !== github → copy the agent in, unless the installed file is already
99
+ * byte-identical to the source. Compared rather than trusted
100
+ * for existing, so a truncated or hand-edited file self-heals;
101
+ * compared rather than re-copied blind, so a run that changes
102
+ * nothing reports `unchanged` and the summary stays quiet
103
+ * (see {@link copyWouldChangeNothing})
104
+ * provider === github → remove it, absent or not
105
+ *
106
+ * Never throws. A caller gates on `converged`; it does not catch. The one
107
+ * refusal that is not an I/O degradation — a claudeDir that is not absolute —
108
+ * is reported the same way rather than thrown, because it reaches here from a
109
+ * manifest read and an install must not die on it.
110
+ */
111
+ export async function convergeTrackerArtifacts(opts) {
112
+ const { claudeDir, provider, warn } = opts;
113
+ // Precondition, asserted in production code rather than only in tests: an
114
+ // empty or relative claudeDir would make the target resolve somewhere
115
+ // unexpected, and the removal branch runs fs.rm against it.
116
+ if (!path.isAbsolute(claudeDir)) {
117
+ warn(`tracker: claudeDir is not an absolute path ("${claudeDir}") — skipping convergence`);
118
+ return { converged: false, agentPresent: false, agent: 'unchanged' };
119
+ }
120
+ const target = agentTarget(claudeDir);
121
+ if (provider === AGENTLESS_PROVIDER) {
122
+ const existed = await pathExists(target);
123
+ if (!existed)
124
+ return { converged: true, agentPresent: false, agent: 'unchanged' };
125
+ try {
126
+ await fs.rm(target, { force: true });
127
+ }
128
+ catch (err) {
129
+ warn(`tracker: failed to remove the Tracker agent (${target}) — ${String(err)}`);
130
+ return { converged: false, agentPresent: true, agent: 'unchanged' };
131
+ }
132
+ return { converged: true, agentPresent: false, agent: 'removed' };
133
+ }
134
+ const dirs = opts.agentSourceDirs ?? agentSourceDirs();
135
+ const candidates = dirs.map(dir => path.join(dir, mdFileName(TRACKER_AGENT_NAME)));
136
+ const source = await firstExisting(candidates);
137
+ if (source === undefined) {
138
+ warn(`tracker: agent source not found for "${TRACKER_AGENT_NAME}" (searched: ${candidates.join(', ')}) — ` +
139
+ `run \`npm run build:mds\` if it is compiled from an .mds generator host`);
140
+ // Probed rather than assumed: a previous run may have left a copy that is
141
+ // still spawnable, and that is the difference between "this run did nothing"
142
+ // and "there is nothing there".
143
+ return { converged: false, agentPresent: await pathExists(target), agent: 'unchanged' };
144
+ }
145
+ // Already converged — nothing to write, and nothing for the summary to announce.
146
+ // The directory is not created either: an identical file at the target means it is
147
+ // already there (see {@link copyWouldChangeNothing}).
148
+ if (await copyWouldChangeNothing(source, target)) {
149
+ return { converged: true, agentPresent: true, agent: 'unchanged' };
150
+ }
151
+ try {
152
+ await fs.mkdir(path.dirname(target), { recursive: true });
153
+ await fs.copyFile(source, target);
154
+ }
155
+ catch (err) {
156
+ warn(`tracker: failed to install the Tracker agent (${target}) — ${String(err)}`);
157
+ return { converged: false, agentPresent: await pathExists(target), agent: 'unchanged' };
158
+ }
159
+ return { converged: true, agentPresent: true, agent: 'installed' };
160
+ }
161
+ //# sourceMappingURL=tracker-install.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devflow-kit",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "A meta-harness for Claude Code — turns a single coding agent into an engineering team: orchestration, parallel review, persistent memory, self-learning, and graph workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,7 +26,8 @@
26
26
  "version:bump": "npx tsx scripts/bump-version.ts",
27
27
  "test": "vitest run",
28
28
  "test:watch": "vitest",
29
- "test:integration": "vitest run --config vitest.integration.config.ts"
29
+ "test:integration": "vitest run --config vitest.integration.config.ts",
30
+ "test:golden:update": "npx tsx scripts/update-golden.ts"
30
31
  },
31
32
  "keywords": [
32
33
  "claude",
@@ -63,7 +64,7 @@
63
64
  "@clack/prompts": "^0.9.1",
64
65
  "commander": "^12.0.0",
65
66
  "picocolors": "^1.1.1",
66
- "subswitch": "0.4.0"
67
+ "subswitch": "0.5.0"
67
68
  },
68
69
  "devDependencies": {
69
70
  "@mdscript/mds": "0.2.0",