devflow-kit 3.9.0 → 3.11.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 (168) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/README.md +1 -1
  3. package/dist/agents/code.md +6 -6
  4. package/dist/agents/fix.md +6 -6
  5. package/dist/agents/harden.md +7 -6
  6. package/dist/agents/refactor.md +6 -6
  7. package/dist/cli/commands/ambient.js +10 -2
  8. package/dist/cli/commands/backup-session.js +131 -0
  9. package/dist/cli/commands/compliance-prompts.js +9 -4
  10. package/dist/cli/commands/compliance.js +184 -24
  11. package/dist/cli/commands/curation.js +23 -10
  12. package/dist/cli/commands/gate-level-prompts.js +39 -25
  13. package/dist/cli/commands/gates.js +5 -7
  14. package/dist/cli/commands/init.js +169 -184
  15. package/dist/cli/commands/install-report.js +129 -0
  16. package/dist/cli/commands/legacy-sweep.js +159 -0
  17. package/dist/cli/commands/proxy.js +6 -1
  18. package/dist/cli/commands/rules.js +75 -21
  19. package/dist/cli/commands/skills.js +42 -9
  20. package/dist/cli/commands/tracker.js +9 -3
  21. package/dist/cli/commands/uninstall.js +339 -75
  22. package/dist/commands/bug-analysis.md +69 -120
  23. package/dist/commands/dynamic-build.md +23 -23
  24. package/dist/commands/implement.md +46 -32
  25. package/dist/commands/release.md +1 -1
  26. package/dist/commands/resolve.md +35 -31
  27. package/dist/commands/self-review.md +8 -7
  28. package/dist/compliance-off/agents/git.md +120 -0
  29. package/dist/compliance-off/agents/triage.md +180 -0
  30. package/dist/compliance-off/commands/bug-analysis.md +398 -0
  31. package/dist/compliance-off/commands/code-review.md +520 -0
  32. package/dist/compliance-off/commands/dynamic-build.md +1251 -0
  33. package/dist/compliance-off/commands/dynamic-tickets.md +676 -0
  34. package/dist/compliance-off/commands/implement.md +736 -0
  35. package/dist/compliance-off/commands/plan.md +671 -0
  36. package/dist/compliance-off/commands/release.md +309 -0
  37. package/dist/compliance-off/commands/resolve.md +742 -0
  38. package/dist/compliance-off/skills/git/references/decision-markers.md +17 -0
  39. package/dist/compliance-off/skills/git/references/learn-conventions.md +80 -0
  40. package/dist/compliance-off/skills/git/references/pr/check-ci-status.md +33 -0
  41. package/dist/compliance-off/skills/git/references/pr/check-merge-readiness.md +49 -0
  42. package/dist/compliance-off/skills/git/references/pr/fetch-review-threads.md +52 -0
  43. package/dist/compliance-off/skills/git/references/pr/validate-branch.md +47 -0
  44. package/dist/compliance-off/skills/git/references/tracker/_contract.md +33 -0
  45. package/dist/compliance-off/skills/git/references/tracker/_mcp.md +153 -0
  46. package/dist/compliance-off/skills/git/references/tracker/github/create-release.md +35 -0
  47. package/dist/compliance-off/skills/git/references/tracker/github/ensure-pr-ready.md +100 -0
  48. package/dist/compliance-off/skills/git/references/tracker/github/ensure-traceable-issue.md +74 -0
  49. package/dist/compliance-off/skills/git/references/tracker/github/fetch-issue.md +69 -0
  50. package/dist/compliance-off/skills/git/references/tracker/github/fetch-issues-batch.md +65 -0
  51. package/dist/compliance-off/skills/git/references/tracker/github/gather-release-evidence.md +48 -0
  52. package/dist/compliance-off/skills/git/references/tracker/github/setup-task.md +77 -0
  53. package/dist/compliance-off/skills/git/references/tracker/jira/create-release.md +35 -0
  54. package/dist/compliance-off/skills/git/references/tracker/jira/ensure-pr-ready.md +100 -0
  55. package/dist/compliance-off/skills/git/references/tracker/jira/ensure-traceable-issue.md +61 -0
  56. package/dist/compliance-off/skills/git/references/tracker/jira/fetch-issue.md +51 -0
  57. package/dist/compliance-off/skills/git/references/tracker/jira/fetch-issues-batch.md +63 -0
  58. package/dist/compliance-off/skills/git/references/tracker/jira/gather-release-evidence.md +47 -0
  59. package/dist/compliance-off/skills/git/references/tracker/jira/setup-task.md +82 -0
  60. package/dist/compliance-off/skills/git/references/tracker/linear/create-release.md +35 -0
  61. package/dist/compliance-off/skills/git/references/tracker/linear/ensure-pr-ready.md +100 -0
  62. package/dist/compliance-off/skills/git/references/tracker/linear/ensure-traceable-issue.md +61 -0
  63. package/dist/compliance-off/skills/git/references/tracker/linear/fetch-issue.md +51 -0
  64. package/dist/compliance-off/skills/git/references/tracker/linear/fetch-issues-batch.md +63 -0
  65. package/dist/compliance-off/skills/git/references/tracker/linear/gather-release-evidence.md +47 -0
  66. package/dist/compliance-off/skills/git/references/tracker/linear/setup-task.md +83 -0
  67. package/dist/compliance-off/skills/git/references/trust-rule.md +7 -0
  68. package/dist/core/agent-models.js +19 -4
  69. package/dist/core/assets.js +126 -17
  70. package/dist/core/compliance-off-files.js +27 -0
  71. package/dist/core/compliance.js +14 -0
  72. package/dist/core/curation-variants.js +460 -112
  73. package/dist/core/flags.js +2 -2
  74. package/dist/core/fs-atomic.js +5 -1
  75. package/dist/core/installed-variants.js +63 -0
  76. package/dist/core/manifest.js +48 -5
  77. package/dist/core/mds-variants.js +700 -3
  78. package/dist/core/orphan-sweep.js +29 -2
  79. package/dist/core/path-set.js +15 -0
  80. package/dist/core/plugins.js +68 -10
  81. package/dist/core/reference-sweep.js +42 -25
  82. package/dist/core/retired-shipped-names.js +278 -0
  83. package/dist/curation-compliance-off/agents/triage.md +176 -0
  84. package/dist/curation-compliance-off/commands/bug-analysis.md +349 -0
  85. package/dist/curation-compliance-off/commands/code-review.md +481 -0
  86. package/dist/curation-compliance-off/commands/dynamic-build.md +1211 -0
  87. package/dist/curation-compliance-off/commands/dynamic-tickets.md +634 -0
  88. package/dist/curation-compliance-off/commands/implement.md +689 -0
  89. package/dist/curation-compliance-off/commands/plan.md +631 -0
  90. package/dist/curation-compliance-off/commands/release.md +279 -0
  91. package/dist/curation-compliance-off/commands/resolve.md +692 -0
  92. package/dist/curation-off/agents/code.md +6 -6
  93. package/dist/curation-off/agents/fix.md +6 -6
  94. package/dist/curation-off/agents/harden.md +7 -6
  95. package/dist/curation-off/agents/refactor.md +6 -6
  96. package/dist/curation-off/commands/bug-analysis.md +69 -119
  97. package/dist/curation-off/commands/dynamic-build.md +23 -23
  98. package/dist/curation-off/commands/implement.md +46 -32
  99. package/dist/curation-off/commands/release.md +1 -1
  100. package/dist/curation-off/commands/resolve.md +35 -31
  101. package/dist/curation-off/commands/self-review.md +8 -7
  102. package/dist/skills/git/references/pr/resolve-review-threads.md +1 -1
  103. package/dist/targets/claude-code/behavior-install.js +23 -9
  104. package/dist/targets/claude-code/charter-render.js +374 -0
  105. package/dist/targets/claude-code/compliance-install.js +62 -17
  106. package/dist/targets/claude-code/compliance-off-files.js +249 -0
  107. package/dist/targets/claude-code/curation-install.js +632 -117
  108. package/dist/targets/claude-code/folder-swap.js +142 -0
  109. package/dist/targets/claude-code/install-backup.js +780 -0
  110. package/dist/targets/claude-code/installer.js +795 -199
  111. package/dist/targets/claude-code/language-stamp.js +6 -0
  112. package/dist/targets/claude-code/legacy-skill-hashes.js +170 -0
  113. package/dist/targets/claude-code/linked-folder.js +176 -0
  114. package/dist/targets/claude-code/reference-converge.js +131 -0
  115. package/dist/targets/claude-code/rules-folder.js +34 -0
  116. package/dist/targets/claude-code/shipped-set.js +160 -0
  117. package/dist/targets/claude-code/tracker-install.js +21 -8
  118. package/dist/targets/claude-code/tracker-servers.js +20 -6
  119. package/dist/targets/claude-code/workflow-folder-install.js +251 -0
  120. package/dist/variant-axes.json +109 -0
  121. package/package.json +1 -1
  122. package/src/assets/agents/_partials/_code.mds +6 -6
  123. package/src/assets/agents/evaluate.md +1 -1
  124. package/src/assets/agents/git.mds +32 -0
  125. package/src/assets/agents/harden.mds +7 -6
  126. package/src/assets/agents/test.md +21 -12
  127. package/src/assets/agents/triage.mds +2 -0
  128. package/src/assets/agents/validate.md +10 -10
  129. package/src/assets/commands/_partials/_compliance.mds +5 -3
  130. package/src/assets/commands/_partials/_engine.mds +9 -8
  131. package/src/assets/commands/_partials/_publication.mds +10 -1
  132. package/src/assets/commands/bug-analysis.mds +80 -126
  133. package/src/assets/commands/code-review.mds +43 -0
  134. package/src/assets/commands/dynamic-build.mds +109 -15
  135. package/src/assets/commands/dynamic-tickets.mds +13 -0
  136. package/src/assets/commands/implement.mds +145 -33
  137. package/src/assets/commands/plan.mds +19 -0
  138. package/src/assets/commands/release.mds +50 -1
  139. package/src/assets/commands/resolve.mds +217 -32
  140. package/src/assets/commands/self-review.mds +8 -7
  141. package/src/assets/mds/git/_ops.mds +23 -0
  142. package/src/assets/mds/git/_ops_steps.mds +17 -0
  143. package/src/assets/mds/git/_pr.mds +1 -1
  144. package/src/assets/mds/git/_references.mds +6 -0
  145. package/src/assets/mds/tracker/_common.mds +4 -0
  146. package/src/assets/mds/tracker/_github.mds +42 -0
  147. package/src/assets/mds/tracker/_jira.mds +43 -0
  148. package/src/assets/mds/tracker/_linear.mds +43 -0
  149. package/src/assets/rules/quality.md +2 -0
  150. package/src/assets/scripts/ci-wait.cjs +204 -126
  151. package/src/assets/scripts/hooks/assets/implement-installed +1 -0
  152. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +13 -11
  153. package/src/assets/scripts/hooks/preamble +18 -8
  154. package/src/assets/scripts/hooks/session-start-orchestrator +4 -2
  155. package/src/assets/scripts/pr-evidence.cjs +41 -2
  156. package/src/assets/scripts/verify-evidence.cjs +408 -56
  157. package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +3 -3
  158. package/src/assets/skills/agent-references/references/commands/plan-levels.md +1 -1
  159. package/src/assets/skills/agent-references/references/synthesize/bug-analysis.md +6 -2
  160. package/src/assets/skills/agent-references/references/validate/passed-alone.md +7 -7
  161. package/src/assets/skills/docs-framework/SKILL.md +5 -2
  162. package/src/assets/skills/git/SKILL.md +3 -3
  163. package/src/assets/skills/git/references/github-api.md +2 -139
  164. package/src/assets/skills/git/references/github-posting.md +158 -0
  165. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  166. package/src/assets/skills/review-methodology/references/patterns.md +1 -1
  167. package/src/assets/skills/review-methodology/references/report-template.md +1 -2
  168. package/src/assets/skills/testing/SKILL.md +2 -4
