agileflow 3.3.0 → 3.4.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 (210) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +6 -6
  3. package/lib/skill-loader.js +0 -1
  4. package/package.json +1 -1
  5. package/scripts/agileflow-statusline.sh +81 -0
  6. package/scripts/agileflow-welcome.js +79 -0
  7. package/scripts/claude-tmux.sh +90 -23
  8. package/scripts/claude-watchdog.sh +225 -0
  9. package/scripts/generators/agent-registry.js +14 -1
  10. package/scripts/generators/inject-babysit.js +22 -9
  11. package/scripts/generators/inject-help.js +19 -9
  12. package/scripts/lib/ac-test-matcher.js +452 -0
  13. package/scripts/lib/audit-cleanup.js +250 -0
  14. package/scripts/lib/audit-registry.js +304 -0
  15. package/scripts/lib/configure-features.js +35 -0
  16. package/scripts/lib/feature-catalog.js +3 -3
  17. package/scripts/lib/gate-enforcer.js +295 -0
  18. package/scripts/lib/model-profiles.js +118 -0
  19. package/scripts/lib/quality-gates.js +163 -0
  20. package/scripts/lib/signal-detectors.js +44 -1
  21. package/scripts/lib/skill-catalog.js +557 -0
  22. package/scripts/lib/skill-recommender.js +311 -0
  23. package/scripts/lib/status-writer.js +255 -0
  24. package/scripts/lib/story-claiming.js +128 -45
  25. package/scripts/lib/task-sync.js +32 -38
  26. package/scripts/lib/tdd-phase-manager.js +455 -0
  27. package/scripts/lib/team-events.js +34 -3
  28. package/scripts/lib/tmux-audit-monitor.js +611 -0
  29. package/scripts/lib/tmux-group-colors.js +113 -0
  30. package/scripts/lib/tool-registry.yaml +241 -0
  31. package/scripts/lib/tool-shed.js +441 -0
  32. package/scripts/messaging-bridge.js +209 -1
  33. package/scripts/native-team-observer.js +219 -0
  34. package/scripts/obtain-context.js +14 -0
  35. package/scripts/ralph-loop.js +30 -5
  36. package/scripts/smart-detect.js +21 -0
  37. package/scripts/spawn-audit-sessions.js +877 -0
  38. package/scripts/team-manager.js +56 -16
  39. package/scripts/tmux-close-windows.sh +180 -0
  40. package/src/core/agents/a11y-analyzer-aria.md +155 -0
  41. package/src/core/agents/a11y-analyzer-forms.md +162 -0
  42. package/src/core/agents/a11y-analyzer-keyboard.md +175 -0
  43. package/src/core/agents/a11y-analyzer-semantic.md +153 -0
  44. package/src/core/agents/a11y-analyzer-visual.md +158 -0
  45. package/src/core/agents/a11y-consensus.md +248 -0
  46. package/src/core/agents/ads-audit-budget.md +181 -0
  47. package/src/core/agents/ads-audit-compliance.md +169 -0
  48. package/src/core/agents/ads-audit-creative.md +164 -0
  49. package/src/core/agents/ads-audit-google.md +226 -0
  50. package/src/core/agents/ads-audit-meta.md +183 -0
  51. package/src/core/agents/ads-audit-tracking.md +197 -0
  52. package/src/core/agents/ads-consensus.md +396 -0
  53. package/src/core/agents/ads-generate.md +145 -0
  54. package/src/core/agents/ads-performance-tracker.md +197 -0
  55. package/src/core/agents/api-quality-analyzer-conventions.md +148 -0
  56. package/src/core/agents/api-quality-analyzer-docs.md +176 -0
  57. package/src/core/agents/api-quality-analyzer-errors.md +183 -0
  58. package/src/core/agents/api-quality-analyzer-pagination.md +171 -0
  59. package/src/core/agents/api-quality-analyzer-versioning.md +143 -0
  60. package/src/core/agents/api-quality-consensus.md +214 -0
  61. package/src/core/agents/arch-analyzer-circular.md +148 -0
  62. package/src/core/agents/arch-analyzer-complexity.md +171 -0
  63. package/src/core/agents/arch-analyzer-coupling.md +146 -0
  64. package/src/core/agents/arch-analyzer-layering.md +151 -0
  65. package/src/core/agents/arch-analyzer-patterns.md +162 -0
  66. package/src/core/agents/arch-consensus.md +227 -0
  67. package/src/core/agents/brainstorm-analyzer-features.md +169 -0
  68. package/src/core/agents/brainstorm-analyzer-growth.md +161 -0
  69. package/src/core/agents/brainstorm-analyzer-integration.md +172 -0
  70. package/src/core/agents/brainstorm-analyzer-market.md +147 -0
  71. package/src/core/agents/brainstorm-analyzer-ux.md +167 -0
  72. package/src/core/agents/brainstorm-consensus.md +237 -0
  73. package/src/core/agents/completeness-consensus.md +5 -5
  74. package/src/core/agents/perf-consensus.md +2 -2
  75. package/src/core/agents/security-consensus.md +2 -2
  76. package/src/core/agents/seo-analyzer-content.md +167 -0
  77. package/src/core/agents/seo-analyzer-images.md +187 -0
  78. package/src/core/agents/seo-analyzer-performance.md +206 -0
  79. package/src/core/agents/seo-analyzer-schema.md +176 -0
  80. package/src/core/agents/seo-analyzer-sitemap.md +172 -0
  81. package/src/core/agents/seo-analyzer-technical.md +144 -0
  82. package/src/core/agents/seo-consensus.md +289 -0
  83. package/src/core/agents/test-consensus.md +2 -2
  84. package/src/core/commands/adr.md +1 -0
  85. package/src/core/commands/ads/audit.md +375 -0
  86. package/src/core/commands/ads/budget.md +97 -0
  87. package/src/core/commands/ads/competitor.md +112 -0
  88. package/src/core/commands/ads/creative.md +85 -0
  89. package/src/core/commands/ads/generate.md +238 -0
  90. package/src/core/commands/ads/google.md +112 -0
  91. package/src/core/commands/ads/health.md +327 -0
  92. package/src/core/commands/ads/landing.md +119 -0
  93. package/src/core/commands/ads/linkedin.md +112 -0
  94. package/src/core/commands/ads/meta.md +91 -0
  95. package/src/core/commands/ads/microsoft.md +115 -0
  96. package/src/core/commands/ads/plan.md +321 -0
  97. package/src/core/commands/ads/test-plan.md +317 -0
  98. package/src/core/commands/ads/tiktok.md +129 -0
  99. package/src/core/commands/ads/track.md +288 -0
  100. package/src/core/commands/ads/youtube.md +124 -0
  101. package/src/core/commands/ads.md +140 -0
  102. package/src/core/commands/assign.md +1 -0
  103. package/src/core/commands/audit.md +43 -6
  104. package/src/core/commands/babysit.md +315 -1266
  105. package/src/core/commands/baseline.md +1 -0
  106. package/src/core/commands/blockers.md +1 -0
  107. package/src/core/commands/board.md +1 -0
  108. package/src/core/commands/changelog.md +1 -0
  109. package/src/core/commands/choose.md +1 -0
  110. package/src/core/commands/ci.md +1 -0
  111. package/src/core/commands/code/accessibility.md +347 -0
  112. package/src/core/commands/code/api.md +297 -0
  113. package/src/core/commands/code/architecture.md +297 -0
  114. package/src/core/commands/{audit → code}/completeness.md +72 -25
  115. package/src/core/commands/{audit → code}/legal.md +63 -16
  116. package/src/core/commands/{audit → code}/logic.md +64 -16
  117. package/src/core/commands/{audit → code}/performance.md +67 -20
  118. package/src/core/commands/{audit → code}/security.md +69 -19
  119. package/src/core/commands/{audit → code}/test.md +67 -20
  120. package/src/core/commands/configure.md +1 -0
  121. package/src/core/commands/council.md +1 -0
  122. package/src/core/commands/deploy.md +1 -0
  123. package/src/core/commands/diagnose.md +1 -0
  124. package/src/core/commands/docs.md +1 -0
  125. package/src/core/commands/epic/edit.md +213 -0
  126. package/src/core/commands/epic.md +1 -0
  127. package/src/core/commands/export.md +238 -0
  128. package/src/core/commands/help.md +16 -1
  129. package/src/core/commands/{discovery → ideate}/brief.md +12 -12
  130. package/src/core/commands/{discovery/new.md → ideate/discover.md} +20 -16
  131. package/src/core/commands/ideate/features.md +496 -0
  132. package/src/core/commands/ideate/new.md +158 -124
  133. package/src/core/commands/impact.md +1 -0
  134. package/src/core/commands/learn/explain.md +118 -0
  135. package/src/core/commands/learn/glossary.md +135 -0
  136. package/src/core/commands/learn/patterns.md +138 -0
  137. package/src/core/commands/learn/tour.md +126 -0
  138. package/src/core/commands/migrate/codemods.md +151 -0
  139. package/src/core/commands/migrate/plan.md +131 -0
  140. package/src/core/commands/migrate/scan.md +114 -0
  141. package/src/core/commands/migrate/validate.md +119 -0
  142. package/src/core/commands/multi-expert.md +1 -0
  143. package/src/core/commands/pr.md +1 -0
  144. package/src/core/commands/review.md +1 -0
  145. package/src/core/commands/seo/audit.md +373 -0
  146. package/src/core/commands/seo/competitor.md +174 -0
  147. package/src/core/commands/seo/content.md +107 -0
  148. package/src/core/commands/seo/geo.md +229 -0
  149. package/src/core/commands/seo/hreflang.md +140 -0
  150. package/src/core/commands/seo/images.md +96 -0
  151. package/src/core/commands/seo/page.md +198 -0
  152. package/src/core/commands/seo/plan.md +163 -0
  153. package/src/core/commands/seo/programmatic.md +131 -0
  154. package/src/core/commands/seo/references/cwv-thresholds.md +64 -0
  155. package/src/core/commands/seo/references/eeat-framework.md +110 -0
  156. package/src/core/commands/seo/references/quality-gates.md +91 -0
  157. package/src/core/commands/seo/references/schema-types.md +102 -0
  158. package/src/core/commands/seo/schema.md +183 -0
  159. package/src/core/commands/seo/sitemap.md +97 -0
  160. package/src/core/commands/seo/technical.md +100 -0
  161. package/src/core/commands/seo.md +107 -0
  162. package/src/core/commands/skill/list.md +68 -212
  163. package/src/core/commands/skill/recommend.md +216 -0
  164. package/src/core/commands/sprint.md +1 -0
  165. package/src/core/commands/status/undo.md +191 -0
  166. package/src/core/commands/status.md +1 -0
  167. package/src/core/commands/story/edit.md +204 -0
  168. package/src/core/commands/story/view.md +29 -7
  169. package/src/core/commands/story-validate.md +1 -0
  170. package/src/core/commands/story.md +1 -0
  171. package/src/core/commands/tdd-next.md +238 -0
  172. package/src/core/commands/tdd.md +211 -0
  173. package/src/core/commands/team/start.md +10 -6
  174. package/src/core/commands/tests.md +1 -0
  175. package/src/core/commands/verify.md +27 -1
  176. package/src/core/commands/workflow.md +2 -0
  177. package/src/core/experts/_core-expertise.yaml +105 -0
  178. package/src/core/experts/analytics/expertise.yaml +5 -99
  179. package/src/core/experts/codebase-query/expertise.yaml +3 -72
  180. package/src/core/experts/compliance/expertise.yaml +6 -72
  181. package/src/core/experts/database/expertise.yaml +9 -52
  182. package/src/core/experts/documentation/expertise.yaml +7 -140
  183. package/src/core/experts/integrations/expertise.yaml +7 -127
  184. package/src/core/experts/mentor/expertise.yaml +8 -35
  185. package/src/core/experts/monitoring/expertise.yaml +7 -49
  186. package/src/core/experts/performance/expertise.yaml +1 -26
  187. package/src/core/experts/security/expertise.yaml +9 -34
  188. package/src/core/experts/ui/expertise.yaml +6 -36
  189. package/src/core/knowledge/ads/ad-audit-checklist-scoring.md +424 -0
  190. package/src/core/knowledge/ads/ad-optimization-logic.md +590 -0
  191. package/src/core/knowledge/ads/ad-technical-specifications.md +385 -0
  192. package/src/core/knowledge/ads/definitive-advertising-reference-2026.md +506 -0
  193. package/src/core/knowledge/ads/paid-advertising-research-2026.md +445 -0
  194. package/src/core/teams/backend.json +41 -0
  195. package/src/core/teams/frontend.json +41 -0
  196. package/src/core/teams/qa.json +41 -0
  197. package/src/core/teams/solo.json +35 -0
  198. package/src/core/templates/agileflow-metadata.json +20 -1
  199. package/tools/cli/commands/setup.js +85 -3
  200. package/tools/cli/commands/update.js +42 -0
  201. package/tools/cli/installers/ide/_base-ide.js +42 -5
  202. package/tools/cli/installers/ide/claude-code.js +71 -3
  203. package/tools/cli/lib/content-injector.js +160 -12
  204. package/tools/cli/lib/docs-setup.js +1 -1
  205. package/src/core/commands/skill/create.md +0 -698
  206. package/src/core/commands/skill/delete.md +0 -316
  207. package/src/core/commands/skill/edit.md +0 -359
  208. package/src/core/commands/skill/test.md +0 -394
  209. package/src/core/commands/skill/upgrade.md +0 -552
  210. package/src/core/templates/skill-template.md +0 -117
