devflow-kit 3.3.0 → 3.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 (220) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +8 -4
  3. package/dist/agents/code-edit.md +133 -0
  4. package/dist/agents/code-fix.md +165 -0
  5. package/dist/agents/code.md +187 -0
  6. package/{src/assets → dist}/agents/design.md +1 -2
  7. package/{src/assets → dist}/agents/diagnose.md +1 -2
  8. package/dist/agents/git.md +33 -722
  9. package/{src/assets → dist}/agents/knowledge.md +12 -5
  10. package/{src/assets → dist}/agents/research.md +8 -2
  11. package/{src/assets → dist}/agents/review.md +8 -11
  12. package/{src/assets → dist}/agents/scrutinize.md +4 -1
  13. package/dist/agents/skim.md +152 -0
  14. package/{src/assets → dist}/agents/triage.md +9 -2
  15. package/dist/cli/commands/behavior-prompts.js +77 -0
  16. package/dist/cli/commands/behavior.js +123 -0
  17. package/dist/cli/commands/flags.js +8 -2
  18. package/dist/cli/commands/init-seed.js +7 -3
  19. package/dist/cli/commands/init.js +134 -5
  20. package/dist/cli/commands/learning.js +38 -3
  21. package/dist/cli/commands/rules.js +16 -0
  22. package/dist/cli/commands/tracker.js +112 -15
  23. package/dist/cli/commands/uninstall.js +42 -1
  24. package/dist/cli.js +2 -0
  25. package/dist/commands/bug-analysis.md +35 -13
  26. package/dist/commands/code-review.md +155 -69
  27. package/dist/commands/debug.md +39 -16
  28. package/dist/commands/dynamic-build.md +61 -72
  29. package/dist/commands/dynamic-plan.md +39 -25
  30. package/dist/commands/dynamic-profile.md +28 -14
  31. package/dist/commands/dynamic-tickets.md +38 -22
  32. package/dist/commands/explore.md +18 -16
  33. package/dist/commands/implement.md +121 -150
  34. package/dist/commands/plan.md +51 -31
  35. package/dist/commands/release.md +72 -6
  36. package/dist/commands/research.md +49 -20
  37. package/dist/commands/resolve.md +54 -46
  38. package/dist/commands/self-review.md +38 -25
  39. package/dist/core/agent-frontmatter.js +101 -0
  40. package/dist/core/agent-models.js +43 -0
  41. package/dist/core/assets.js +55 -10
  42. package/dist/core/claude-md-audit.js +190 -0
  43. package/dist/core/compliance-compose.js +6 -4
  44. package/dist/core/feature-switch.js +41 -1
  45. package/dist/core/flags.js +139 -0
  46. package/dist/core/fs-atomic.js +8 -3
  47. package/dist/core/learning-variants.js +213 -0
  48. package/dist/core/manifest.js +73 -3
  49. package/dist/core/mds-variants.js +98 -19
  50. package/dist/core/plugins.js +111 -72
  51. package/dist/core/project-paths.js +0 -4
  52. package/dist/core/proxy-log.js +1 -1
  53. package/dist/core/proxy-state.js +1 -1
  54. package/dist/core/tracker.js +120 -2
  55. package/dist/learning-off/agents/code-edit.md +129 -0
  56. package/dist/learning-off/agents/code-fix.md +161 -0
  57. package/dist/learning-off/agents/code.md +183 -0
  58. package/dist/learning-off/agents/design.md +118 -0
  59. package/dist/learning-off/agents/diagnose.md +210 -0
  60. package/dist/learning-off/agents/knowledge.md +96 -0
  61. package/dist/learning-off/agents/research.md +155 -0
  62. package/dist/learning-off/agents/review.md +224 -0
  63. package/dist/learning-off/agents/scrutinize.md +120 -0
  64. package/{src/assets → dist/learning-off}/agents/skim.md +5 -8
  65. package/dist/learning-off/agents/triage.md +170 -0
  66. package/dist/learning-off/commands/bug-analysis.md +420 -0
  67. package/dist/learning-off/commands/code-review.md +530 -0
  68. package/dist/learning-off/commands/debug.md +314 -0
  69. package/dist/learning-off/commands/dynamic-build.md +1245 -0
  70. package/dist/learning-off/commands/dynamic-plan.md +426 -0
  71. package/dist/learning-off/commands/dynamic-profile.md +214 -0
  72. package/dist/learning-off/commands/dynamic-tickets.md +634 -0
  73. package/dist/learning-off/commands/explore.md +210 -0
  74. package/dist/learning-off/commands/implement.md +774 -0
  75. package/dist/learning-off/commands/plan.md +671 -0
  76. package/dist/learning-off/commands/release.md +311 -0
  77. package/dist/learning-off/commands/research.md +229 -0
  78. package/dist/learning-off/commands/resolve.md +842 -0
  79. package/dist/learning-off/commands/self-review.md +266 -0
  80. package/dist/skills/git/references/learn-conventions.md +27 -3
  81. package/dist/skills/git/references/pr/check-ci-status.md +19 -0
  82. package/dist/skills/git/references/pr/check-merge-readiness.md +19 -0
  83. package/dist/skills/git/references/pr/fetch-review-threads.md +30 -0
  84. package/dist/skills/git/references/pr/post-resolution-summary.md +18 -0
  85. package/dist/skills/git/references/pr/post-review-summary.md +20 -0
  86. package/dist/skills/git/references/pr/resolve-review-threads.md +28 -4
  87. package/dist/skills/git/references/pr/update-pr-evidence.md +13 -0
  88. package/dist/skills/git/references/pr/validate-branch.md +26 -0
  89. package/dist/skills/git/references/tracker/_contract.md +33 -0
  90. package/dist/skills/git/references/tracker/github/associate-release.md +24 -0
  91. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +31 -1
  92. package/dist/skills/git/references/tracker/github/create-release.md +30 -1
  93. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +89 -2
  94. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +16 -1
  95. package/dist/skills/git/references/tracker/github/fetch-issue.md +38 -1
  96. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +49 -1
  97. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +29 -0
  98. package/dist/skills/git/references/tracker/github/manage-debt.md +21 -1
  99. package/dist/skills/git/references/tracker/github/post-wave-report.md +17 -1
  100. package/dist/skills/git/references/tracker/github/setup-task.md +53 -1
  101. package/dist/skills/git/references/tracker/jira/associate-release.md +25 -1
  102. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +31 -1
  103. package/dist/skills/git/references/tracker/jira/create-release.md +30 -1
  104. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +89 -2
  105. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +16 -1
  106. package/dist/skills/git/references/tracker/jira/fetch-issue.md +38 -1
  107. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +49 -1
  108. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +29 -0
  109. package/dist/skills/git/references/tracker/jira/manage-debt.md +21 -1
  110. package/dist/skills/git/references/tracker/jira/post-wave-report.md +17 -1
  111. package/dist/skills/git/references/tracker/jira/setup-task.md +55 -3
  112. package/dist/skills/git/references/tracker/linear/associate-release.md +25 -1
  113. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +31 -1
  114. package/dist/skills/git/references/tracker/linear/create-release.md +30 -1
  115. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +89 -2
  116. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +16 -1
  117. package/dist/skills/git/references/tracker/linear/fetch-issue.md +38 -1
  118. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +49 -1
  119. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +29 -0
  120. package/dist/skills/git/references/tracker/linear/manage-debt.md +21 -1
  121. package/dist/skills/git/references/tracker/linear/post-wave-report.md +17 -1
  122. package/dist/skills/git/references/tracker/linear/setup-task.md +55 -3
  123. package/dist/targets/claude-code/agent-type-owner.js +164 -0
  124. package/dist/targets/claude-code/behavior-install.js +111 -0
  125. package/dist/targets/claude-code/installer.js +140 -37
  126. package/dist/targets/claude-code/language-stamp.js +185 -0
  127. package/dist/targets/claude-code/learning-install.js +497 -0
  128. package/dist/targets/claude-code/tracker-install.js +33 -16
  129. package/dist/targets/claude-code/tracker-servers.js +107 -0
  130. package/package.json +4 -3
  131. package/src/assets/agents/_partials/_code.mds +109 -0
  132. package/src/assets/agents/code-edit.mds +81 -0
  133. package/src/assets/agents/code-fix.mds +113 -0
  134. package/src/assets/agents/code.mds +135 -0
  135. package/src/assets/agents/design.mds +148 -0
  136. package/src/assets/agents/diagnose.mds +225 -0
  137. package/src/assets/agents/evaluate.md +8 -4
  138. package/src/assets/agents/explore.md +58 -0
  139. package/src/assets/agents/git.mds +33 -722
  140. package/src/assets/agents/knowledge.mds +131 -0
  141. package/src/assets/agents/learning.md +19 -69
  142. package/src/assets/agents/research.mds +182 -0
  143. package/src/assets/agents/review.mds +282 -0
  144. package/src/assets/agents/scrutinize.mds +135 -0
  145. package/src/assets/agents/simplify.md +3 -0
  146. package/src/assets/agents/skim.mds +165 -0
  147. package/src/assets/agents/synthesize.md +26 -352
  148. package/src/assets/agents/test.md +8 -0
  149. package/src/assets/agents/tracker.md +20 -7
  150. package/src/assets/agents/triage.mds +201 -0
  151. package/src/assets/agents/validate.md +13 -6
  152. package/src/assets/commands/_partials/_compliance.mds +41 -4
  153. package/src/assets/commands/_partials/_decisions.mds +33 -2
  154. package/src/assets/commands/_partials/_engine.mds +14 -6
  155. package/src/assets/commands/_partials/_knowledge.mds +25 -12
  156. package/src/assets/commands/_partials/_preamble.mds +34 -10
  157. package/src/assets/commands/_partials/_publication.mds +5 -4
  158. package/src/assets/commands/_partials/_roster.mds +4 -3
  159. package/src/assets/commands/_partials/_settings.mds +13 -5
  160. package/src/assets/commands/_partials/_wave.mds +9 -1
  161. package/src/assets/commands/bug-analysis.mds +27 -5
  162. package/src/assets/commands/code-review.mds +158 -52
  163. package/src/assets/commands/debug.mds +41 -3
  164. package/src/assets/commands/dynamic-build.mds +47 -27
  165. package/src/assets/commands/dynamic-plan.mds +40 -9
  166. package/src/assets/commands/dynamic-profile.mds +9 -1
  167. package/src/assets/commands/dynamic-tickets.mds +21 -6
  168. package/src/assets/commands/explore.mds +28 -2
  169. package/src/assets/commands/implement.mds +126 -129
  170. package/src/assets/commands/plan.mds +58 -14
  171. package/src/assets/commands/{release.md → release.mds} +30 -26
  172. package/src/assets/commands/research.mds +42 -11
  173. package/src/assets/commands/resolve.mds +59 -15
  174. package/src/assets/commands/self-review.mds +30 -5
  175. package/src/assets/mds/.mdsroot +4 -0
  176. package/src/assets/mds/git/_ops.mds +383 -0
  177. package/src/assets/mds/git/_ops_pr.mds +201 -0
  178. package/src/assets/mds/git/_ops_steps.mds +147 -0
  179. package/src/assets/mds/git/_pr.mds +39 -44
  180. package/src/assets/mds/git/_references.mds +6 -3
  181. package/src/assets/mds/tracker/_common.mds +5 -5
  182. package/src/assets/mds/tracker/_contract.mds +73 -0
  183. package/src/assets/mds/tracker/_github.mds +73 -16
  184. package/src/assets/mds/tracker/_jira.mds +77 -19
  185. package/src/assets/mds/tracker/_linear.mds +77 -19
  186. package/src/assets/mds/tracker/_mcp.mds +2 -2
  187. package/src/assets/mds/tracker/_steps.mds +98 -0
  188. package/src/assets/rules/behavior.md +6 -0
  189. package/src/assets/rules/context-economy.md +10 -0
  190. package/src/assets/rules/go.md +1 -0
  191. package/src/assets/rules/java.md +1 -0
  192. package/src/assets/rules/python.md +1 -0
  193. package/src/assets/rules/rust.md +1 -0
  194. package/src/assets/rules/typescript.md +1 -0
  195. package/src/assets/scripts/claude-md-audit.cjs +611 -0
  196. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +3 -3
  197. package/src/assets/scripts/hooks/json-helper.cjs +13 -5
  198. package/src/assets/scripts/hooks/json-parse +34 -10
  199. package/src/assets/scripts/hooks/lib/project-paths.cjs +0 -6
  200. package/src/assets/scripts/hooks/session-start-context +315 -7
  201. package/src/assets/skills/agent-references/SKILL.md +33 -0
  202. package/src/assets/skills/agent-references/references/learning/maintain.md +71 -0
  203. package/src/assets/skills/agent-references/references/synthesize/bug-analysis.md +71 -0
  204. package/src/assets/skills/agent-references/references/synthesize/design.md +36 -0
  205. package/src/assets/skills/agent-references/references/synthesize/exploration.md +38 -0
  206. package/src/assets/skills/agent-references/references/synthesize/planning.md +60 -0
  207. package/src/assets/skills/agent-references/references/synthesize/research.md +61 -0
  208. package/src/assets/skills/agent-references/references/synthesize/review.md +76 -0
  209. package/src/assets/skills/apply-decisions/SKILL.md +1 -1
  210. package/src/assets/skills/apply-feature-knowledge/SKILL.md +5 -5
  211. package/src/assets/skills/compliance/SKILL.md +1 -1
  212. package/src/assets/skills/docs-framework/SKILL.md +80 -83
  213. package/src/assets/skills/docs-framework/references/patterns.md +14 -25
  214. package/src/assets/skills/docs-framework/references/violations.md +7 -7
  215. package/src/assets/skills/feature-knowledge/SKILL.md +43 -12
  216. package/src/assets/skills/gap-analysis/SKILL.md +0 -16
  217. package/src/assets/skills/gap-analysis/references/compliance.md +19 -0
  218. package/src/assets/skills/quality-gates/SKILL.md +1 -1
  219. package/dist/skills/git/references/pr/ensure-pr-ready.md +0 -24
  220. package/src/assets/agents/code.md +0 -330
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Who else defines an agent type: the install-edge check behind devflow's Explore
3
+ * override.
4
+ *
5
+ * Pure core (`parseAgentType`) with one bounded I/O function (`findForeignAgentOwner`)
6
+ * at the installer edge. Result-style outcomes: nothing here throws.
7
+ *
8
+ * D-EXPLORE-OVERRIDE: devflow ships an agent whose frontmatter `name:` is `Explore`,
9
+ * the same type as the Explore agent built into Claude Code. This is a documented,
10
+ * supported override, not a collision: Claude Code merges its agent sources into one
11
+ * map keyed by the exact `name:` string, in the order built-in, plugin, user, project,
12
+ * flag, policy, and a later source replaces an earlier one. A user-level agent named
13
+ * Explore therefore replaces the built-in for every session on the machine, plan mode
14
+ * included, because the lookup is by agent type and no session mode bypasses it.
15
+ *
16
+ * Verified against Claude Code 2.1.296 (`claude --version`) by reading the binary:
17
+ * - the merge is `bz` (the Map keyed by `agentType`, last source wins);
18
+ * - the built-in Explore is `omitClaudeMd: true` with `model: "inherit"`, and its
19
+ * inherit-cap to Opus is gated on `source === "built-in"`, so it does not follow
20
+ * the override;
21
+ * - `omitGitStatus` is set by `agentType === "Explore" || agentType === "Plan"` and
22
+ * checks no source, so devflow's Explore is spared the git-status block too;
23
+ * - agents are matched with strict equality, so `explore` and `Explore` are two
24
+ * different agents.
25
+ * Uninstalling removes devflow's copy, and the built-in applies again.
26
+ *
27
+ * D-EXPLORE-NO-CLOBBER: two user-level agents with one type fall back to file load
28
+ * order, so devflow never installs a second Explore beside the user's own. The install
29
+ * skips its copy when another file under the agents directory defines the type, and
30
+ * never touches that file. Matching is exact, as Claude Code's is: a lowercase
31
+ * `explore` agent is a different agent and does not block the install.
32
+ */
33
+ import { promises as fs, constants as fsConstants } from 'fs';
34
+ import * as path from 'path';
35
+ /**
36
+ * Registry agents that deliberately take over a built-in agent type, mapped to the
37
+ * frontmatter `name:` they ship. The install checks each against the user's own agents.
38
+ */
39
+ export const BUILTIN_OVERRIDE_AGENTS = new Map([['explore', 'Explore']]);
40
+ /** Bytes read from the start of an agent file: enough for any frontmatter name line. */
41
+ export const FRONTMATTER_READ_BYTES = 4096;
42
+ /** Directory levels below the agents directory that the scan descends. */
43
+ export const MAX_AGENT_SCAN_DEPTH = 3;
44
+ /** Entries the scan inspects before it stops: a user directory is not an archive. */
45
+ export const MAX_AGENT_SCAN_ENTRIES = 500;
46
+ /** Frontmatter lines inspected for the name key. */
47
+ const MAX_FRONTMATTER_LINES = 60;
48
+ /**
49
+ * The agent type a markdown file declares: the top-level `name:` key of its leading
50
+ * `---` frontmatter block, unquoted. Undefined when the file has no closed frontmatter
51
+ * block, no top-level `name:` key, or an empty value.
52
+ *
53
+ * Reads only the key Claude Code keys agents by. It is deliberately not a YAML parser:
54
+ * a name written any other way (block scalar, flow mapping) reads as absent, so the
55
+ * check errs toward installing rather than skipping.
56
+ */
57
+ export function parseAgentType(head) {
58
+ const lines = head.replace(/^/, '').split(/\r?\n/);
59
+ if (lines[0]?.trimEnd() !== '---')
60
+ return undefined;
61
+ const limit = Math.min(lines.length, MAX_FRONTMATTER_LINES);
62
+ for (let i = 1; i < limit; i++) {
63
+ const line = lines[i];
64
+ if (line.trimEnd() === '---')
65
+ return undefined;
66
+ const key = /^name[ \t]*:(.*)$/.exec(line);
67
+ if (key === null)
68
+ continue;
69
+ return closesFrontmatter(lines, i + 1, limit) ? unquoteScalar(key[1]) : undefined;
70
+ }
71
+ return undefined;
72
+ }
73
+ /** True when a `---` line closes the block somewhere in `lines[from, limit)`. */
74
+ function closesFrontmatter(lines, from, limit) {
75
+ for (let i = from; i < limit; i++) {
76
+ if (lines[i].trimEnd() === '---')
77
+ return true;
78
+ }
79
+ return false;
80
+ }
81
+ function unquoteScalar(raw) {
82
+ const value = raw.trim();
83
+ const quoted = /^(["'])(.*?)\1(?:[ \t]+#.*)?$/.exec(value);
84
+ const text = quoted !== null ? quoted[2] : value.replace(/[ \t]+#.*$/, '');
85
+ const trimmed = text.trim();
86
+ return trimmed === '' ? undefined : trimmed;
87
+ }
88
+ /**
89
+ * The first file under `agentsRoot`, outside `ownDir`, whose frontmatter declares
90
+ * `agentType`: undefined when there is none.
91
+ *
92
+ * Bounded: descends at most {@link MAX_AGENT_SCAN_DEPTH} levels, inspects at most
93
+ * {@link MAX_AGENT_SCAN_ENTRIES} entries and reads at most {@link FRONTMATTER_READ_BYTES}
94
+ * bytes of any file. Symlinks, to files or directories, are never followed. A missing
95
+ * agents directory means no owner; an unreadable root is an error outcome; an
96
+ * unreadable entry below it is skipped. Entries are visited in sorted order, so the
97
+ * answer does not depend on the filesystem's listing order.
98
+ */
99
+ export async function findForeignAgentOwner(opts) {
100
+ const { agentsRoot, ownDir, agentType } = opts;
101
+ const own = path.resolve(ownDir);
102
+ let inspected = 0;
103
+ const walk = async (dir, depth) => {
104
+ let entries;
105
+ try {
106
+ entries = await fs.readdir(dir, { withFileTypes: true });
107
+ }
108
+ catch (err) {
109
+ if (depth === 0)
110
+ throw err;
111
+ return undefined;
112
+ }
113
+ entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
114
+ for (const entry of entries) {
115
+ if (inspected >= MAX_AGENT_SCAN_ENTRIES)
116
+ return undefined;
117
+ inspected++;
118
+ if (entry.isSymbolicLink())
119
+ continue;
120
+ const full = path.join(dir, entry.name);
121
+ if (entry.isDirectory()) {
122
+ if (path.resolve(full) === own || depth >= MAX_AGENT_SCAN_DEPTH)
123
+ continue;
124
+ const found = await walk(full, depth + 1);
125
+ if (found !== undefined)
126
+ return found;
127
+ continue;
128
+ }
129
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
130
+ continue;
131
+ if (parseAgentType(await readHead(full)) === agentType)
132
+ return full;
133
+ }
134
+ return undefined;
135
+ };
136
+ try {
137
+ return { ok: true, value: await walk(agentsRoot, 0) };
138
+ }
139
+ catch (err) {
140
+ if (err.code === 'ENOENT')
141
+ return { ok: true, value: undefined };
142
+ return {
143
+ ok: false,
144
+ error: { kind: 'unreadable-root', dir: agentsRoot, reason: err instanceof Error ? err.message : String(err) },
145
+ };
146
+ }
147
+ }
148
+ /** The first {@link FRONTMATTER_READ_BYTES} bytes of a file as text; '' when it cannot be read. */
149
+ async function readHead(file) {
150
+ let handle;
151
+ try {
152
+ handle = await fs.open(file, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
153
+ const buffer = Buffer.alloc(FRONTMATTER_READ_BYTES);
154
+ const { bytesRead } = await handle.read(buffer, 0, FRONTMATTER_READ_BYTES, 0);
155
+ return buffer.subarray(0, bytesRead).toString('utf-8');
156
+ }
157
+ catch {
158
+ return '';
159
+ }
160
+ finally {
161
+ await handle?.close().catch(() => { });
162
+ }
163
+ }
164
+ //# sourceMappingURL=agent-type-owner.js.map
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Behavior rule installer for the Claude Code target.
3
+ *
4
+ * Convergence function: installs `rules/devflow/behavior.md` when the machine switch is
5
+ * on AND the install includes rules, and removes it in every other state.
6
+ *
7
+ * D-BEHAVIOR-OPT-IN: the behavior rule is feature-owned and OPT-IN. No plugin declares
8
+ * it (FEATURE_OWNED_RULES in src/core/plugins.ts), so the plugin installer never copies
9
+ * it; this module is its only writer. The file exists exactly when
10
+ * `features.behavior && features.rules`, and the same function serves every caller that
11
+ * can change either input: `devflow init`, `devflow behavior --enable/--disable` and
12
+ * `devflow rules --enable`. A full install wipes `rules/devflow/` first, and a full
13
+ * uninstall removes the directory, so neither needs a rule of its own.
14
+ *
15
+ * The copy goes through installRuleFile, so a shadow at ~/.devflow/rules/behavior.md
16
+ * overrides the shipped text the way it does for every other rule. The shadow only
17
+ * changes WHAT is installed; the switch still decides WHETHER.
18
+ *
19
+ * I/O orchestration in src/targets/; warn-not-throw for per-item failures, with the
20
+ * `warn` callback injected so callers choose how a warning surfaces.
21
+ */
22
+ import { promises as fs } from 'fs';
23
+ import * as path from 'path';
24
+ import { installRuleFile } from './installer.js';
25
+ // ── Types ──────────────────────────────────────────────────────────────────
26
+ export const BEHAVIOR_RULE_NAME = 'behavior';
27
+ // ── Path helpers ───────────────────────────────────────────────────────────
28
+ /** Installed behavior rule file: {claudeDir}/rules/devflow/behavior.md */
29
+ export function behaviorRulePath(claudeDir) {
30
+ return path.join(claudeDir, 'rules', 'devflow', `${BEHAVIOR_RULE_NAME}.md`);
31
+ }
32
+ async function pathExists(p) {
33
+ try {
34
+ await fs.access(p);
35
+ return true;
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ // ── Public API ─────────────────────────────────────────────────────────────
42
+ /**
43
+ * Converge the behavior rule in the Claude Code install target.
44
+ *
45
+ * Convergence matrix:
46
+ * enabled + rulesEnabled → rule file installed (shipped text, or a valid shadow)
47
+ * enabled + !rulesEnabled → rule file removed
48
+ * !enabled → rule file removed
49
+ *
50
+ * Never throws. A relative `claudeDir` is refused with a warning rather than resolved
51
+ * against the working directory, because the removal arm deletes a path.
52
+ */
53
+ export async function convergeBehaviorRule(opts) {
54
+ const { claudeDir, devflowDir, enabled, rulesEnabled, warn } = opts;
55
+ if (!path.isAbsolute(claudeDir)) {
56
+ warn(`behavior: claudeDir is not an absolute path ("${claudeDir}") — skipping convergence`);
57
+ return { installed: false, removed: false, converged: false };
58
+ }
59
+ const target = behaviorRulePath(claudeDir);
60
+ if (enabled && rulesEnabled) {
61
+ try {
62
+ await fs.mkdir(path.dirname(target), { recursive: true });
63
+ const outcome = await installRuleFile(BEHAVIOR_RULE_NAME, devflowDir, path.dirname(target));
64
+ if (outcome === 'skipped') {
65
+ warn('behavior: rule install failed — the file could not be copied');
66
+ return { installed: false, removed: false, converged: false };
67
+ }
68
+ if (outcome === 'source-invalid-shadow:empty-shadow-file' || outcome === 'source-invalid-shadow:not-a-file') {
69
+ warn("behavior: the shadow is invalid — Devflow's version was installed");
70
+ return { installed: true, removed: false, converged: false };
71
+ }
72
+ return { installed: true, removed: false, converged: true };
73
+ }
74
+ catch (err) {
75
+ warn(`behavior: rule install failed — ${String(err)}`);
76
+ return { installed: false, removed: false, converged: false };
77
+ }
78
+ }
79
+ // Off, or rules off: no rule. Probe first so the caller can report a removal;
80
+ // absence is already the desired end state.
81
+ if (!(await pathExists(target)))
82
+ return { installed: false, removed: false, converged: true };
83
+ try {
84
+ await fs.rm(target, { force: true });
85
+ return { installed: false, removed: true, converged: true };
86
+ }
87
+ catch (err) {
88
+ warn(`behavior: failed to remove rule — ${String(err)}`);
89
+ return { installed: true, removed: false, converged: false };
90
+ }
91
+ }
92
+ // ── Manifest-slice wrapper ─────────────────────────────────────────────────
93
+ /**
94
+ * Thin wrapper around convergeBehaviorRule that takes its inputs from a manifest slice
95
+ * in one place, so the call sites (init.ts, rules.ts, behavior.ts) do not each hand-assemble them.
96
+ *
97
+ * @param opts.rulesEnabledOverride - When provided, replaces manifest.features.rules as
98
+ * the rulesEnabled source. Used by rules.ts, where rulesEnabled reflects the actual
99
+ * install outcome rather than the value stored in the manifest.
100
+ */
101
+ export async function convergeBehaviorFromManifest(opts) {
102
+ const { claudeDir, devflowDir, manifest, warn, rulesEnabledOverride } = opts;
103
+ return convergeBehaviorRule({
104
+ claudeDir,
105
+ devflowDir,
106
+ enabled: manifest.features.behavior,
107
+ rulesEnabled: rulesEnabledOverride ?? manifest.features.rules,
108
+ warn,
109
+ });
110
+ }
111
+ //# sourceMappingURL=behavior-install.js.map
@@ -1,13 +1,18 @@
1
1
  import { promises as fs } from 'fs';
2
2
  import { existsSync } from 'fs';
3
3
  import * as path from 'path';
4
- import { SKILL_NAMESPACE, prefixSkillName, unprefixSkillName, getAllSkillNames, getAllAgentNames, getAllCommandNames, FEATURE_OWNED_SKILLS, resolveSkillInstallPlan } from '../../core/plugins.js';
5
- import { skillsDir, agentSourceDirs, rulesDir, commandsDir, scriptsDir, compiledSkillRefsDir } from '../../core/assets.js';
4
+ import { SKILL_NAMESPACE, prefixSkillName, unprefixSkillName, getAllSkillNames, getAllAgentNames, getAllCommandNames, FEATURE_OWNED_SKILLS, resolveSkillInstallPlan, omitLearningGatedSkills } from '../../core/plugins.js';
5
+ import { skillsDir, agentSourceDirs, rulesDir, commandSourceDirs, scriptsDir, compiledSkillRefsDir } from '../../core/assets.js';
6
6
  import { getPackageRoot, isContainedIn } from '../../core/paths.js';
7
7
  import { sweepOrphanedAssets, mdFileName, mdEntryName } from '../../core/orphan-sweep.js';
8
8
  import { generatedReferenceManifest, installedReferenceManifest, PR_HOST_DESTINATION_ROOT, SKILL_REFS_SKILL_NAME, TRACKER_DESTINATION_ROOT } from '../../core/mds-variants.js';
9
9
  import { sweepOrphanedReferences, MAX_REFERENCE_SWEEP_DEPTH } from '../../core/reference-sweep.js';
10
10
  import { TRACKER_AGENT_NAME } from './tracker-install.js';
11
+ import { isTrackerStampedAgent } from './tracker-servers.js';
12
+ import { stampAgentTools } from '../../core/agent-frontmatter.js';
13
+ import { TRACKER_MCP_SERVERS_DEFAULT, trackerServerTools } from '../../core/tracker.js';
14
+ import { BUILTIN_OVERRIDE_AGENTS, findForeignAgentOwner } from './agent-type-owner.js';
15
+ import { restampInstalledCommands } from './language-stamp.js';
11
16
  // ---------------------------------------------------------------------------
12
17
  // Shadow validation helpers (exported — reused by Step 4 list commands)
13
18
  // ---------------------------------------------------------------------------
@@ -1263,6 +1268,61 @@ export async function composeScripts(scriptsTarget) {
1263
1268
  }
1264
1269
  catch { /* already exists from prior install — leave as-is */ }
1265
1270
  }
1271
+ /**
1272
+ * Install one agent that carries the tracker-server stamp: the shipped source with
1273
+ * `servers` appended to its `tools:` line, written in one step so no copy of it ever sits on
1274
+ * disk unstamped (D-TRACKER-SERVER-ALLOWLIST, src/targets/claude-code/tracker-servers.ts).
1275
+ *
1276
+ * A source that cannot take the stamp is installed as shipped and reported: an install must
1277
+ * not die on a stamp, and the unstamped agent then reaches no tracker server, which
1278
+ * `devflow tracker --servers` or a re-run of `devflow init` repairs.
1279
+ */
1280
+ async function writeStampedAgent(srcFile, target, servers, warn) {
1281
+ const source = await fs.readFile(srcFile, 'utf-8');
1282
+ const stamped = stampAgentTools(source, trackerServerTools(servers));
1283
+ if (!stamped.ok) {
1284
+ warn(`tracker servers: ${path.basename(target)} could not be stamped (${stamped.error}); installed without tracker servers`);
1285
+ await fs.copyFile(srcFile, target);
1286
+ return;
1287
+ }
1288
+ await fs.writeFile(target, stamped.value.content);
1289
+ }
1290
+ /**
1291
+ * D-EXPLORE-NO-CLOBBER, install half: true when `agentName` takes over a built-in
1292
+ * agent type (Explore) and the user already owns an agent of that type under
1293
+ * `{claudeDir}/agents`, so devflow's copy must not be installed.
1294
+ *
1295
+ * The user's file is only ever read. A copy of devflow's own left by an earlier install
1296
+ * is removed from `agentsTarget`, since two user-level agents of one type fall back to
1297
+ * file load order and the stale one could win. Every outcome that is not a conflict
1298
+ * installs: an unreadable agents directory warns and installs rather than withholding
1299
+ * the override on a guess. Warn-not-throw, so it never aborts an install.
1300
+ */
1301
+ async function skipsForUserOwnedType(agentName, claudeDir, agentsTarget, warn) {
1302
+ const agentType = BUILTIN_OVERRIDE_AGENTS.get(agentName);
1303
+ if (agentType === undefined)
1304
+ return false;
1305
+ const scan = await findForeignAgentOwner({
1306
+ agentsRoot: path.join(claudeDir, 'agents'),
1307
+ ownDir: agentsTarget,
1308
+ agentType,
1309
+ });
1310
+ if (!scan.ok) {
1311
+ warn(`Could not check ${scan.error.dir} for an existing ${agentType} agent (${scan.error.reason}); installing devflow's ${agentType} agent`);
1312
+ return false;
1313
+ }
1314
+ if (scan.value === undefined)
1315
+ return false;
1316
+ try {
1317
+ await fs.rm(path.join(agentsTarget, mdFileName(agentName)), { force: true });
1318
+ }
1319
+ catch (err) {
1320
+ warn(`Could not remove devflow's earlier ${agentType} agent — ${String(err)}`);
1321
+ }
1322
+ warn(`Skipped devflow's ${agentType} agent: ${scan.value} already defines ${agentType}, and Claude Code keys agents by name. ` +
1323
+ `That file is left untouched; delete or rename it and re-run \`devflow init\` to use devflow's.`);
1324
+ return true;
1325
+ }
1266
1326
  /**
1267
1327
  * First path in `candidates` that exists on disk, or undefined when none do.
1268
1328
  * Bounded by candidates.length. The fs.access rejection is the existence probe,
@@ -1278,6 +1338,39 @@ async function firstExisting(candidates) {
1278
1338
  }
1279
1339
  return undefined;
1280
1340
  }
1341
+ /**
1342
+ * Resolve the directory a registry skill installs from: its valid shadow, else
1343
+ * the shipped source (an invalid shadow is reported and the source is
1344
+ * installed in its place).
1345
+ *
1346
+ * The ONE spelling of that decision: the install loop and
1347
+ * convergeLearningVariants both call it, so a skill the learning toggle installs
1348
+ * later is shadowed exactly as one the install installed.
1349
+ *
1350
+ * The shipped source is stat-checked FIRST, even under a valid shadow, so an
1351
+ * absent source is a packaging failure that throws (the hard-error policy for a
1352
+ * declared source) rather than being masked by a shadow. Callers that must not
1353
+ * throw catch it.
1354
+ *
1355
+ * @param packageRoot - Package root the shipped source is read from. Injectable so
1356
+ * a caller working on a temp tree resolves the skill from the same root as its
1357
+ * other sources; omitted, it is the running package.
1358
+ */
1359
+ export async function resolveSkillSource(skillName, devflowDir, packageRoot) {
1360
+ const skillSource = path.join(skillsDir(packageRoot), skillName);
1361
+ let isDir = false;
1362
+ try {
1363
+ isDir = (await fs.stat(skillSource)).isDirectory();
1364
+ }
1365
+ catch { /* stat failed — source absent */ }
1366
+ if (!isDir) {
1367
+ throw new Error(`Skill source not found for declared skill "${skillName}": ${skillSource}. ` +
1368
+ `Ensure the skill directory exists in src/assets/skills/.`);
1369
+ }
1370
+ const shadowDir = path.join(devflowDir, 'skills', skillName);
1371
+ const shadow = await validateSkillShadow(shadowDir);
1372
+ return { dir: shadow === 'valid' ? shadowDir : skillSource, shadow };
1373
+ }
1281
1374
  /**
1282
1375
  * Registry skills that have a shadow directory under `~/.devflow/skills/`.
1283
1376
  *
@@ -1347,7 +1440,7 @@ async function removeDeselectedSkills(skillsDir, names) {
1347
1440
  * Returns an InstallReport describing which shadows were applied and which were skipped.
1348
1441
  */
1349
1442
  export async function installViaFileCopy(options) {
1350
- const { plugins, claudeDir, devflowDir, skillsMap, agentsMap, rulesMap = new Map(), isPartialInstall, spinner, warn = () => { }, } = options;
1443
+ const { plugins, claudeDir, devflowDir, skillsMap, agentsMap, rulesMap = new Map(), isPartialInstall, spinner, learning, trackerServers = TRACKER_MCP_SERVERS_DEFAULT, warn = () => { }, } = options;
1351
1444
  const report = {
1352
1445
  shadowedSkills: [],
1353
1446
  shadowedRules: [],
@@ -1484,11 +1577,13 @@ export async function installViaFileCopy(options) {
1484
1577
  warn(`Could not remove deselected skill "${prefixSkillName(failure.name)}" — ${String(failure.error)}`);
1485
1578
  }
1486
1579
  // Install commands from selected plugins using registry-driven lookup.
1487
- // Source: dist/commands/{name}.md (single lookup directory for all commands).
1580
+ // Source: dist/commands/{name}.md, or — with learning off — the learning-off
1581
+ // variant dist/learning-off/commands/{name}.md when the host has one
1582
+ // (D-LEARNING-VARIANT-INSTALL; commandSourceDirs owns the order).
1488
1583
  // A declared command with no compiled source file is a hard error, not a skip.
1489
1584
  spinner.message('Installing commands and agents...');
1490
1585
  const commandsTarget = path.join(claudeDir, 'commands', 'devflow');
1491
- const cDir = commandsDir();
1586
+ const commandDirs = commandSourceDirs(learning);
1492
1587
  const commandsSourceNames = new Set();
1493
1588
  for (const plugin of plugins) {
1494
1589
  for (const cmd of plugin.commands) {
@@ -1499,12 +1594,11 @@ export async function installViaFileCopy(options) {
1499
1594
  if (commandsSourceNames.size > 0) {
1500
1595
  await fs.mkdir(commandsTarget, { recursive: true });
1501
1596
  for (const name of commandsSourceNames) {
1502
- const srcFile = path.join(cDir, mdFileName(name));
1503
- try {
1504
- await fs.access(srcFile);
1505
- }
1506
- catch {
1507
- throw new Error(`Command source not found for declared command "${name}": ${srcFile}. ` +
1597
+ const srcFile = await firstExisting(commandDirs.map(dir => path.join(dir, mdFileName(name))));
1598
+ if (srcFile === undefined) {
1599
+ // The error names the learning-ON path: that file exists for every declared
1600
+ // command, while a learning-off file exists only for a host with an arm.
1601
+ throw new Error(`Command source not found for declared command "${name}": ${path.join(commandSourceDirs(true)[0], mdFileName(name))}. ` +
1508
1602
  `Ensure build:mds ran successfully before install.`);
1509
1603
  }
1510
1604
  await fs.copyFile(srcFile, path.join(commandsTarget, mdFileName(name)));
@@ -1514,6 +1608,18 @@ export async function installViaFileCopy(options) {
1514
1608
  // knownNames spans ALL plugins (getAllCommandNames) so commands from uninstalled
1515
1609
  // plugins survive a partial run. Only names absent from the full registry are removed.
1516
1610
  recordSweep(report, 'command', await sweepOrphanedAssets(commandsTarget, new Set(getAllCommandNames()), mdEntryName));
1611
+ // D-LANGUAGE-FOCUS-STAMP: record the language focuses the EFFECTIVE selection installs on the
1612
+ // installed /code-review, on every install shape. This is the language gate the command reads
1613
+ // (no run-time probe of Claude Code's directory), so it runs after the copy and the sweep and
1614
+ // whether or not code-review's own plugin is in this run: `--plugin=devflow-typescript` copies no
1615
+ // command, yet must update a code-review an earlier run installed. The copy above wrote the shipped
1616
+ // `(none)` line; the rewrite compares against what is on disk now, so neither that copy nor the
1617
+ // pre-clean can defeat it. A failure is a warning, never an abort.
1618
+ await restampInstalledCommands({
1619
+ claudeDir,
1620
+ effectivePlugins: options.effectivePlugins ?? plugins,
1621
+ warn,
1622
+ });
1517
1623
  // Install agents (deduplicated), resolved dist-first with a src fallback:
1518
1624
  // dist/agents/{name}.md (compiled from an .mds generator host) wins over
1519
1625
  // src/assets/agents/{name}.md. A declared agent absent from BOTH is a
@@ -1532,7 +1638,8 @@ export async function installViaFileCopy(options) {
1532
1638
  // registry via getAllAgentNames() — still treats a converged tracker.md as known
1533
1639
  // and leaves it alone.
1534
1640
  const agentsTarget = path.join(claudeDir, 'agents', 'devflow');
1535
- const agentDirs = options.agentSourceDirs ?? agentSourceDirs();
1641
+ // Learning-aware (D-LEARNING-VARIANT-INSTALL). An injected list wins as given.
1642
+ const agentDirs = options.agentSourceDirs ?? agentSourceDirs(undefined, learning);
1536
1643
  const allAgentNames = new Set();
1537
1644
  for (const plugin of plugins) {
1538
1645
  for (const agent of plugin.agents) {
@@ -1546,14 +1653,22 @@ export async function installViaFileCopy(options) {
1546
1653
  if (allAgentNames.size > 0) {
1547
1654
  await fs.mkdir(agentsTarget, { recursive: true });
1548
1655
  for (const agentName of allAgentNames) {
1656
+ if (await skipsForUserOwnedType(agentName, claudeDir, agentsTarget, warn))
1657
+ continue;
1549
1658
  const candidates = agentDirs.map(dir => path.join(dir, mdFileName(agentName)));
1550
1659
  const srcFile = await firstExisting(candidates);
1551
1660
  if (srcFile === undefined) {
1552
- throw new Error(`Agent source not found for declared agent "${agentName}": ${candidates[0]}. ` +
1661
+ throw new Error(`Agent source not found for declared agent "${agentName}": ${path.join(options.agentSourceDirs?.[0] ?? agentSourceDirs()[0], mdFileName(agentName))}. ` +
1553
1662
  `Run \`npm run build:mds\` if it is compiled from an .mds generator host, otherwise ` +
1554
1663
  `ensure the agent file exists in src/assets/agents/ (searched: ${candidates.join(', ')}).`);
1555
1664
  }
1556
- await fs.copyFile(srcFile, path.join(agentsTarget, mdFileName(agentName)));
1665
+ const agentTarget = path.join(agentsTarget, mdFileName(agentName));
1666
+ if (isTrackerStampedAgent(agentName)) {
1667
+ await writeStampedAgent(srcFile, agentTarget, trackerServers, warn);
1668
+ }
1669
+ else {
1670
+ await fs.copyFile(srcFile, agentTarget);
1671
+ }
1557
1672
  }
1558
1673
  }
1559
1674
  // Sweep stale agent files — ungated: runs on every install shape.
@@ -1564,33 +1679,21 @@ export async function installViaFileCopy(options) {
1564
1679
  // Resolved from flat src/assets/skills/{name}/ (no per-plugin subdirectory).
1565
1680
  // A declared skill whose source directory is absent is a build/packaging failure
1566
1681
  // and throws rather than silently skipping (matches command pattern).
1682
+ //
1683
+ // D-LEARNING-VARIANT-INSTALL: a learning-off machine skips the learning-gated
1684
+ // skills here. The pre-clean above walked the UNFILTERED map, so on a full
1685
+ // install a leftover copy is already gone; a partial install leaves removal to
1686
+ // convergeLearningVariants, which init runs straight after this call.
1567
1687
  spinner.message('Installing skills...');
1568
- for (const [skillName] of skillsMap) {
1569
- const skillSource = path.join(skillsDir(), skillName);
1570
- let isDir = false;
1571
- try {
1572
- const stat = await fs.stat(skillSource);
1573
- isDir = stat.isDirectory();
1574
- }
1575
- catch { /* stat failed — source absent */ }
1576
- if (!isDir) {
1577
- throw new Error(`Skill source not found for declared skill "${skillName}": ${skillSource}. ` +
1578
- `Ensure the skill directory exists in src/assets/skills/.`);
1579
- }
1580
- const shadowDir = path.join(devflowDir, 'skills', skillName);
1581
- const prefixedName = prefixSkillName(skillName);
1582
- const skillTarget = path.join(claudeDir, 'skills', prefixedName);
1583
- const shadowState = await validateSkillShadow(shadowDir);
1584
- if (shadowState === 'valid') {
1585
- await copyDirectory(shadowDir, skillTarget);
1688
+ for (const [skillName] of omitLearningGatedSkills(skillsMap, learning)) {
1689
+ const resolved = await resolveSkillSource(skillName, devflowDir);
1690
+ const skillTarget = path.join(claudeDir, 'skills', prefixSkillName(skillName));
1691
+ await copyDirectory(resolved.dir, skillTarget);
1692
+ if (resolved.shadow === 'valid') {
1586
1693
  report.shadowedSkills.push(skillName);
1587
1694
  }
1588
- else if (shadowState === 'missing-skill-md') {
1695
+ else if (resolved.shadow === 'missing-skill-md') {
1589
1696
  report.skippedShadows.push({ kind: 'skill', name: skillName, reason: 'missing-skill-md' });
1590
- await copyDirectory(skillSource, skillTarget);
1591
- }
1592
- else {
1593
- await copyDirectory(skillSource, skillTarget);
1594
1697
  }
1595
1698
  // Converge the generated references onto the skill that was just installed. One call
1596
1699
  // site downstream of all three branches above, so a shadowed devflow:git receives the