devflow-kit 2.5.0 → 3.0.1

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 +82 -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 +246 -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,8 +1,18 @@
1
1
  /**
2
2
  * Compliance artifact installer for the Claude Code target.
3
3
  *
4
- * Convergence function: installs or removes the compliance skill directory
5
- * and rule file based on the current feature state.
4
+ * Convergence function: installs the compliance skill directory on every
5
+ * machine and installs or removes the rule file based on the current feature state.
6
+ *
7
+ * D-COMPLIANCE-INSTALL-ALWAYS: the skill and all six framework references are
8
+ * installed whatever the machine's own selection, because a repository can turn
9
+ * the review lens on by itself (`compliance` in `.devflow/project.json`, folded
10
+ * into the `COMPLIANCE` field of the settings line). What the machine switch still
11
+ * owns is the RULE — the one artifact Claude Code loads into every prompt — and
12
+ * the stamp on SKILL.md: the machine's frameworks when compliance is on, the
13
+ * neutral zero-framework stamp when it is off. Which references a run loads is
14
+ * decided by the ids its caller passes (D-COMPLIANCE-REPO-LENS), never by which
15
+ * files are present.
6
16
  *
7
17
  * Applies ADR-013: I/O orchestration in src/targets/; pure helpers in src/core/.
8
18
  * Applies PF-009: warn-not-throw for per-item failures.
@@ -12,10 +22,12 @@
12
22
  import { promises as fs } from 'fs';
13
23
  import * as path from 'path';
14
24
  import { skillsDir, rulesDir } from '../../core/assets.js';
15
- import { ALWAYS_PRESENT_REFS, normalizeFrameworks } from '../../core/compliance.js';
25
+ import { ALWAYS_PRESENT_REFS, COMPLIANCE_FRAMEWORKS, normalizeFrameworks } from '../../core/compliance.js';
16
26
  import { parseComplianceFragment, composeComplianceSkill, composeComplianceRule } from '../../core/compliance-compose.js';
17
27
  import { validateSkillShadow, validateRuleShadow } from './installer.js';
18
28
  // ── Path helpers ───────────────────────────────────────────────────────────
29
+ /** Every registry framework id — the reference set every install carries. */
30
+ const ALL_FRAMEWORK_IDS = COMPLIANCE_FRAMEWORKS.map(fw => fw.id);
19
31
  /** Installed compliance skill dir: {claudeDir}/skills/devflow:compliance/ */