@@ -0,0 +1,311 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * skill-recommender.js
4
+ *
5
+ * Tech stack detector + recommendation engine for skills.
6
+ * Reads package.json (or other project files) to detect the framework,
7
+ * styling, testing, database, and language. Maps detected stack to
8
+ * curated skills from skills.sh.
9
+ *
10
+ * Follows the signal-detectors.js pattern (detector functions returning recommendations).
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const { getAllCuratedSkills } = require('./skill-catalog');
18
+
19
+ // =============================================================================
20
+ // Tech Stack Detection
21
+ // =============================================================================
22
+
23
+ /**
24
+ * Detect project tech stack from package.json and project files.
25
+ * @param {string} projectRoot - Project root directory
26
+ * @returns {Object} Detected stack with categories
27
+ */
28
+ function detectTechStack(projectRoot) {
29
+ const stack = {
30
+ frameworks: [],
31
+ styling: [],
32
+ testing: [],
33
+ databases: [],
34
+ languages: [],
35
+ devops: [],
36
+ security: [],
37
+ };
38
+
39
+ // Read package.json
40
+ const pkgPath = path.join(projectRoot, 'package.json');
41
+ let pkg = null;
42
+ if (fs.existsSync(pkgPath)) {
43
+ try {
44
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
45
+ } catch {
46
+ // Ignore parse errors
47
+ }
48
+ }
49
+
50
+ if (pkg) {
51
+ const allDeps = {
52
+ ...pkg.dependencies,
53
+ ...pkg.devDependencies,
54
+ };
55
+
56
+ // Framework detection
57
+ if (allDeps.next) stack.frameworks.push('next', 'react');
58
+ else if (allDeps.react) stack.frameworks.push('react');
59
+ if (allDeps.vue || allDeps['vue-router']) stack.frameworks.push('vue');
60
+ if (allDeps.svelte || allDeps['@sveltejs/kit']) stack.frameworks.push('svelte', 'sveltekit');
61
+ if (allDeps['@angular/core']) stack.frameworks.push('angular');
62
+ if (allDeps.express) stack.frameworks.push('express', 'node');
63
+ if (allDeps.fastify) stack.frameworks.push('fastify', 'node');
64
+ if (allDeps.hono) stack.frameworks.push('hono', 'node');
65
+ if (allDeps['react-native'] || allDeps.expo) stack.frameworks.push('react-native', 'mobile');
66
+ if (allDeps.nuxt) stack.frameworks.push('nuxt', 'vue');
67
+ if (allDeps.astro) stack.frameworks.push('astro');
68
+ if (allDeps.remix || allDeps['@remix-run/react']) stack.frameworks.push('remix', 'react');
69
+
70
+ // Styling detection
71
+ if (allDeps.tailwindcss) stack.styling.push('tailwind', 'tailwindcss');
72
+ if (allDeps['styled-components']) stack.styling.push('styled-components');
73
+ if (allDeps['@emotion/react']) stack.styling.push('emotion');
74
+ if (allDeps.sass) stack.styling.push('sass');
75
+
76
+ // Testing detection
77
+ if (allDeps.jest) stack.testing.push('jest');
78
+ if (allDeps.vitest) stack.testing.push('vitest', 'vite');
79
+ if (allDeps.playwright || allDeps['@playwright/test']) stack.testing.push('playwright', 'e2e');
80
+ if (allDeps.cypress) stack.testing.push('cypress', 'e2e');
81
+ if (allDeps.mocha) stack.testing.push('mocha');
82
+
83
+ // Database detection
84
+ if (allDeps.prisma || allDeps['@prisma/client']) stack.databases.push('prisma', 'orm');
85
+ if (allDeps['@supabase/supabase-js']) stack.databases.push('supabase', 'postgres');
86
+ if (allDeps.mongoose || allDeps.mongodb) stack.databases.push('mongodb', 'nosql');
87
+ if (allDeps.pg || allDeps.postgres) stack.databases.push('postgresql', 'postgres');
88
+ if (allDeps.redis || allDeps.ioredis) stack.databases.push('redis', 'cache');
89
+ if (allDeps.drizzle || allDeps['drizzle-orm']) stack.databases.push('drizzle', 'orm');
90
+ if (allDeps.knex) stack.databases.push('knex', 'sql');
91
+ if (allDeps.sequelize) stack.databases.push('sequelize', 'orm');
92
+ if (allDeps.typeorm) stack.databases.push('typeorm', 'orm');
93
+
94
+ // Language detection
95
+ if (allDeps.typescript) stack.languages.push('typescript', 'ts');
96
+ if (allDeps['@apollo/server'] || allDeps['@apollo/client'] || allDeps.graphql) {
97
+ stack.frameworks.push('graphql', 'apollo');
98
+ }
99
+
100
+ // DevOps detection
101
+ if (allDeps.docker || fs.existsSync(path.join(projectRoot, 'Dockerfile'))) {
102
+ stack.devops.push('docker', 'containers');
103
+ }
104
+ if (fs.existsSync(path.join(projectRoot, '.github', 'workflows'))) {
105
+ stack.devops.push('github-actions', 'ci');
106
+ }
107
+ if (allDeps['socket.io'] || allDeps.ws) stack.frameworks.push('websocket', 'real-time');
108
+ }
109
+
110
+ // Python detection
111
+ const pyFiles = ['requirements.txt', 'pyproject.toml', 'setup.py', 'Pipfile'];
112
+ for (const f of pyFiles) {
113
+ if (fs.existsSync(path.join(projectRoot, f))) {
114
+ stack.languages.push('python');
115
+
116
+ // Check for specific Python frameworks
117
+ try {
118
+ const content = fs.readFileSync(path.join(projectRoot, f), 'utf8');
119
+ if (content.includes('fastapi')) stack.frameworks.push('fastapi', 'python');
120
+ if (content.includes('django')) stack.frameworks.push('django', 'python');
121
+ if (content.includes('flask')) stack.frameworks.push('flask', 'python');
122
+ if (content.includes('pytest')) stack.testing.push('pytest');
123
+ if (content.includes('pydantic')) stack.frameworks.push('pydantic');
124
+ } catch {
125
+ // Ignore read errors
126
+ }
127
+ break;
128
+ }
129
+ }
130
+
131
+ // Go detection
132
+ if (fs.existsSync(path.join(projectRoot, 'go.mod'))) {
133
+ stack.languages.push('go', 'golang');
134
+ stack.frameworks.push('go-backend');
135
+ }
136
+
137
+ // PHP detection
138
+ if (fs.existsSync(path.join(projectRoot, 'composer.json'))) {
139
+ stack.languages.push('php');
140
+ }
141
+
142
+ // Docker/K8s detection
143
+ if (fs.existsSync(path.join(projectRoot, 'Dockerfile'))) {
144
+ stack.devops.push('docker');
145
+ }
146
+ if (
147
+ fs.existsSync(path.join(projectRoot, 'k8s')) ||
148
+ fs.existsSync(path.join(projectRoot, 'kubernetes'))
149
+ ) {
150
+ stack.devops.push('kubernetes', 'k8s');
151
+ }
152
+ if (
153
+ fs.existsSync(path.join(projectRoot, 'terraform')) ||
154
+ fs.existsSync(path.join(projectRoot, 'main.tf'))
155
+ ) {
156
+ stack.devops.push('terraform', 'iac');
157
+ }
158
+ if (fs.existsSync(path.join(projectRoot, 'vercel.json'))) {
159
+ stack.devops.push('vercel', 'deployment');
160
+ }
161
+
162
+ // Deduplicate all arrays
163
+ for (const key of Object.keys(stack)) {
164
+ stack[key] = [...new Set(stack[key])];
165
+ }
166
+
167
+ return stack;
168
+ }
169
+
170
+ // =============================================================================
171
+ // Recommendation Engine
172
+ // =============================================================================
173
+
174
+ /**
175
+ * Score a skill against detected tech stack.
176
+ * @param {Object} skill - Curated skill entry
177
+ * @param {Object} stack - Detected tech stack
178
+ * @returns {number} Relevance score (0-100)
179
+ */
180
+ function scoreSkill(skill, stack) {
181
+ const allTags = [
182
+ ...stack.frameworks,
183
+ ...stack.styling,
184
+ ...stack.testing,
185
+ ...stack.databases,
186
+ ...stack.languages,
187
+ ...stack.devops,
188
+ ...stack.security,
189
+ ];
190
+
191
+ if (allTags.length === 0) return 0;
192
+
193
+ let matchCount = 0;
194
+ for (const tag of skill.tags) {
195
+ if (allTags.includes(tag)) {
196
+ matchCount++;
197
+ }
198
+ }
199
+
200
+ if (matchCount === 0) return 0;
201
+
202
+ // Score: percentage of skill tags that match, weighted by total matches
203
+ const tagCoverage = matchCount / skill.tags.length;
204
+ return Math.round(tagCoverage * 100);
205
+ }
206
+
207
+ /**
208
+ * Get skill recommendations based on detected tech stack.
209
+ * @param {string} projectRoot - Project root directory
210
+ * @param {Object} [options] - Options
211
+ * @param {string[]} [options.installedSkills] - Names of already-installed skills to filter out
212
+ * @param {number} [options.minScore] - Minimum relevance score (default: 20)
213
+ * @param {number} [options.maxResults] - Maximum results per category (default: 5)
214
+ * @returns {Object} Recommendations with stack info and ranked skills
215
+ */
216
+ function getRecommendations(projectRoot, options = {}) {
217
+ const { installedSkills = [], minScore = 20, maxResults = 5 } = options;
218
+
219
+ const stack = detectTechStack(projectRoot);
220
+ const allSkills = getAllCuratedSkills();
221
+ const installedSet = new Set(installedSkills.map(n => n.toLowerCase()));
222
+
223
+ // Score all skills
224
+ const scored = allSkills
225
+ .map(skill => ({
226
+ ...skill,
227
+ score: scoreSkill(skill, stack),
228
+ installed: installedSet.has(skill.name.toLowerCase()),
229
+ }))
230
+ .filter(s => s.score >= minScore && !s.installed)
231
+ .sort((a, b) => b.score - a.score);
232
+
233
+ // Group by category with limits
234
+ const byCategory = {};
235
+ for (const skill of scored) {
236
+ if (!byCategory[skill.category]) {
237
+ byCategory[skill.category] = [];
238
+ }
239
+ if (byCategory[skill.category].length < maxResults) {
240
+ byCategory[skill.category].push(skill);
241
+ }
242
+ }
243
+
244
+ return {
245
+ stack,
246
+ recommendations: byCategory,
247
+ totalMatches: scored.length,
248
+ };
249
+ }
250
+
251
+ /**
252
+ * Format recommendations as a display string.
253
+ * @param {Object} result - Result from getRecommendations
254
+ * @returns {string} Formatted display text
255
+ */
256
+ function formatRecommendations(result) {
257
+ const { stack, recommendations, totalMatches } = result;
258
+
259
+ const lines = [];
260
+
261
+ // Show detected stack
262
+ const detected = [];
263
+ if (stack.frameworks.length) detected.push(`Frameworks: ${stack.frameworks.join(', ')}`);
264
+ if (stack.languages.length) detected.push(`Languages: ${stack.languages.join(', ')}`);
265
+ if (stack.databases.length) detected.push(`Databases: ${stack.databases.join(', ')}`);
266
+ if (stack.testing.length) detected.push(`Testing: ${stack.testing.join(', ')}`);
267
+ if (stack.styling.length) detected.push(`Styling: ${stack.styling.join(', ')}`);
268
+ if (stack.devops.length) detected.push(`DevOps: ${stack.devops.join(', ')}`);
269
+
270
+ if (detected.length > 0) {
271
+ lines.push('**Detected Tech Stack:**');
272
+ for (const d of detected) {
273
+ lines.push(`- ${d}`);
274
+ }
275
+ lines.push('');
276
+ }
277
+
278
+ // Show recommendations by category
279
+ const categories = Object.keys(recommendations);
280
+ if (categories.length === 0) {
281
+ lines.push('No matching skills found for your tech stack.');
282
+ lines.push('Browse the full marketplace: `npx skills find`');
283
+ return lines.join('\n');
284
+ }
285
+
286
+ lines.push(`**Recommended Skills (${totalMatches} matches):**`);
287
+ lines.push('');
288
+
289
+ for (const category of categories) {
290
+ const skills = recommendations[category];
291
+ lines.push(`### ${category}`);
292
+ lines.push('');
293
+ for (const s of skills) {
294
+ lines.push(`- **${s.name}** (${s.score}% match) - ${s.description}`);
295
+ lines.push(` Install: \`${s.installCmd}\``);
296
+ }
297
+ lines.push('');
298
+ }
299
+
300
+ lines.push('---');
301
+ lines.push('Browse more: `npx skills find`');
302
+
303
+ return lines.join('\n');
304
+ }
305
+
306
+ module.exports = {
307
+ detectTechStack,
308
+ scoreSkill,
309
+ getRecommendations,
310
+ formatRecommendations,
311
+ };
@@ -0,0 +1,255 @@
1
+ /**
2
+ * status-writer.js - Canonical write module for status.json mutations
3
+ *
4
+ * ALL status.json story updates should go through this module to ensure:
5
+ * 1. Atomic read-modify-write via file-lock.js
6
+ * 2. State machine validation on status transitions
7
+ * 3. Automatic dependency resolution when stories complete
8
+ *
9
+ * Usage:
10
+ * const { updateStory, readStory } = require('./status-writer');
11
+ * updateStory(rootDir, 'US-0042', { status: 'completed' });
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ // Lazy-load file-lock for atomic writes
20
+ let _fileLock;
21
+ function getFileLock() {
22
+ if (_fileLock === undefined) {
23
+ try {
24
+ _fileLock = require('./file-lock');
25
+ } catch (e) {
26
+ _fileLock = null;
27
+ }
28
+ }
29
+ return _fileLock;
30
+ }
31
+
32
+ // Lazy-load story-state-machine for transition validation
33
+ let _stateMachine;
34
+ function getStateMachine() {
35
+ if (_stateMachine === undefined) {
36
+ try {
37
+ _stateMachine = require('./story-state-machine');
38
+ } catch (e) {
39
+ _stateMachine = null;
40
+ }
41
+ }
42
+ return _stateMachine;
43
+ }
44
+
45
+ // Lazy-load paths module
46
+ let _paths;
47
+ function getPaths() {
48
+ if (_paths === undefined) {
49
+ try {
50
+ _paths = require('../../lib/paths');
51
+ } catch (e) {
52
+ _paths = null;
53
+ }
54
+ }
55
+ return _paths;
56
+ }
57
+
58
+ /**
59
+ * Resolve the status.json file path for a given project root.
60
+ * @param {string} rootDir - Project root directory
61
+ * @returns {string} Absolute path to status.json
62
+ */
63
+ function getStatusFilePath(rootDir) {
64
+ const paths = getPaths();
65
+ if (paths && typeof paths.getStatusPath === 'function') {
66
+ return paths.getStatusPath(rootDir);
67
+ }
68
+ return path.join(rootDir, 'docs', '09-agents', 'status.json');
69
+ }
70
+
71
+ /**
72
+ * Read a single story from status.json.
73
+ *
74
+ * @param {string} rootDir - Project root directory
75
+ * @param {string} storyId - Story ID (e.g., 'US-0042')
76
+ * @returns {{ ok: boolean, story?: object, error?: string }}
77
+ */
78
+ function readStory(rootDir, storyId) {
79
+ try {
80
+ const statusPath = getStatusFilePath(rootDir);
81
+ if (!fs.existsSync(statusPath)) {
82
+ return { ok: false, error: 'status.json not found' };
83
+ }
84
+
85
+ const data = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
86
+ if (!data.stories || !data.stories[storyId]) {
87
+ return { ok: false, error: `Story ${storyId} not found` };
88
+ }
89
+
90
+ return { ok: true, story: data.stories[storyId] };
91
+ } catch (e) {
92
+ return { ok: false, error: e.message };
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Resolve dependencies when a story transitions to completed/done.
98
+ * Pure in-memory operation — mutates `data` in place.
99
+ *
100
+ * Iterates all stories, finds those with `depends_on` or `blocked_by`
101
+ * containing `completedStoryId`. If all dependencies are now
102
+ * completed/done, transitions the story from `blocked` → `ready`.
103
+ *
104
+ * @param {object} data - Full status.json data object (mutated in place)
105
+ * @param {string} completedStoryId - The story that just completed
106
+ * @returns {{ unblocked: string[] }} List of story IDs that were unblocked
107
+ */
108
+ function resolveDependencies(data, completedStoryId) {
109
+ const unblocked = [];
110
+ if (!data || !data.stories) return { unblocked };
111
+
112
+ const sm = getStateMachine();
113
+ const completedStatuses = sm ? sm.COMPLETED_STATUSES : ['completed', 'archived'];
114
+
115
+ // Helper: check if a story ID is in a completed/done state
116
+ function isCompleted(sid) {
117
+ const s = data.stories[sid];
118
+ if (!s) return false;
119
+ return completedStatuses.includes(s.status) || s.status === 'done';
120
+ }
121
+
122
+ for (const [storyId, story] of Object.entries(data.stories)) {
123
+ // Only consider blocked stories
124
+ if (story.status !== 'blocked') continue;
125
+
126
+ // Collect dependency IDs from both fields
127
+ const deps = [];
128
+ if (Array.isArray(story.depends_on)) deps.push(...story.depends_on);
129
+ if (Array.isArray(story.blocked_by)) deps.push(...story.blocked_by);
130
+
131
+ // Skip stories that don't depend on the completed story
132
+ if (!deps.includes(completedStoryId)) continue;
133
+
134
+ // Check if ALL dependencies are now completed/done
135
+ const allMet = deps.every(depId => isCompleted(depId));
136
+ if (!allMet) continue;
137
+
138
+ // Transition blocked → ready
139
+ if (sm) {
140
+ const result = sm.transition({ id: storyId, status: 'blocked' }, 'ready', {
141
+ actor: 'status-writer',
142
+ reason: `Dependencies resolved (${completedStoryId} completed)`,
143
+ });
144
+ if (result.success) {
145
+ story.status = 'ready';
146
+ story.updated_at = new Date().toISOString();
147
+ unblocked.push(storyId);
148
+ }
149
+ } else {
150
+ // No state machine available — direct transition
151
+ story.status = 'ready';
152
+ story.updated_at = new Date().toISOString();
153
+ unblocked.push(storyId);
154
+ }
155
+ }
156
+
157
+ return { unblocked };
158
+ }
159
+
160
+ /**
161
+ * Update a single story in status.json using atomic read-modify-write.
162
+ *
163
+ * When `updates.status` is provided and differs from the current status,
164
+ * validates the transition via story-state-machine. When transitioning
165
+ * to completed/done, triggers resolveDependencies() automatically.
166
+ *
167
+ * @param {string} rootDir - Project root directory
168
+ * @param {string} storyId - Story ID (e.g., 'US-0042')
169
+ * @param {object} updates - Fields to update (e.g., { status: 'completed', assigned_to: 'AG-API' })
170
+ * @param {object} [options={}] - Options
171
+ * @param {boolean} [options.skipValidation=false] - Skip state machine validation
172
+ * @returns {{ ok: boolean, unblocked?: string[], error?: string }}
173
+ */
174
+ function updateStory(rootDir, storyId, updates, options = {}) {
175
+ const { skipValidation = false } = options;
176
+
177
+ try {
178
+ const statusPath = getStatusFilePath(rootDir);
179
+ if (!fs.existsSync(statusPath)) {
180
+ return { ok: false, error: 'status.json not found' };
181
+ }
182
+
183
+ const fileLock = getFileLock();
184
+
185
+ // Mutation function applied inside the lock
186
+ let resultMeta = { unblocked: [] };
187
+
188
+ const modifyFn = data => {
189
+ if (!data.stories) data.stories = {};
190
+ if (!data.stories[storyId]) {
191
+ throw new Error(`Story ${storyId} not found`);
192
+ }
193
+
194
+ const story = data.stories[storyId];
195
+
196
+ // Validate status transition if status is changing
197
+ if (updates.status && updates.status !== story.status && !skipValidation) {
198
+ const sm = getStateMachine();
199
+ if (sm) {
200
+ const valid = sm.isValidTransition(story.status, updates.status);
201
+ if (!valid) {
202
+ const validTargets = sm.getValidTransitions(story.status);
203
+ throw new Error(
204
+ `Invalid transition: ${story.status} → ${updates.status}. ` +
205
+ `Valid transitions: ${validTargets.join(', ') || 'none'}`
206
+ );
207
+ }
208
+ }
209
+ }
210
+
211
+ // Apply updates (null values delete the field)
212
+ Object.assign(story, updates);
213
+ for (const [key, val] of Object.entries(updates)) {
214
+ if (val === null) delete story[key];
215
+ }
216
+ story.updated_at = new Date().toISOString();
217
+
218
+ // Trigger dependency resolution on completion
219
+ const sm = getStateMachine();
220
+ const completedStatuses = sm ? sm.COMPLETED_STATUSES : ['completed', 'archived'];
221
+ if (
222
+ updates.status &&
223
+ (completedStatuses.includes(updates.status) || updates.status === 'done')
224
+ ) {
225
+ const resolved = resolveDependencies(data, storyId);
226
+ resultMeta.unblocked = resolved.unblocked;
227
+ }
228
+
229
+ return data;
230
+ };
231
+
232
+ if (fileLock && typeof fileLock.atomicReadModifyWrite === 'function') {
233
+ const result = fileLock.atomicReadModifyWrite(statusPath, modifyFn);
234
+ if (!result.success) {
235
+ return { ok: false, error: result.error || 'Atomic write failed' };
236
+ }
237
+ } else {
238
+ // Fallback: direct read-modify-write (no lock)
239
+ const data = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
240
+ const modified = modifyFn(data);
241
+ fs.writeFileSync(statusPath, JSON.stringify(modified, null, 2) + '\n');
242
+ }
243
+
244
+ return { ok: true, unblocked: resultMeta.unblocked };
245
+ } catch (e) {
246
+ return { ok: false, error: e.message };
247
+ }
248
+ }
249
+
250
+ module.exports = {
251
+ updateStory,
252
+ readStory,
253
+ resolveDependencies,
254
+ getStatusFilePath,
255
+ };