@@ -44,6 +44,7 @@ import * as path from 'path';
44
44
  import { writeFileAtomicExclusive } from '../../core/fs-atomic.js';
45
45
  import { mdFileName } from '../../core/orphan-sweep.js';
46
46
  import { installedLanguageFocuses } from '../../core/plugins.js';
47
+ import { isUnrealFolder } from './linked-folder.js';
47
48
  /** The fixed prefix of the stamp line; the prompt and the anchored match share it. */
48
49
  export const LANGUAGE_STAMP_PREFIX = 'Installed language focuses: ';
49
50
  /** What an empty list renders as, and what the build ships. */
@@ -138,6 +139,11 @@ export async function restampInstalledCommands(opts) {
138
139
  warn(`language stamp: claudeDir is not an absolute path ("${claudeDir}") — the installed commands were not stamped`);
139
140
  return { focuses, rewritten, unchanged, absent, failed: [...LANGUAGE_STAMPED_COMMANDS] };
140
141
  }
142
+ // Whatever the caller's guard says, nothing is read or written through a commands folder that is a
143
+ // link or not a folder; every stamped command is then left as it is, and `failed` says so.
144
+ if (await isUnrealFolder(path.join(claudeDir, 'commands', 'devflow'))) {
145
+ return { focuses, rewritten, unchanged, absent, failed: [...LANGUAGE_STAMPED_COMMANDS] };
146
+ }
141
147
  for (const command of LANGUAGE_STAMPED_COMMANDS) {
142
148
  const target = path.join(claudeDir, 'commands', 'devflow', mdFileName(command));
143
149
  let current;
@@ -0,0 +1,170 @@
1
+ /**
2
+ * The released copies of the bare legacy skills: for each bare name of the legacy skill list that a
3
+ * release installed as `~/.claude/skills/<name>/`, the SHA-256 (lowercase hex) of every distinct
4
+ * `SKILL.md` that a release installed under that name.
5
+ *
6
+ * `devflow init` moves a bare legacy folder out of the shared skills folder only when its
7
+ * `SKILL.md` has one of these hashes (`sweepLegacySkillFolders`), and a full `devflow uninstall`
8
+ * removes one only on the same check (`judgeBareLegacySkill`). The folder name alone proves
9
+ * nothing: a skill of the user, or of another tool, may carry a name such as `python` or `plan`.
10
+ * A name with no entry here was never installed bare by a release, so no folder of that name is
11
+ * moved.
12
+ *
13
+ * Frozen data, made on 2026-10-30. Source: the 21 published `devflow-kit` packages 0.6.1 to 1.8.3,
14
+ * the releases that installed bare skill folders. (0.4.0 to 0.6.0 installed one nested folder,
15
+ * `skills/devflow/`; 2.0.0 and later install `devflow:`-prefixed folders.) Each package was fetched
16
+ * with `npm pack devflow-kit@<version> --ignore-scripts` and unpacked; no package script or code
17
+ * ran. Every `SKILL.md` below `src/claude/skills/devflow/<name>/` (0.6.1 to 0.9.0) or
18
+ * `plugins/<plugin>/skills/<name>/` (1.0.0 to 1.8.3) was hashed with `shasum -a 256`. Those
19
+ * installers copy a skill file byte for byte, so the installed file has the hash of the packaged
20
+ * file. The comment on each hash gives the first and the last release that held it.
21
+ *
22
+ * The table only grows: a release cannot be un-shipped. tests/guards/legacy-skill-hashes.test.ts
23
+ * proves the shape of the table and its match with tests/fixtures/released-shipped-names.ts.
24
+ */
25
+ const hashes = (list) => Object.freeze([...list]);
26
+ export const BARE_LEGACY_SKILL_HASHES = Object.freeze({
27
+ 'accessibility': hashes([
28
+ '06434c13344e85bf55f4fcf45e8e794ef26ad0751e7e56cac7363661e1b7b7d4', // 1.0.0 to 1.8.3
29
+ ]),
30
+ 'agent-teams': hashes([
31
+ '75591cef45adbc36e95910e08a0db681c56401e1c95db5226afe819dde36ce0c', // 1.0.0 to 1.8.3
32
+ ]),
33
+ 'ambient-router': hashes([
34
+ 'b4a6bfc188d720ede3afefc3802b2a9192dd67cf1a3694a5ee221b383347cc21', // 1.1.0
35
+ '92a3ee58a6deb50f4dbbd50913527de78f7755ed23d492c6ffc18b42d035a867', // 1.2.0
36
+ 'd32ef0392357eff61a47a3f758d2c1674fdf029ca6e74cf465e114723db57497', // 1.3.0 to 1.4.0
37
+ '2d73b902b3610b42455caf6fe0dfe5725dfe92a8590def4a125b280754a6c4f0', // 1.5.0
38
+ 'e4221f41c567b04c2785d4127a4fc7f7fe1099afa490fc03f67e537010e8b5e8', // 1.6.0
39
+ '3e6ed6a711dcdf1f710266065684db637d34f15f0a3b2174d4867ea35fcf613b', // 1.6.1 to 1.8.1
40
+ 'fb3a7025ce437ca2abc8fb8a71cae10e452f335e0d99adf979c601042e16570b', // 1.8.2 to 1.8.3
41
+ ]),
42
+ 'architecture-patterns': hashes([
43
+ 'a72ea9c4dcf4083e8f3d8a70dca282a807972e70e163a3e0988a61a875db4101', // 1.0.0 to 1.2.0
44
+ 'd5da95e223c024e31c000c0d511e845f308045ae89e5a31cfaf2a0eebf07a238', // 1.3.0 to 1.8.3
45
+ ]),
46
+ 'code-smell': hashes([
47
+ '2917880c99f1c2febb596319900c61826f58327ac933860d18eeaf98952b2bee', // 0.6.1 to 0.9.0
48
+ ]),
49
+ 'complexity-patterns': hashes([
50
+ '3a6f56e229b19d27938274dc788a02334e5d96d8c56a55a80661b62c431af50f', // 1.0.0 to 1.2.0
51
+ 'fc068855bca532e1c1a87d59617d9b093db5fef98064b0a6b0c84add0a4c7163', // 1.3.0 to 1.8.3
52
+ ]),
53
+ 'consistency-patterns': hashes([
54
+ 'ed270ef6c976d109beb3fda510cc84399a884d84e26f5a5fcf420ceb878444cb', // 1.0.0 to 1.2.0
55
+ '493a8ae10c759bb57a44f833251e77f1c0577e19b701f917ad6474d64bb0e96c', // 1.3.0 to 1.8.3
56
+ ]),
57
+ 'core-patterns': hashes([
58
+ '5c1c8272250c06a1e1c0b263f171b04096380385ccdda3a252b830f2179597ba', // 1.0.0 to 1.8.3
59
+ ]),
60
+ 'database-patterns': hashes([
61
+ '3cd61ce6b8b005cb72ace5af6fa3620d57f1363166621e88b6da0bb4b9566491', // 1.0.0 to 1.2.0
62
+ '1d28c6cdf41154f284956883c461bc56448342f8cb8d1c1118f9f85b3f4f3486', // 1.3.0 to 1.8.3
63
+ ]),
64
+ 'debug': hashes([
65
+ '7199af54865413f2dc4333e6dc83c63e633f98d0ad78bdbdded384b145249f21', // 0.6.1 to 0.9.0
66
+ ]),
67
+ 'debug-orchestration': hashes([
68
+ 'eb2a5fbf55fc973dff8f5206a093248fde8626c63be967fb819fce15dda8c5cd', // 1.6.0 to 1.8.3
69
+ ]),
70
+ 'dependencies-patterns': hashes([
71
+ '7711a6225b764aea7bf4f4d14cb0fe0ec2afbb3ab106ae17595c94cb126174c0', // 1.0.0 to 1.2.0
72
+ '86f89ec146a451aecf320349a2ff71f1f314a43bdeab221163b60e99361e8fbf', // 1.3.0 to 1.8.3
73
+ ]),
74
+ 'docs-framework': hashes([
75
+ '7d97aa5d1ea61d5d1a9fa0f24960468c2fd365d25d6632700fdfa5efd9b180a0', // 1.0.0
76
+ '7ec949848e9d73249df1785d1e5e837928062a423c7ad4ee588543bea7d89950', // 1.1.0 to 1.5.0
77
+ '40454b337b552064a58424bf8ea505e486d0c0849a7ff14bd499a5072eb1b168', // 1.6.0
78
+ 'e191008ac58722d14d7dadce4d828376585d26aa676b3e74882ea8c90fa8cd2d', // 1.6.1 to 1.8.3
79
+ ]),
80
+ 'documentation-patterns': hashes([
81
+ '2cc11fdae2924f862573ef61aba5c42390089e1973b374a181475548d2e6de88', // 1.0.0 to 1.2.0
82
+ 'b5eaef09cd851054062f72ecbbd13e9392872ab2fd2867c7c6ee5be380b09904', // 1.3.0 to 1.8.3
83
+ ]),
84
+ 'frontend-design': hashes([
85
+ '015bcec670df04b8793bc8ceacf42ca579e87fb4ae9cc826f17d6067f5f39b4f', // 1.0.0 to 1.8.3
86
+ ]),
87
+ 'git-safety': hashes([
88
+ 'd0af494607b6956e2a31dc27b36fe622c2b606465d012efbac5b51f3cf264efb', // 1.0.0 to 1.8.3
89
+ ]),
90
+ 'git-workflow': hashes([
91
+ '8f696c7f8758549e6f9547bdc56cf7370841411bc34f8bfba001951034d391ef', // 1.0.0 to 1.8.3
92
+ ]),
93
+ 'github-patterns': hashes([
94
+ '0a93eed85575836e42980ffcc1dcad8679eb41dd0db1eaf428c51f34c101b2fa', // 1.0.0 to 1.8.3
95
+ ]),
96
+ 'go': hashes([
97
+ '80b68a2f640b16b2d6a369cfe003ed6366a4a6c6c0a71868451a1652f2ede8d6', // 1.2.0 to 1.8.3
98
+ ]),
99
+ 'implementation-orchestration': hashes([
100
+ '023bd2b2b754945daf83b101652e8c9a7c45092d0ebc2dd56dd9f170f6ef2cf2', // 1.6.0 to 1.8.3
101
+ ]),
102
+ 'implementation-patterns': hashes([
103
+ '484816f577cb1c2d4fe6bcbe1f8adbc8c02c8a41eff19ecfa5711e2afca0cf54', // 1.0.0 to 1.8.3
104
+ ]),
105
+ 'input-validation': hashes([
106
+ '2ce71e678840855d271dc7418b207f50c1d1123de80b9b88488154c5697053dd', // 0.6.1 to 0.9.0
107
+ '2e08b78f98de0e6ae17d455f832672e0a3ddaff9fa13337597f54130f3bc1a6b', // 1.0.0 to 1.8.3
108
+ ]),
109
+ 'java': hashes([
110
+ '17cc4b3d476bd3ac9bbd3d367d04a88f4695d0e56f58db434aaaad80d875b6c3', // 1.2.0 to 1.8.3
111
+ ]),
112
+ 'knowledge-persistence': hashes([
113
+ '3f43892b11fb2ffefb6ff1f083872338ed5d511b6e2bf6b125fa2c610afa5c79', // 1.6.0 to 1.8.3
114
+ ]),
115
+ 'performance-patterns': hashes([
116
+ 'bc025c6908ea87fab292cfe37396ee901c7acb1cccfaa27f78d9cd6f5798eea5', // 1.0.0 to 1.2.0
117
+ 'bdd93bb51e829460d65a1ee572d0a95f7a0246b7a26bb1034a1c981e79b5ee9d', // 1.3.0 to 1.8.3
118
+ ]),
119
+ 'plan-orchestration': hashes([
120
+ 'ce940a2d0f2f3d80e9200ca8c4294d6fb26076f4fe6a640a157ffedd8ee37491', // 1.6.0 to 1.8.3
121
+ ]),
122
+ 'python': hashes([
123
+ 'ff03facdc25a6d6dc5d6e6979d2b3e30206b687ffc064cb0bc69591027ddcbb6', // 1.2.0 to 1.8.3
124
+ ]),
125
+ 'react': hashes([
126
+ '3df91866eec7559cbef1e48c74af9b6f23861ef703fbe2bf00822d2d2842f666', // 1.0.0 to 1.1.0
127
+ '671e6a38d811f1eab42a3e0e69445c8528f8222957032c166a77a2b066a1f989', // 1.2.0 to 1.8.3
128
+ ]),
129
+ 'regression-patterns': hashes([
130
+ 'c4a8cfa79007c3838d5fceab272a7c6cfd808c4f64925385dff557670380b2aa', // 1.0.0 to 1.2.0
131
+ 'd3a1a163f20a933d8fefd78e28b602ff34c6d13829fbb99eb78b29f07591f4a5', // 1.3.0 to 1.8.3
132
+ ]),
133
+ 'research': hashes([
134
+ '528c0fd4538df7b949a7d355e7daf7a6e96848053f112b437c26477ca2f456fe', // 0.6.1
135
+ '0a9f207dc8028961328521d9eafa2e4e51740cbed4f7560b71a562e480a08503', // 0.7.0 to 0.9.0
136
+ ]),
137
+ 'review-methodology': hashes([
138
+ '5cc67590d427510bfab6ba0daca3fd05df5aae74f7efeff4230aba4ec167e053', // 1.0.0 to 1.2.0
139
+ '928ddf64d96488c747552216b7079a943abcb7da2c8174d75a1281c94f61c19e', // 1.3.0 to 1.8.3
140
+ ]),
141
+ 'rust': hashes([
142
+ 'efee293456d582cc38941102be4052d3a0f352253c6d913b6d1b3a92134093fa', // 1.2.0 to 1.8.3
143
+ ]),
144
+ 'search-first': hashes([
145
+ '37d60dc1fa6ab1aa690df450747c9a9e7c9b52ff8826ff3e675e612037d04794', // 1.5.0 to 1.8.3
146
+ ]),
147
+ 'security-patterns': hashes([
148
+ '6662d43fa607913db531f8445ceedf7dd17266e64551d59d7d91969082d7c9ac', // 1.0.0 to 1.2.0
149
+ 'f52c3b5acc69b2d6764b3aebb955a51f7de417d065ed9c085aae31684a7b7af0', // 1.3.0 to 1.8.3
150
+ ]),
151
+ 'self-review': hashes([
152
+ 'c7f0b6557316d2abc7cc3229c403d8abb736048a47f1e8d1d993d46351c36033', // 1.0.0 to 1.2.0
153
+ 'f405c0f368884a714be309dcc9e85389672b22a7fd8b49bd2d5fd801cc4d9c51', // 1.3.0 to 1.8.3
154
+ ]),
155
+ 'test-design': hashes([
156
+ '9c048d74b6387143fce401f0edf271f2e02daef231ea66a6662915d7f0f25a5b', // 0.6.1 to 0.9.0
157
+ ]),
158
+ 'test-driven-development': hashes([
159
+ '54abb24907d3cf6c20c96981b4e2d20c80fbb51fb36559a3df9d4ac749af5782', // 1.1.0 to 1.2.0
160
+ '0457cfe9f23d2ba7f7e1faa00d027d8c3500458fcbb9957413be7ffad4c7f874', // 1.3.0 to 1.5.0
161
+ '21abf6a42b294bc3c6a7f51eaf981f08ab7bd6cec2c3c50312fc5d8b37ccba4e', // 1.6.0 to 1.8.3
162
+ ]),
163
+ 'test-patterns': hashes([
164
+ '8c75e34c3544824d1f8dc26c407909ec4bf177db583862b34e827bd1aea1f279', // 1.0.0 to 1.8.3
165
+ ]),
166
+ 'typescript': hashes([
167
+ 'd70fc7facd2bec850c0b4cfb6eeeaa673648674024236a55ee6cbab7c2689241', // 1.0.0 to 1.8.3
168
+ ]),
169
+ });
170
+ //# sourceMappingURL=legacy-skill-hashes.js.map
@@ -0,0 +1,176 @@
1
+ import { promises as fs } from 'fs';
2
+ import { writeFileAtomicExclusive } from '../../core/fs-atomic.js';
3
+ /** Classify `dir` with `lstat`, so a link is seen as a link and never as the directory it points to. */
4
+ export async function directoryState(dir) {
5
+ try {
6
+ const stat = await fs.lstat(dir);
7
+ if (stat.isSymbolicLink())
8
+ return 'link';
9
+ return stat.isDirectory() ? 'real' : 'unusable';
10
+ }
11
+ catch (err) {
12
+ return err.code === 'ENOENT' ? 'absent' : 'unusable';
13
+ }
14
+ }
15
+ /** Whether `dir` is a link or not a folder, so that a run must not write into it. An absent folder is fine: the run creates it. */
16
+ export async function isUnrealFolder(dir) {
17
+ const state = await directoryState(dir);
18
+ return state === 'link' || state === 'unusable';
19
+ }
20
+ /** Whether a link stands at `entry`. An absent or unreadable entry is not a link. */
21
+ export async function entryIsLink(entry) {
22
+ return (await directoryState(entry)) === 'link';
23
+ }
24
+ /**
25
+ * Remove the link at `entry` with `unlink`: the link, never what it points to.
26
+ *
27
+ * The one place the pre-clean replaces a link. `readdir` and `mkdir` follow a link, so a pre-clean
28
+ * that emptied through one would empty another directory. Never throws; a link that cannot be
29
+ * removed is `stuck`, and the caller leaves the path alone.
30
+ */
31
+ export async function unlinkLinkAt(entry) {
32
+ if (!(await entryIsLink(entry)))
33
+ return 'no-link';
34
+ try {
35
+ await fs.unlink(entry);
36
+ return 'unlinked';
37
+ }
38
+ catch {
39
+ return 'stuck';
40
+ }
41
+ }
42
+ /**
43
+ * Prove, with {@link isUnrealFolder}, that a run may write into `dir`, and record the folder in
44
+ * `report.refusedFolders` when it may not. Returns true for a refused folder, so the caller skips
45
+ * every write into it.
46
+ *
47
+ * The answer for each folder a run writes into besides the `devflow:git` references, which the
48
+ * installer's `referencesHostState` answers: the commands, rules and agents folders and every other
49
+ * skill folder. An absent folder is fine, since the install creates it. A link is refused and never
50
+ * repaired, because the installer did not create it. A full install's pre-clean has already replaced
51
+ * the links it could, so this refuses only what is left. Never throws.
52
+ */
53
+ export async function refuseUnrealFolder(report, dir) {
54
+ if (!(await isUnrealFolder(dir)))
55
+ return false;
56
+ report.refusedFolders.push(dir);
57
+ return true;
58
+ }
59
+ /**
60
+ * Make `entry` safe to write: a link there is unlinked, never followed, so the caller then creates a
61
+ * real file or folder in its place. The entry can be a file or a folder. Only the last component is
62
+ * checked, so a caller checks each folder on the way down before it checks an entry in it.
63
+ *
64
+ * Fail closed: an entry `lstat` cannot inspect is refused. Never throws.
65
+ */
66
+ export async function clearLinkedEntry(entry) {
67
+ try {
68
+ const stat = await fs.lstat(entry);
69
+ if (!stat.isSymbolicLink())
70
+ return 'clear';
71
+ }
72
+ catch (err) {
73
+ return err.code === 'ENOENT' ? 'clear' : 'refused';
74
+ }
75
+ try {
76
+ await fs.unlink(entry);
77
+ return 'replaced';
78
+ }
79
+ catch {
80
+ return 'refused';
81
+ }
82
+ }
83
+ /**
84
+ * {@link clearLinkedEntry} plus the report: a replaced link goes to `report.replacedLinks`, a refused
85
+ * entry to `report.refusedFolders`. Returns true when the caller may write at `entry`.
86
+ *
87
+ * The one call a write step makes before it creates or overwrites a file or folder inside a Devflow
88
+ * folder. Omit `report` when the step has none; the link is still never written through.
89
+ */
90
+ export async function clearLinkForWrite(entry, report) {
91
+ const clearance = await clearLinkedEntry(entry);
92
+ if (clearance === 'replaced')
93
+ report?.replacedLinks.push(entry);
94
+ if (clearance === 'refused')
95
+ report?.refusedFolders.push(entry);
96
+ return clearance !== 'refused';
97
+ }
98
+ /** A report that holds no link yet. */
99
+ export function newLinkReport() {
100
+ return { replacedLinks: [], refusedFolders: [] };
101
+ }
102
+ /**
103
+ * One report from several: the paths of every report in order, each path once. A command that gets
104
+ * the links of more than one step prints this, so a folder that two steps met is named once.
105
+ * Returns a new report; the inputs are not changed.
106
+ */
107
+ export function mergeLinkReports(...reports) {
108
+ return {
109
+ replacedLinks: [...new Set(reports.flatMap(report => report.replacedLinks))],
110
+ refusedFolders: [...new Set(reports.flatMap(report => report.refusedFolders))],
111
+ };
112
+ }
113
+ /**
114
+ * The line for a link a step replaced. One wording for the install summary and for every converge
115
+ * that reports through a `warn` callback.
116
+ */
117
+ export function replacedLinkMessage(linkPath) {
118
+ return `${linkPath} was a link. It was replaced with a real file or folder, and what it pointed to was left as it was.`;
119
+ }
120
+ /**
121
+ * The first sentence of every refused-folder line: what the folder is. The install and the uninstall
122
+ * both open with it and then say what they did and what to do.
123
+ */
124
+ export function refusedFolderStatement(folder) {
125
+ return `${folder} is a link or not a folder.`;
126
+ }
127
+ /**
128
+ * The line for a folder a step wrote nothing at, because it is a link it could not remove or not a
129
+ * folder. One wording for the install summary and for every converge that reports through a `warn`
130
+ * callback.
131
+ */
132
+ export function refusedFolderMessage(folder) {
133
+ return `${refusedFolderStatement(folder)} Nothing was written there. Replace it with a real file or folder and run the install again.`;
134
+ }
135
+ /** Send one line per link in `report` to `warn`: the replaced links first, then the refused folders. */
136
+ export function warnLinkReport(report, warn) {
137
+ for (const linkPath of report.replacedLinks)
138
+ warn(replacedLinkMessage(linkPath));
139
+ for (const folder of report.refusedFolders)
140
+ warn(refusedFolderMessage(folder));
141
+ }
142
+ /** The mode of a file that replaces a link. The atomic writer would copy the mode of the link's target instead. */
143
+ const REPLACED_LINK_FILE_MODE = 0o644;
144
+ /** The temp file beside `file` that {@link writeFileReplacingLink} writes through: `<file>.tmp.<pid>`. */
145
+ export function linkReplacementTempPath(file) {
146
+ return `${file}.tmp.${process.pid}`;
147
+ }
148
+ /**
149
+ * Write `content` at `file` through a temp file beside it and one rename. A rename puts the new file
150
+ * in the place of whatever stands at `file`, so a link there is replaced and never followed; the link
151
+ * is recorded in `links.replacedLinks` once the write is done. A file that replaces a link has the
152
+ * fixed mode 0644, never the mode of what the link pointed to, which the atomic writer would copy
153
+ * because its `stat` follows the link. Any other file goes through the atomic writer and keeps its
154
+ * own mode. The caller has proved that the folder holding `file` is a real one. Throws what the
155
+ * writer throws, so the caller keeps its own catch.
156
+ */
157
+ export async function writeFileReplacingLink(file, content, links) {
158
+ if (!(await entryIsLink(file))) {
159
+ await writeFileAtomicExclusive(file, content);
160
+ return;
161
+ }
162
+ const temp = linkReplacementTempPath(file);
163
+ try {
164
+ // A stale temp file is removed first; the exclusive open then refuses anything placed there since.
165
+ await fs.rm(temp, { force: true });
166
+ await fs.writeFile(temp, content, { encoding: 'utf-8', flag: 'wx', mode: REPLACED_LINK_FILE_MODE });
167
+ await fs.chmod(temp, REPLACED_LINK_FILE_MODE);
168
+ await fs.rename(temp, file);
169
+ }
170
+ catch (err) {
171
+ await fs.rm(temp, { force: true }).catch(() => undefined);
172
+ throw err;
173
+ }
174
+ links?.replacedLinks.push(file);
175
+ }
176
+ //# sourceMappingURL=linked-folder.js.map
@@ -0,0 +1,131 @@
1
+ /**
2
+ * The references kind of the variant converge: make the installed `devflow:git` references
3
+ * match the machine's compliance switch (AC-41, AC-50).
4
+ *
5
+ * `devflow init`, `devflow compliance` and `devflow curation` reach it through
6
+ * convergeVariantAxes, so a compliance toggle needs no reinstall. It does, in order:
7
+ *
8
+ * 1. the overlay, with the manifest and the ONE source root of the switch
9
+ * (`installedReferenceManifest({ compliance })`, `skillRefsSourceDir`): it rewrites the
10
+ * references whose text differs between the two states and prunes the omitted
11
+ * operation files from `pr/` and `tracker/`. With compliance off the source root is the
12
+ * compliance-off tree, and an absent root is a failure that writes nothing: the converge
13
+ * never falls through to the compliance-on references;
14
+ * 2. with compliance off, the removal list ({@link removeComplianceOffFiles});
15
+ * with compliance on, the restore of the files the skill copy puts in place
16
+ * ({@link restoreComplianceOffFiles}), from the source the install would copy.
17
+ *
18
+ * It acts on a skill that is ALREADY installed. A machine whose plugin selection holds no
19
+ * `devflow:git` has nothing to converge, and that is a clean result. A skill directory, or a
20
+ * `references` directory inside it, that is a link or not a directory is a failure: the overlay
21
+ * would stage, prune and set modes through it. A full install empties the skill directory first and
22
+ * so replaces such a link; this converge and a partial install have no such step, so each refuses
23
+ * instead, on the same proof ({@link referencesHostState}).
24
+ *
25
+ * Never throws. Every failure is a warning and a `failures` entry, and the next step still
26
+ * runs. With compliance off a caller reads `failures` and `stillPosting` and fails closed.
27
+ */
28
+ import * as path from 'path';
29
+ import { skillRefsSourceDir } from '../../core/assets.js';
30
+ import { installedReferenceManifest, SKILL_REFS_SKILL_NAME } from '../../core/mds-variants.js';
31
+ import { getPackageRoot } from '../../core/paths.js';
32
+ import { prefixSkillName } from '../../core/plugins.js';
33
+ import { removeComplianceOffFiles, restoreComplianceOffFiles } from './compliance-off-files.js';
34
+ import { overlayGeneratedReferences, overlayUnitLabel, referencesHostState, resolveSkillSource } from './installer.js';
35
+ import { isUnrealFolder } from './linked-folder.js';
36
+ export async function convergeReferences(opts) {
37
+ const { claudeDir, devflowDir, compliance, warn, backups } = opts;
38
+ const failures = [];
39
+ const fail = (message) => {
40
+ warn(`reference converge: ${message}`);
41
+ failures.push(message);
42
+ };
43
+ // Precondition, asserted in production code: a relative claudeDir would resolve every
44
+ // install target somewhere unexpected, the removals included.
45
+ if (!path.isAbsolute(claudeDir)) {
46
+ fail(`claudeDir is not an absolute path ("${claudeDir}") — skipping convergence`);
47
+ return { converged: false, rewritten: [], removed: [], restored: [], failures, stillPosting: [], unproven: !compliance };
48
+ }
49
+ const root = opts.packageRoot ?? getPackageRoot();
50
+ const skillsDir = path.join(claudeDir, 'skills');
51
+ const gitSkillDir = path.join(skillsDir, prefixSkillName(SKILL_REFS_SKILL_NAME));
52
+ // Every step below moves, removes or writes inside the skills folder. One that is a link or not a
53
+ // folder sends all of them into another place, and `referencesHostState` only looks at the entries
54
+ // below it. It is refused once, here: nothing is converged, the removal list of compliance off
55
+ // included, so the result is unproven and the caller fails closed.
56
+ if (await isUnrealFolder(skillsDir)) {
57
+ fail(`${skillsDir} is a link or not a folder, so nothing in it was converged`);
58
+ return { converged: false, rewritten: [], removed: [], restored: [], failures, stillPosting: [], unproven: !compliance };
59
+ }
60
+ const manifest = installedReferenceManifest({ compliance });
61
+ let rewritten = [];
62
+ let unproven = false;
63
+ const removed = [];
64
+ const host = await referencesHostState(gitSkillDir);
65
+ if (host.kind === 'refused') {
66
+ fail(`${host.path} is not a real directory, so the references in it were not converged`);
67
+ unproven = true;
68
+ }
69
+ else if (host.kind === 'ready') {
70
+ try {
71
+ const overlay = await overlayGeneratedReferences({
72
+ referencesTarget: path.join(gitSkillDir, 'references'),
73
+ sourceRoot: skillRefsSourceDir(compliance, root),
74
+ manifest,
75
+ warn,
76
+ backups,
77
+ });
78
+ rewritten = overlay.overlaidRefs;
79
+ for (const failure of overlay.overlayFailures) {
80
+ fail(`${overlayUnitLabel(failure.unit)} was not refreshed (${failure.error})`);
81
+ unproven = true;
82
+ }
83
+ for (const failed of overlay.pruned.failed) {
84
+ fail(`could not prune ${failed.name} (${String(failed.error)})`);
85
+ unproven = true;
86
+ }
87
+ // What the prune removed is named beside what the compliance-off list removes. The prune names
88
+ // are relative to the references folder; this list spells them from the skills folder.
89
+ removed.push(...overlay.pruned.removed.map(name => `${prefixSkillName(SKILL_REFS_SKILL_NAME)}/references/${name}`));
90
+ }
91
+ catch (err) {
92
+ // The overlay throws for a packaging failure only: an absent source root, or a manifest entry
93
+ // the root lacks. Nothing is written in either case, and nothing falls through to another root.
94
+ fail(err instanceof Error ? err.message : String(err));
95
+ unproven = true;
96
+ }
97
+ }
98
+ const restored = [];
99
+ const stillPosting = [];
100
+ if (!compliance) {
101
+ const removal = await removeComplianceOffFiles(skillsDir);
102
+ removed.push(...removal.removed);
103
+ for (const refusal of removal.refused) {
104
+ fail(`${refusal.entry} was not removed — ${refusal.reason}`);
105
+ stillPosting.push(path.join(skillsDir, ...refusal.entry.split('/')));
106
+ }
107
+ }
108
+ else {
109
+ // The overlay owns every generated reference (publication-gate.md among them), so the restore
110
+ // writes back only the files that come from the skill source.
111
+ const ownedByOverlay = new Set(manifest.map(rel => `${prefixSkillName(SKILL_REFS_SKILL_NAME)}/references/${rel}`));
112
+ const restoration = await restoreComplianceOffFiles({
113
+ skillsDir,
114
+ sourceDirOf: async (skill) => (await resolveSkillSource(skill, devflowDir, opts.packageRoot)).dir,
115
+ ownedElsewhere: ownedByOverlay,
116
+ });
117
+ restored.push(...restoration.restored);
118
+ for (const refusal of restoration.refused)
119
+ fail(`${refusal.entry} was not restored — ${refusal.reason}`);
120
+ }
121
+ return {
122
+ converged: failures.length === 0,
123
+ rewritten,
124
+ removed,
125
+ restored,
126
+ failures,
127
+ stillPosting: compliance ? [] : stillPosting,
128
+ unproven: compliance ? false : unproven,
129
+ };
130
+ }
131
+ //# sourceMappingURL=reference-converge.js.map
@@ -0,0 +1,34 @@
1
+ import { directoryState, refusedFolderStatement, unlinkLinkAt } from './linked-folder.js';
2
+ /**
3
+ * Remove the rules folder devflow installs into, without deleting a file of the user's: the files
4
+ * devflow did not ship move to the backup folder, the files it shipped are removed by the list the
5
+ * session made, and a plain `rmdir` removes the folder only when nothing is left in it.
6
+ *
7
+ * A link is unlinked and never followed, and a path that is not a folder is left as it is. Never
8
+ * throws.
9
+ */
10
+ export async function removeRulesFolder(rulesFolder, backups) {
11
+ const state = await directoryState(rulesFolder);
12
+ switch (state) {
13
+ case 'absent':
14
+ return 'absent';
15
+ case 'link':
16
+ return (await unlinkLinkAt(rulesFolder)) === 'unlinked' ? 'unlinked' : 'refused';
17
+ case 'unusable':
18
+ return 'refused';
19
+ case 'real': {
20
+ await backups.prepass([rulesFolder]);
21
+ const removal = await backups.removeListed(rulesFolder);
22
+ return removal.folderRemoved ? 'removed' : 'kept';
23
+ }
24
+ default: {
25
+ const unreachable = state;
26
+ return unreachable;
27
+ }
28
+ }
29
+ }
30
+ /** The line for a rules folder that {@link removeRulesFolder} refused to touch. */
31
+ export function rulesFolderRefusedMessage(rulesFolder) {
32
+ return `${refusedFolderStatement(rulesFolder)} It was left as it is.`;
33
+ }
34
+ //# sourceMappingURL=rules-folder.js.map