20
32
  function skillTarget(claudeDir) {
21
33
  return path.join(claudeDir, 'skills', 'devflow:compliance');
@@ -67,13 +79,15 @@ async function loadComplianceFragments(canonicalSrc, frameworks, warn) {
67
79
  }
68
80
  // ── Skill installer ────────────────────────────────────────────────────────
69
81
  /**
70
- * Install the compliance skill directory (selective references).
82
+ * Install the compliance skill directory (every reference).
71
83
  *
72
84
  * SKILL.md source: shadow at {devflowDir}/skills/compliance/SKILL.md (when valid),
73
- * otherwise canonical src/assets/skills/compliance/SKILL.md.
85
+ * otherwise canonical src/assets/skills/compliance/SKILL.md. It is composed with
86
+ * `stampFrameworks` — the machine's selection, or none for the neutral stamp.
74
87
  *
75
- * Reference files installed: ALWAYS_PRESENT_REFS + one {id}.md per selected framework.
76
- * References always come from the canonical source — framework refs are not user-overridable.
88
+ * Reference files installed: ALWAYS_PRESENT_REFS + one {id}.md for EVERY registry
89
+ * framework (D-COMPLIANCE-INSTALL-ALWAYS). References always come from the canonical
90
+ * source — framework refs are not user-overridable.
77
91
  *
78
92
  * `fragments` is loaded once by convergeComplianceArtifacts and shared with the rule
79
93
  * installer — the SKILL.md and the rule compose from the same parsed set.
@@ -81,7 +95,7 @@ async function loadComplianceFragments(canonicalSrc, frameworks, warn) {
81
95
  * Applies PF-011: build under a .tmp sibling, remove old target, rename.
82
96
  * Applies PF-009: unexpected I/O failures are reported via warn; never thrown.
83
97
  */
84
- async function installSkillDir(claudeDir, devflowDir, frameworks, fragments, warn) {
98
+ async function installSkillDir(claudeDir, devflowDir, stampFrameworks, fragments, warn) {
85
99
  const canonicalSrc = path.join(skillsDir(), 'compliance');
86
100
  const target = skillTarget(claudeDir);
87
101
  const tmpTarget = `${target}.tmp`;
@@ -100,7 +114,7 @@ async function installSkillDir(claudeDir, devflowDir, frameworks, fragments, war
100
114
  // SKILL.md: compose from template (shadow or canonical) + fragments.
101
115
  // C1: a shadow without tokens passes through byte-identical.
102
116
  const templateContent = await fs.readFile(skillMdSrc, 'utf-8');
103
- const { content: composedSkill, warnings: skillWarnings } = composeComplianceSkill(templateContent, frameworks, fragments);
117
+ const { content: composedSkill, warnings: skillWarnings } = composeComplianceSkill(templateContent, stampFrameworks, fragments);
104
118
  for (const w of skillWarnings)
105
119
  warn(`compliance: ${w}`);
106
120
  await fs.writeFile(path.join(tmpTarget, 'SKILL.md'), composedSkill, 'utf-8');
@@ -121,9 +135,10 @@ async function installSkillDir(claudeDir, devflowDir, frameworks, fragments, war
121
135
  }
122
136
  // Per-framework reference files: source is frameworks/{id}/reference.md,
123
137
  // destination is references/{id}.md (installed artifact layout unchanged — C1
124
- // for consumers). Every id here is a registry-validated bare name — no separator,
125
- // no traversal — from normalizeFrameworks in convergeComplianceArtifacts.
126
- for (const fw of frameworks) {
138
+ // for consumers). Every registry id, whatever the machine selected: a repository
139
+ // may declare any of them. The ids come from the static registry — no separator,
140
+ // no traversal. C7: bounded by the registry's six entries.
141
+ for (const fw of ALL_FRAMEWORK_IDS) {
127
142
  const srcRef = path.join(canonicalSrc, 'frameworks', fw, 'reference.md');
128
143
  const dstRef = path.join(refDst, `${fw}.md`);
129
144
  try {
@@ -183,10 +198,10 @@ async function installRuleFile(claudeDir, devflowDir, frameworks, fragments, war
183
198
  /**
184
199
  * Converge compliance artifacts in the Claude Code install target.
185
200
  *
186
- * Convergence matrix:
187
- * enabled + rulesEnabled → install skill dir (selective refs) + stamped rule
188
- * enabled + !rulesEnabled → install skill dir only; remove stale rule
189
- * !enabled → remove both artifacts (warn-not-throw per PF-009)
201
+ * Convergence matrix (D-COMPLIANCE-INSTALL-ALWAYS):
202
+ * enabled + rulesEnabled → skill dir (every ref, machine stamp) + stamped rule
203
+ * enabled + !rulesEnabled → skill dir (every ref, machine stamp); remove stale rule
204
+ * !enabled → skill dir (every ref, neutral stamp); remove rule
190
205
  *
191
206
  * PF-015: both artifact operations execute unconditionally — no || short-circuits.
192
207
  * PF-011: skill dir write uses temp-sibling+rename to avoid ENOENT windows.
@@ -218,63 +233,32 @@ export async function convergeComplianceArtifacts(opts) {
218
233
  converged = false;
219
234
  warn(msg);
220
235
  };
221
- // ── Disable path ─────────────────────────────────────────────────────────
222
- if (!enabled) {
223
- // Detect pre-existing artifacts BEFORE removal (to set removedPreexisting).
224
- const skillExisted = await pathExists(skillTarget(claudeDir));
225
- const ruleExisted = await pathExists(ruleTarget(claudeDir));
226
- // PF-015: collect results independently — one failure must not skip the other.
227
- let skillErr = null;
228
- let ruleErr = null;
229
- // Step 1: attempt skill dir removal
230
- try {
231
- if (skillExisted) {
232
- await fs.rm(skillTarget(claudeDir), { recursive: true, force: true });
233
- }
234
- }
235
- catch (err) {
236
- skillErr = String(err);
237
- }
238
- // Step 2: attempt rule removal (runs regardless of Step 1 outcome — PF-015)
239
- try {
240
- if (ruleExisted) {
241
- await fs.rm(ruleTarget(claudeDir), { force: true });
242
- }
243
- }
244
- catch (err) {
245
- ruleErr = String(err);
246
- }
247
- // PF-009: warn after BOTH attempts so neither failure blocks the other.
248
- if (skillErr !== null) {
249
- trackingWarn(`compliance: failed to remove skill dir — ${skillErr}`);
250
- }
251
- if (ruleErr !== null) {
252
- trackingWarn(`compliance: failed to remove rule — ${ruleErr}`);
253
- }
254
- return { removedPreexisting: skillExisted || ruleExisted, converged };
255
- }
256
- // ── Enable path ──────────────────────────────────────────────────────────
257
- //
258
- // PF-015: installSkillDir and the rule step are independent operations.
259
- // An error in installSkillDir is caught internally and reported via trackingWarn,
260
- // so execution always continues to the rule step.
261
236
  // Fragments are read and parsed once per convergence and shared by both artifacts:
262
237
  // they are the same registry-owned files either way, so parsing twice would only
263
- // duplicate the I/O and report each malformed fragment twice.
264
- const fragments = await loadComplianceFragments(path.join(skillsDir(), 'compliance'), safeFrameworks, trackingWarn);
265
- await installSkillDir(claudeDir, devflowDir, safeFrameworks, fragments, trackingWarn);
266
- if (rulesEnabled) {
238
+ // duplicate the I/O and report each malformed fragment twice. Only the stamped
239
+ // frameworks need one — a compliance-off machine stamps none.
240
+ const stampFrameworks = enabled ? safeFrameworks : [];
241
+ const fragments = await loadComplianceFragments(path.join(skillsDir(), 'compliance'), stampFrameworks, trackingWarn);
242
+ // PF-015: the skill and the rule are independent operations. An error in
243
+ // installSkillDir is caught internally and reported via trackingWarn, so
244
+ // execution always continues to the rule step.
245
+ await installSkillDir(claudeDir, devflowDir, stampFrameworks, fragments, trackingWarn);
246
+ if (enabled && rulesEnabled) {
267
247
  await installRuleFile(claudeDir, devflowDir, safeFrameworks, fragments, trackingWarn);
248
+ return { removedPreexisting: false, converged };
268
249
  }
269
- else {
270
- // Rules disabled: remove any stale rule left from a prior enabled run.
271
- // Ignore ENOENT (force: true) — absence is the desired end state.
250
+ // Compliance off, or rules off: no rule. Probe first so a disable convergence
251
+ // can report that it removed one; absence is already the desired end state.
252
+ const ruleExisted = await pathExists(ruleTarget(claudeDir));
253
+ if (ruleExisted) {
272
254
  try {
273
255
  await fs.rm(ruleTarget(claudeDir), { force: true });
274
256
  }
275
- catch { /* absent = already in desired end state */ }
257
+ catch (err) {
258
+ trackingWarn(`compliance: failed to remove rule — ${String(err)}`);
259
+ }
276
260
  }
277
- return { removedPreexisting: false, converged };
261
+ return { removedPreexisting: !enabled && ruleExisted, converged };
278
262
  }
279
263
  // ── Manifest-slice wrapper ─────────────────────────────────────────────────
280
264
  /**
@@ -1,9 +1,114 @@
1
1
  /**
2
- * Shared hook types for Claude Code settings.json.
3
- * Used by learn.ts, ambient.ts, and memory.ts.
2
+ * Shared hook types and hook-ownership helpers for Claude Code settings.json.
3
+ * Used by every module that registers or removes a devflow hook (ambient.ts,
4
+ * capture.ts, memory.ts, context.ts, proxy.ts, legacy-hooks.ts).
4
5
  *
5
6
  * NOTE: hud.ts uses a structurally different Settings type (statusLine, not hooks)
6
7
  * and is intentionally excluded from this shared module.
7
8
  */
8
- export {};
9
+ import * as path from 'path';
10
+ /** The directory, relative to a devflow root, that holds every hook devflow registers. */
11
+ export const HOOKS_DIR_SUFFIX = '/scripts/hooks/';
12
+ /** The command ending devflow writes for a `run-hook <marker>` hook. */
13
+ export function runHookSuffix(marker) {
14
+ return `${HOOKS_DIR_SUFFIX}run-hook ${marker}`;
15
+ }
16
+ /** The command devflow registers for the `run-hook <marker>` hook under `devflowDir`. */
17
+ export function runHookCommand(devflowDir, marker) {
18
+ return `${path.join(devflowDir, 'scripts', 'hooks', 'run-hook')} ${marker}`;
19
+ }
20
+ /**
21
+ * A predicate matching a hook whose command ends in any of `suffixes`, read with
22
+ * surrounding whitespace trimmed and backslashes as slashes (a Windows install).
23
+ * A missing or non-string command — a hand-edited settings.json — matches nothing.
24
+ */
25
+ export function endsWithAny(suffixes) {
26
+ return (hook) => {
27
+ const raw = hook.command;
28
+ if (typeof raw !== 'string')
29
+ return false;
30
+ const command = raw.trim().replace(/\\/g, '/');
31
+ return suffixes.some((suffix) => command.endsWith(suffix));
32
+ };
33
+ }
34
+ /**
35
+ * The ownership predicate for devflow's `run-hook <marker>` hooks.
36
+ *
37
+ * D-EXACT-HOOK-OWNER: a hook is devflow's when its command ENDS in
38
+ * `/scripts/hooks/run-hook <marker>` for one of `markers`, or in one of the
39
+ * module's named `legacySuffixes` (a form an earlier release registered, such as
40
+ * `/scripts/hooks/session-start-memory.sh`), under any directory — so installs made
41
+ * under a custom or retired devflow directory are still recognised. It is never
42
+ * devflow's because it merely CONTAINS a marker word: a user's `~/bin/memory-worker`,
43
+ * `echo capture-turn` or `/opt/tools/run-hook preamble` is theirs (applies ADR-024 —
44
+ * remove only what devflow can prove it wrote). Removal goes through `removeHooks`,
45
+ * one hook at a time. Every hook module builds its predicates here;
46
+ * D-AMBIENT-EXACT-HOOK is the ambient instance of this rule.
47
+ */
48
+ export function devflowHookOwner(markers, legacySuffixes = []) {
49
+ return endsWithAny([...markers.map(runHookSuffix), ...legacySuffixes]);
50
+ }
51
+ /** A matcher group's hooks, or an empty list for a hand-edited group of another shape. */
52
+ function hooksOf(matcher) {
53
+ return Array.isArray(matcher?.hooks) ? matcher.hooks : [];
54
+ }
55
+ /**
56
+ * Remove every hook matching `shouldRemove` from one event's matcher groups.
57
+ * Mutates `settings` (callers pass their own parsed copy). Returns true if any hook
58
+ * was removed.
59
+ *
60
+ * D-EXACT-HOOK-OWNER: removal is per HOOK, not per matcher group — a group keeps
61
+ * the user's sibling hooks, in their order, and is dropped only when nothing is left
62
+ * in it. A group whose `hooks` is not an array is kept as is. Empty event arrays and
63
+ * an empty `hooks` object are cleaned up.
64
+ */
65
+ export function removeHooks(settings, eventName, shouldRemove) {
66
+ const matchers = settings.hooks?.[eventName];
67
+ if (!settings.hooks || !Array.isArray(matchers))
68
+ return false;
69
+ let removed = false;
70
+ const kept = [];
71
+ for (const matcher of matchers) {
72
+ const hooks = hooksOf(matcher);
73
+ const remaining = hooks.filter((hook) => !shouldRemove(hook));
74
+ if (remaining.length === hooks.length) {
75
+ kept.push(matcher);
76
+ continue;
77
+ }
78
+ removed = true;
79
+ if (remaining.length > 0)
80
+ kept.push({ ...matcher, hooks: remaining });
81
+ }
82
+ if (!removed)
83
+ return false;
84
+ if (kept.length === 0) {
85
+ delete settings.hooks[eventName];
86
+ }
87
+ else {
88
+ settings.hooks[eventName] = kept;
89
+ }
90
+ if (Object.keys(settings.hooks).length === 0) {
91
+ delete settings.hooks;
92
+ }
93
+ return true;
94
+ }
95
+ /** Whether any hook registered for `eventName` matches `isOurs`. */
96
+ export function hasHook(settings, eventName, isOurs) {
97
+ const matchers = settings.hooks?.[eventName];
98
+ return Array.isArray(matchers) && matchers.some((m) => hooksOf(m).some(isOurs));
99
+ }
100
+ /**
101
+ * Append `entry` as a new matcher group for `eventName` unless a hook matching
102
+ * `isOurs` is already registered there. Mutates `settings`. Returns true when the
103
+ * entry was added.
104
+ */
105
+ export function ensureHook(settings, eventName, isOurs, entry) {
106
+ if (hasHook(settings, eventName, isOurs)) {
107
+ return false;
108
+ }
109
+ settings.hooks ??= {};
110
+ settings.hooks[eventName] ??= [];
111
+ settings.hooks[eventName].push(entry);
112
+ return true;
113
+ }
9
114
  //# sourceMappingURL=hooks.js.map
@@ -215,11 +215,10 @@ export async function chmodRecursive(dir, mode, _depth = 0) {
215
215
  * `tracker/` ({@link TRACKER_DESTINATION_ROOT}) holds the per-provider mechanics and `pr/`
216
216
  * ({@link PR_HOST_DESTINATION_ROOT}) the PR/review host bodies; the build emits both
217
217
  * wholesale, so anything inside them the manifest does not name is by construction a
218
- * leftover — a retired op, a provider the selection dropped, a shadow-supplied file, a
218
+ * leftover — a retired op, a provider the registry dropped, a shadow-supplied file, a
219
219
  * staging tree a crashed run stranded — and removing it is the only way the installed
220
- * tree can equal the manifest. `pr/` is wanted under EVERY provider (applies ADR-026), so
221
- * a provider switch neither adds nor removes the directory; what it converges is the
222
- * directory's CONTENTS, exactly as `tracker/`'s are converged. Both entries are the
220
+ * tree can equal the manifest. Every install carries both whole
221
+ * (D-INSTALL-ALL-PROVIDERS), so what the prune converges is each directory's CONTENTS. Both entries are the
223
222
  * registry's own constants, so a renamed destination root moves the build and the prune
224
223
  * together.
225
224
  *
@@ -1067,42 +1066,6 @@ export async function overlayGeneratedReferences(opts) {
1067
1066
  }
1068
1067
  return { overlaidRefs, unchangedRefs, overlayFailures, pruned };
1069
1068
  }
1070
- /**
1071
- * Converge the installed `devflow:git` references onto ONE provider's install set.
1072
- *
1073
- * The provider-scoped entry point to {@link overlayGeneratedReferences}: it
1074
- * resolves the install manifest and the target directory from a claudeDir and a
1075
- * provider, and changes nothing else. There is exactly ONE overlay spelling in
1076
- * this codebase and this is its only wrapper — `devflow init` reaches the
1077
- * overlay through `installViaFileCopy`, `devflow tracker --set` reaches it
1078
- * through here, and both converge to the same manifest for the same provider.
1079
- *
1080
- * Convergence is two-directional by construction, because the underlying overlay
1081
- * PRUNES everything under its converged subtrees the manifest does not name: a
1082
- * jira → github change removes the jira tree and `_mcp.md` in the same call that
1083
- * refreshes the github tree (applies PF-015). `references/pr/` is wanted under
1084
- * every provider (applies ADR-026), so a provider change leaves it standing —
1085
- * converged, not removed.
1086
- *
1087
- * Throws on an absent generated tree, exactly as its callee does — that is a
1088
- * build artifact that was never produced, not an I/O degradation, and the
1089
- * refusal lands before the target directory is created so a refused overlay
1090
- * leaves the install as it found it.
1091
- *
1092
- * @param opts.provider - The RESOLVED tracker provider id.
1093
- * @param opts.referencesRoot - The GENERATED tree to install from; defaults to
1094
- * `compiledSkillRefsDir()`. Injectable so the absent-tree refusal is provable
1095
- * without deleting `dist/` out from under a concurrent test run (applies
1096
- * PF-013 — a seam the caller can drive, not a global the test has to break).
1097
- */
1098
- export async function overlayInstalledReferences(opts) {
1099
- return overlayGeneratedReferences({
1100
- referencesTarget: path.join(opts.claudeDir, 'skills', prefixSkillName(SKILL_REFS_SKILL_NAME), 'references'),
1101
- sourceRoot: opts.referencesRoot,
1102
- manifest: installedReferenceManifest({ provider: opts.provider }),
1103
- warn: opts.warn,
1104
- });
1105
- }
1106
1069
  /** The directory inside an installed skill that the reference overlay converges. */
1107
1070
  const SKILL_REFERENCES_DIRNAME = 'references';
1108
1071
  /**
@@ -1229,11 +1192,24 @@ function collectRelativeImports(source) {
1229
1192
  export async function composeScripts(scriptsTarget) {
1230
1193
  await fs.mkdir(scriptsTarget, { recursive: true });
1231
1194
  // (a) src/assets/scripts/ verbatim
1195
+ //
1196
+ // D-SCRIPTS-EXEC-SCOPE: only what this step copied is made executable — each of the
1197
+ // source tree's top-level entries, at its destination. A chmod over the whole target
1198
+ // would also reach what (b) and (c) wrote there on an earlier run (package.json, the
1199
+ // mirrored dist/hud/ closure), so a re-run gave package.json an exec bit the first
1200
+ // run never did and re-init stopped being a no-op on disk (#388 AC-3). Scoping the
1201
+ // chmod to the copied entries makes the first and every later run agree.
1232
1202
  const srcScripts = scriptsDir();
1233
1203
  try {
1234
1204
  await copyDirectory(srcScripts, scriptsTarget);
1235
1205
  if (process.platform !== 'win32') {
1236
- await chmodRecursive(scriptsTarget, 0o755);
1206
+ for (const entry of await fs.readdir(srcScripts, { withFileTypes: true })) {
1207
+ const copied = path.join(scriptsTarget, entry.name);
1208
+ if (entry.isDirectory())
1209
+ await chmodRecursive(copied, 0o755);
1210
+ else if (entry.isFile())
1211
+ await fs.chmod(copied, 0o755);
1212
+ }
1237
1213
  }
1238
1214
  }
1239
1215
  catch { /* scripts dir may not exist yet during development */ }
@@ -1392,11 +1368,11 @@ export async function installViaFileCopy(options) {
1392
1368
  // needed (D-OVERLAY-OWNERSHIP), for the same reason. The agent directory is
1393
1369
  // emptied AROUND the one file `convergeTrackerArtifacts` owns: taking it
1394
1370
  // would leave converge with nothing to byte-compare against, so a
1395
- // steady-state jira re-init would re-copy the agent and announce
1371
+ // steady-state re-init would re-copy the agent and announce
1396
1372
  // `tracker agent installed` on every run. Everything else is removed
1397
1373
  // exactly as the unconditional wipe removed it, and the file is still
1398
- // converged on this run — under github converge deletes it, and drift in it
1399
- // is restored, so preserving it strands nothing.
1374
+ // converged on this run — drift in it is restored, so preserving it strands
1375
+ // nothing.
1400
1376
  try {
1401
1377
  await emptyDirectoryExcept(path.join(claudeDir, 'agents', 'devflow'), new Set([mdFileName(TRACKER_AGENT_NAME)]));
1402
1378
  }
@@ -1451,7 +1427,7 @@ export async function installViaFileCopy(options) {
1451
1427
  // file — including a reference at the references ROOT, which the overlay may replace
1452
1428
  // but never delete (D-OVERLAY-FLAT-UNIT) — does not survive a full install.
1453
1429
  if (!isPartialInstall) {
1454
- const overlayOwned = overlayOwnedSkillPaths(installedReferenceManifest({ provider: options.trackerProvider }));
1430
+ const overlayOwned = overlayOwnedSkillPaths(installedReferenceManifest());
1455
1431
  for (const skill of skillsMap.keys()) {
1456
1432
  // Empty the prefixed directory (its contents are re-created during the install
1457
1433
  // phase), minus whatever another converger owns inside it.
@@ -1517,14 +1493,12 @@ export async function installViaFileCopy(options) {
1517
1493
  // build/packaging failure and throws rather than silently skipping (matches
1518
1494
  // command pattern); the message names the build step as well as the tree.
1519
1495
  //
1520
- // D-TRACKER-AGENT-OWNER: every declared agent but ONE. The Tracker agent's
1521
- // presence is conditional on the resolved provider, and `convergeTrackerArtifacts`
1522
- // owns that decision alone (plan A3) — it runs after this function in init and is
1523
- // the sole caller in `devflow tracker --set`. Copying it here too made every
1524
- // install do the work twice and the two owners contradict each other in both
1525
- // directions: a github run reported `tracker agent removed` for a file only that
1526
- // same run had written, and a fresh jira install never reported `installed`
1527
- // because converge found this loop's byte-identical copy already in place.
1496
+ // D-TRACKER-AGENT-OWNER: every declared agent but ONE. The Tracker agent is
1497
+ // converged by `convergeTrackerArtifacts` alone (plan A3), which runs after this
1498
+ // function in init and reports whether this run wrote it. Copying it here too
1499
+ // would make every install do the work twice, and a fresh install would never
1500
+ // report `installed` because converge would find this loop's byte-identical copy
1501
+ // already in place.
1528
1502
  //
1529
1503
  // The name is skipped from the COPY set only. It stays declared in
1530
1504
  // `devflow-core-skills.agents`, so the sweep below — which keys on the full
@@ -1598,10 +1572,9 @@ export async function installViaFileCopy(options) {
1598
1572
  if (skillName === SKILL_REFS_SKILL_NAME) {
1599
1573
  const overlay = await overlayGeneratedReferences({
1600
1574
  referencesTarget: path.join(skillTarget, 'references'),
1601
- // Only the tracker mechanics this install can reach: {github} ∪ the
1602
- // selected provider. The overlay converges rather than merges, so a
1603
- // provider left behind by a previous selection is pruned here.
1604
- manifest: installedReferenceManifest({ provider: options.trackerProvider }),
1575
+ // Every provider's mechanics (D-INSTALL-ALL-PROVIDERS). The overlay
1576
+ // converges rather than merges, so a retired generated document is pruned.
1577
+ manifest: installedReferenceManifest(),
1605
1578
  warn,
1606
1579
  });
1607
1580
  report.overlaidRefs.push(...overlay.overlaidRefs);