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.
- package/CHANGELOG.md +10 -0
- package/README.md +6 -6
- package/lib/skill-loader.js +0 -1
- package/package.json +1 -1
- package/scripts/agileflow-statusline.sh +81 -0
- package/scripts/agileflow-welcome.js +79 -0
- package/scripts/claude-tmux.sh +90 -23
- package/scripts/claude-watchdog.sh +225 -0
- package/scripts/generators/agent-registry.js +14 -1
- package/scripts/generators/inject-babysit.js +22 -9
- package/scripts/generators/inject-help.js +19 -9
- package/scripts/lib/ac-test-matcher.js +452 -0
- package/scripts/lib/audit-cleanup.js +250 -0
- package/scripts/lib/audit-registry.js +304 -0
- package/scripts/lib/configure-features.js +35 -0
- package/scripts/lib/feature-catalog.js +3 -3
- package/scripts/lib/gate-enforcer.js +295 -0
- package/scripts/lib/model-profiles.js +118 -0
- package/scripts/lib/quality-gates.js +163 -0
- package/scripts/lib/signal-detectors.js +44 -1
- package/scripts/lib/skill-catalog.js +557 -0
- package/scripts/lib/skill-recommender.js +311 -0
- package/scripts/lib/status-writer.js +255 -0
- package/scripts/lib/story-claiming.js +128 -45
- package/scripts/lib/task-sync.js +32 -38
- package/scripts/lib/tdd-phase-manager.js +455 -0
- package/scripts/lib/team-events.js +34 -3
- package/scripts/lib/tmux-audit-monitor.js +611 -0
- package/scripts/lib/tmux-group-colors.js +113 -0
- package/scripts/lib/tool-registry.yaml +241 -0
- package/scripts/lib/tool-shed.js +441 -0
- package/scripts/messaging-bridge.js +209 -1
- package/scripts/native-team-observer.js +219 -0
- package/scripts/obtain-context.js +14 -0
- package/scripts/ralph-loop.js +30 -5
- package/scripts/smart-detect.js +21 -0
- package/scripts/spawn-audit-sessions.js +877 -0
- package/scripts/team-manager.js +56 -16
- package/scripts/tmux-close-windows.sh +180 -0
- package/src/core/agents/a11y-analyzer-aria.md +155 -0
- package/src/core/agents/a11y-analyzer-forms.md +162 -0
- package/src/core/agents/a11y-analyzer-keyboard.md +175 -0
- package/src/core/agents/a11y-analyzer-semantic.md +153 -0
- package/src/core/agents/a11y-analyzer-visual.md +158 -0
- package/src/core/agents/a11y-consensus.md +248 -0
- package/src/core/agents/ads-audit-budget.md +181 -0
- package/src/core/agents/ads-audit-compliance.md +169 -0
- package/src/core/agents/ads-audit-creative.md +164 -0
- package/src/core/agents/ads-audit-google.md +226 -0
- package/src/core/agents/ads-audit-meta.md +183 -0
- package/src/core/agents/ads-audit-tracking.md +197 -0
- package/src/core/agents/ads-consensus.md +396 -0
- package/src/core/agents/ads-generate.md +145 -0
- package/src/core/agents/ads-performance-tracker.md +197 -0
- package/src/core/agents/api-quality-analyzer-conventions.md +148 -0
- package/src/core/agents/api-quality-analyzer-docs.md +176 -0
- package/src/core/agents/api-quality-analyzer-errors.md +183 -0
- package/src/core/agents/api-quality-analyzer-pagination.md +171 -0
- package/src/core/agents/api-quality-analyzer-versioning.md +143 -0
- package/src/core/agents/api-quality-consensus.md +214 -0
- package/src/core/agents/arch-analyzer-circular.md +148 -0
- package/src/core/agents/arch-analyzer-complexity.md +171 -0
- package/src/core/agents/arch-analyzer-coupling.md +146 -0
- package/src/core/agents/arch-analyzer-layering.md +151 -0
- package/src/core/agents/arch-analyzer-patterns.md +162 -0
- package/src/core/agents/arch-consensus.md +227 -0
- package/src/core/agents/brainstorm-analyzer-features.md +169 -0
- package/src/core/agents/brainstorm-analyzer-growth.md +161 -0
- package/src/core/agents/brainstorm-analyzer-integration.md +172 -0
- package/src/core/agents/brainstorm-analyzer-market.md +147 -0
- package/src/core/agents/brainstorm-analyzer-ux.md +167 -0
- package/src/core/agents/brainstorm-consensus.md +237 -0
- package/src/core/agents/completeness-consensus.md +5 -5
- package/src/core/agents/perf-consensus.md +2 -2
- package/src/core/agents/security-consensus.md +2 -2
- package/src/core/agents/seo-analyzer-content.md +167 -0
- package/src/core/agents/seo-analyzer-images.md +187 -0
- package/src/core/agents/seo-analyzer-performance.md +206 -0
- package/src/core/agents/seo-analyzer-schema.md +176 -0
- package/src/core/agents/seo-analyzer-sitemap.md +172 -0
- package/src/core/agents/seo-analyzer-technical.md +144 -0
- package/src/core/agents/seo-consensus.md +289 -0
- package/src/core/agents/test-consensus.md +2 -2
- package/src/core/commands/adr.md +1 -0
- package/src/core/commands/ads/audit.md +375 -0
- package/src/core/commands/ads/budget.md +97 -0
- package/src/core/commands/ads/competitor.md +112 -0
- package/src/core/commands/ads/creative.md +85 -0
- package/src/core/commands/ads/generate.md +238 -0
- package/src/core/commands/ads/google.md +112 -0
- package/src/core/commands/ads/health.md +327 -0
- package/src/core/commands/ads/landing.md +119 -0
- package/src/core/commands/ads/linkedin.md +112 -0
- package/src/core/commands/ads/meta.md +91 -0
- package/src/core/commands/ads/microsoft.md +115 -0
- package/src/core/commands/ads/plan.md +321 -0
- package/src/core/commands/ads/test-plan.md +317 -0
- package/src/core/commands/ads/tiktok.md +129 -0
- package/src/core/commands/ads/track.md +288 -0
- package/src/core/commands/ads/youtube.md +124 -0
- package/src/core/commands/ads.md +140 -0
- package/src/core/commands/assign.md +1 -0
- package/src/core/commands/audit.md +43 -6
- package/src/core/commands/babysit.md +315 -1266
- package/src/core/commands/baseline.md +1 -0
- package/src/core/commands/blockers.md +1 -0
- package/src/core/commands/board.md +1 -0
- package/src/core/commands/changelog.md +1 -0
- package/src/core/commands/choose.md +1 -0
- package/src/core/commands/ci.md +1 -0
- package/src/core/commands/code/accessibility.md +347 -0
- package/src/core/commands/code/api.md +297 -0
- package/src/core/commands/code/architecture.md +297 -0
- package/src/core/commands/{audit → code}/completeness.md +72 -25
- package/src/core/commands/{audit → code}/legal.md +63 -16
- package/src/core/commands/{audit → code}/logic.md +64 -16
- package/src/core/commands/{audit → code}/performance.md +67 -20
- package/src/core/commands/{audit → code}/security.md +69 -19
- package/src/core/commands/{audit → code}/test.md +67 -20
- package/src/core/commands/configure.md +1 -0
- package/src/core/commands/council.md +1 -0
- package/src/core/commands/deploy.md +1 -0
- package/src/core/commands/diagnose.md +1 -0
- package/src/core/commands/docs.md +1 -0
- package/src/core/commands/epic/edit.md +213 -0
- package/src/core/commands/epic.md +1 -0
- package/src/core/commands/export.md +238 -0
- package/src/core/commands/help.md +16 -1
- package/src/core/commands/{discovery → ideate}/brief.md +12 -12
- package/src/core/commands/{discovery/new.md → ideate/discover.md} +20 -16
- package/src/core/commands/ideate/features.md +496 -0
- package/src/core/commands/ideate/new.md +158 -124
- package/src/core/commands/impact.md +1 -0
- package/src/core/commands/learn/explain.md +118 -0
- package/src/core/commands/learn/glossary.md +135 -0
- package/src/core/commands/learn/patterns.md +138 -0
- package/src/core/commands/learn/tour.md +126 -0
- package/src/core/commands/migrate/codemods.md +151 -0
- package/src/core/commands/migrate/plan.md +131 -0
- package/src/core/commands/migrate/scan.md +114 -0
- package/src/core/commands/migrate/validate.md +119 -0
- package/src/core/commands/multi-expert.md +1 -0
- package/src/core/commands/pr.md +1 -0
- package/src/core/commands/review.md +1 -0
- package/src/core/commands/seo/audit.md +373 -0
- package/src/core/commands/seo/competitor.md +174 -0
- package/src/core/commands/seo/content.md +107 -0
- package/src/core/commands/seo/geo.md +229 -0
- package/src/core/commands/seo/hreflang.md +140 -0
- package/src/core/commands/seo/images.md +96 -0
- package/src/core/commands/seo/page.md +198 -0
- package/src/core/commands/seo/plan.md +163 -0
- package/src/core/commands/seo/programmatic.md +131 -0
- package/src/core/commands/seo/references/cwv-thresholds.md +64 -0
- package/src/core/commands/seo/references/eeat-framework.md +110 -0
- package/src/core/commands/seo/references/quality-gates.md +91 -0
- package/src/core/commands/seo/references/schema-types.md +102 -0
- package/src/core/commands/seo/schema.md +183 -0
- package/src/core/commands/seo/sitemap.md +97 -0
- package/src/core/commands/seo/technical.md +100 -0
- package/src/core/commands/seo.md +107 -0
- package/src/core/commands/skill/list.md +68 -212
- package/src/core/commands/skill/recommend.md +216 -0
- package/src/core/commands/sprint.md +1 -0
- package/src/core/commands/status/undo.md +191 -0
- package/src/core/commands/status.md +1 -0
- package/src/core/commands/story/edit.md +204 -0
- package/src/core/commands/story/view.md +29 -7
- package/src/core/commands/story-validate.md +1 -0
- package/src/core/commands/story.md +1 -0
- package/src/core/commands/tdd-next.md +238 -0
- package/src/core/commands/tdd.md +211 -0
- package/src/core/commands/team/start.md +10 -6
- package/src/core/commands/tests.md +1 -0
- package/src/core/commands/verify.md +27 -1
- package/src/core/commands/workflow.md +2 -0
- package/src/core/experts/_core-expertise.yaml +105 -0
- package/src/core/experts/analytics/expertise.yaml +5 -99
- package/src/core/experts/codebase-query/expertise.yaml +3 -72
- package/src/core/experts/compliance/expertise.yaml +6 -72
- package/src/core/experts/database/expertise.yaml +9 -52
- package/src/core/experts/documentation/expertise.yaml +7 -140
- package/src/core/experts/integrations/expertise.yaml +7 -127
- package/src/core/experts/mentor/expertise.yaml +8 -35
- package/src/core/experts/monitoring/expertise.yaml +7 -49
- package/src/core/experts/performance/expertise.yaml +1 -26
- package/src/core/experts/security/expertise.yaml +9 -34
- package/src/core/experts/ui/expertise.yaml +6 -36
- package/src/core/knowledge/ads/ad-audit-checklist-scoring.md +424 -0
- package/src/core/knowledge/ads/ad-optimization-logic.md +590 -0
- package/src/core/knowledge/ads/ad-technical-specifications.md +385 -0
- package/src/core/knowledge/ads/definitive-advertising-reference-2026.md +506 -0
- package/src/core/knowledge/ads/paid-advertising-research-2026.md +445 -0
- package/src/core/teams/backend.json +41 -0
- package/src/core/teams/frontend.json +41 -0
- package/src/core/teams/qa.json +41 -0
- package/src/core/teams/solo.json +35 -0
- package/src/core/templates/agileflow-metadata.json +20 -1
- package/tools/cli/commands/setup.js +85 -3
- package/tools/cli/commands/update.js +42 -0
- package/tools/cli/installers/ide/_base-ide.js +42 -5
- package/tools/cli/installers/ide/claude-code.js +71 -3
- package/tools/cli/lib/content-injector.js +160 -12
- package/tools/cli/lib/docs-setup.js +1 -1
- package/src/core/commands/skill/create.md +0 -698
- package/src/core/commands/skill/delete.md +0 -316
- package/src/core/commands/skill/edit.md +0 -359
- package/src/core/commands/skill/test.md +0 -394
- package/src/core/commands/skill/upgrade.md +0 -552
- 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
|
+
};
|