@qa-gentic/stlc-agents 1.0.5 → 1.0.7

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 (35) hide show
  1. package/README.md +175 -34
  2. package/bin/postinstall.js +125 -44
  3. package/bin/qa-stlc.js +15 -8
  4. package/package.json +2 -2
  5. package/skills/{qa-stlc/AGENT-BEHAVIOR.md → AGENT-BEHAVIOR.md} +7 -6
  6. package/{.github/copilot-instructions/deduplication-protocol.md → skills/deduplication-protocol/SKILL.md} +16 -21
  7. package/skills/generate-gherkin/SKILL.md +287 -0
  8. package/skills/generate-gherkin/references/step-by-step.md +267 -0
  9. package/skills/{qa-stlc/generate-playwright-code.md → generate-playwright-code/SKILL.md} +13 -23
  10. package/{.github/copilot-instructions/generate-test-cases.md → skills/generate-test-cases/SKILL.md} +16 -2
  11. package/skills/qa-jira-manager/SKILL.md +287 -0
  12. package/{.github/copilot-instructions/write-helix-files.md → skills/write-helix-files/SKILL.md} +11 -17
  13. package/src/{boilerplate-bundle.js → cli/boilerplate-bundle.js} +8 -8
  14. package/src/cli/cmd-init.js +145 -0
  15. package/src/{cmd-mcp-config.js → cli/cmd-mcp-config.js} +72 -9
  16. package/src/cli/cmd-skills.js +209 -0
  17. package/src/{cmd-verify.js → cli/cmd-verify.js} +35 -3
  18. package/src/cli/prompt-integration.js +87 -0
  19. package/src/stlc_agents/agent_helix_writer/tools/boilerplate.py +8 -8
  20. package/src/stlc_agents/agent_jira_manager/__init__.py +0 -0
  21. package/src/stlc_agents/agent_jira_manager/server.py +500 -0
  22. package/src/stlc_agents/agent_jira_manager/tools/__init__.py +0 -0
  23. package/src/stlc_agents/agent_jira_manager/tools/jira_workitem.py +467 -0
  24. package/src/stlc_agents/shared_jira/__init__.py +0 -0
  25. package/src/stlc_agents/shared_jira/auth.py +270 -0
  26. package/.github/copilot-instructions/AGENT-BEHAVIOR.md +0 -448
  27. package/.github/copilot-instructions/generate-gherkin.md +0 -550
  28. package/.github/copilot-instructions/generate-playwright-code.md +0 -464
  29. package/skills/qa-stlc/deduplication-protocol.md +0 -303
  30. package/skills/qa-stlc/generate-gherkin.md +0 -550
  31. package/skills/qa-stlc/generate-test-cases.md +0 -176
  32. package/skills/qa-stlc/write-helix-files.md +0 -374
  33. package/src/cmd-init.js +0 -92
  34. package/src/cmd-skills.js +0 -124
  35. /package/src/{cmd-scaffold.js → cli/cmd-scaffold.js} +0 -0
@@ -0,0 +1,145 @@
1
+ /**
2
+ * cmd-init.js — `qa-stlc init`
3
+ *
4
+ * Full bootstrap: install Python agents + skills + MCP config.
5
+ * Accepts --integration <ado|jira|both>. When omitted, reads
6
+ * ~/.qa-stlc/integration (written by postinstall) or prompts interactively.
7
+ */
8
+ "use strict";
9
+
10
+ const { execSync, spawnSync } = require("child_process");
11
+ const path = require("path");
12
+ const fs = require("fs");
13
+ const os = require("os");
14
+
15
+ const cmdSkills = require("./cmd-skills");
16
+ const cmdMcpConfig = require("./cmd-mcp-config");
17
+ const pickIntegration = require("./prompt-integration");
18
+
19
+ const PREF_FILE = path.join(os.homedir(), ".qa-stlc", "integration");
20
+
21
+ function readSavedIntegration() {
22
+ try {
23
+ if (fs.existsSync(PREF_FILE)) return fs.readFileSync(PREF_FILE, "utf8").trim();
24
+ } catch (_) {}
25
+ return null;
26
+ }
27
+
28
+ function saveIntegration(value) {
29
+ try {
30
+ fs.mkdirSync(path.dirname(PREF_FILE), { recursive: true });
31
+ fs.writeFileSync(PREF_FILE, value, "utf8");
32
+ } catch (_) {}
33
+ }
34
+
35
+ const C = {
36
+ reset: "\x1b[0m", bold: "\x1b[1m",
37
+ green: "\x1b[32m", cyan: "\x1b[36m",
38
+ yellow: "\x1b[33m", red: "\x1b[31m", dim: "\x1b[2m",
39
+ };
40
+ const ok = (m) => console.log(`${C.green}✓${C.reset} ${m}`);
41
+ const info = (m) => console.log(`${C.cyan}→${C.reset} ${m}`);
42
+ const warn = (m) => console.log(`${C.yellow}⚠${C.reset} ${m}`);
43
+ const die = (m) => { console.error(`${C.red}✗${C.reset} ${m}`); process.exit(1); };
44
+
45
+ module.exports = async function init(opts) {
46
+ console.log(`\n${C.bold}QA STLC Agents — init${C.reset}\n`);
47
+
48
+ // ── 1. Check Python ──────────────────────────────────────────────────────
49
+ const python = opts.python || "python3";
50
+ info(`Checking Python (${python})…`);
51
+ const pyCheck = spawnSync(python, ["--version"], { encoding: "utf8" });
52
+ if (pyCheck.status !== 0) {
53
+ die(`Python not found at '${python}'. Install Python 3.10+ or pass --python <path>.`);
54
+ }
55
+ const pyVersion = (pyCheck.stdout || pyCheck.stderr || "").trim();
56
+ const match = pyVersion.match(/Python (\d+)\.(\d+)/);
57
+ if (!match || parseInt(match[1]) < 3 || parseInt(match[2]) < 10) {
58
+ die(`Python 3.10+ required, found: ${pyVersion}`);
59
+ }
60
+ ok(`${pyVersion} found.`);
61
+
62
+ // ── 2. Resolve integration choice ────────────────────────────────────────
63
+ // Priority: --integration flag > saved preference > interactive prompt
64
+ let integration = (opts.integration || "").toLowerCase().trim();
65
+ const validIntegrations = ["ado", "jira", "both"];
66
+ if (integration && !validIntegrations.includes(integration)) {
67
+ die(`Invalid --integration value: "${integration}". Use ado, jira, or both.`);
68
+ }
69
+ if (!integration) {
70
+ integration = readSavedIntegration();
71
+ }
72
+ if (!integration) {
73
+ integration = await pickIntegration("ado");
74
+ }
75
+ saveIntegration(integration);
76
+
77
+ const needsAdo = integration === "ado" || integration === "both";
78
+ const needsJira = integration === "jira" || integration === "both";
79
+ info(`Integration: ${C.bold}${integration}${C.reset}`);
80
+
81
+ // ── 3. pip install qa-gentic-stlc-agents ──────────────────────────────────
82
+ info("Installing qa-gentic-stlc-agents (pip)…");
83
+ const pip = spawnSync(python, ["-m", "pip", "install", "qa-gentic-stlc-agents>=1.0.1", "--quiet"], {
84
+ stdio: "inherit",
85
+ encoding: "utf8",
86
+ });
87
+ if (pip.status !== 0) {
88
+ die("pip install failed. Run manually: pip install qa-gentic-stlc-agents");
89
+ }
90
+ ok("qa-gentic-stlc-agents installed.");
91
+
92
+ // ── 4. Install skills ─────────────────────────────────────────────────────
93
+ info("Installing skills…");
94
+ const skillTarget = opts.vscode ? "both" : "claude";
95
+ await cmdSkills({ target: skillTarget, integration });
96
+
97
+ // ── 5. Write MCP config ───────────────────────────────────────────────────
98
+ info("Writing MCP config…");
99
+ await cmdMcpConfig({
100
+ vscode: opts.vscode || false,
101
+ print: false,
102
+ python: python,
103
+ playwrightPort: "8931",
104
+ integration,
105
+ });
106
+
107
+ // ── 6. Done ───────────────────────────────────────────────────────────────
108
+ const mcpLocation = opts.vscode ? ".vscode/mcp.json" : ".mcp.json";
109
+ const skillsLocation = opts.vscode
110
+ ? ".github/copilot-instructions/ (reload VS Code window to activate)"
111
+ : ".claude/skills/";
112
+
113
+ const adoSection = needsAdo ? `
114
+ ${C.dim}ADO pipeline:${C.reset}
115
+ ${C.dim}1 →${C.reset} ${C.yellow}qa-test-case-manager${C.reset} ${C.dim}ADO work item → manual test cases${C.reset}
116
+ ${C.dim}2 →${C.reset} ${C.yellow}qa-gherkin-generator${C.reset} ${C.dim}Epic / Feature / PBI → .feature file${C.reset}
117
+ ${C.dim}3 →${C.reset} ${C.yellow}qa-playwright-generator${C.reset} ${C.dim}Gherkin + live browser → self-healing Playwright${C.reset}
118
+ ${C.dim}4 →${C.reset} ${C.yellow}qa-helix-writer${C.reset} ${C.dim}Generated files → Helix-QA project${C.reset}` : "";
119
+
120
+ const jiraSection = needsJira ? `
121
+ ${C.dim}Jira pipeline:${C.reset}
122
+ ${C.dim}1 →${C.reset} ${C.yellow}qa-jira-manager${C.reset} ${C.dim}Fetch Jira issue + analyse acceptance criteria${C.reset}
123
+ ${C.dim}2 →${C.reset} ${C.yellow}qa-jira-manager${C.reset} ${C.dim}Generate test cases → create in Jira with 'is tested by' links${C.reset}
124
+ ${C.dim}3 →${C.reset} ${C.yellow}qa-gherkin-generator${C.reset} ${C.dim}Generate Gherkin .feature file from Jira issue AC${C.reset}
125
+ ${C.dim}4 →${C.reset} ${C.yellow}qa-playwright-generator${C.reset} ${C.dim}Gherkin + live browser → self-healing Playwright TypeScript${C.reset}
126
+ ${C.dim}5 →${C.reset} ${C.yellow}qa-helix-writer${C.reset} ${C.dim}Generated files → Helix-QA project on disk${C.reset}
127
+ ${C.dim} Requires: JIRA_CLIENT_ID + JIRA_CLIENT_SECRET + JIRA_CLOUD_ID in .env${C.reset}` : "";
128
+
129
+ console.log(`
130
+ ${C.bold}Setup complete.${C.reset}
131
+
132
+ ${C.dim}Integration:${C.reset} ${C.bold}${integration}${C.reset}
133
+ ${C.dim}MCP config :${C.reset} ${mcpLocation}
134
+ ${C.dim}Skills :${C.reset} ${skillsLocation}
135
+
136
+ ${C.bold}Start Playwright MCP${C.reset} ${C.dim}(keep running in a separate terminal):${C.reset}
137
+
138
+ ${C.cyan}npx @playwright/mcp@latest --port 8931${C.reset}
139
+
140
+ ${C.bold}STLC Workflow:${C.reset}
141
+ ${adoSection}${jiraSection}
142
+
143
+ ${C.dim}Change integration at any time: qa-stlc init --integration <ado|jira|both>${C.reset}
144
+ `);
145
+ };
@@ -18,6 +18,19 @@ const ok = (m) => console.log(`${C.green}✓${C.reset} ${m}`);
18
18
  const info = (m) => console.log(`${C.cyan}→${C.reset} ${m}`);
19
19
  const warn = (m) => console.log(`${C.yellow}⚠${C.reset} ${m}`);
20
20
 
21
+ const os = require("os");
22
+ const fs_ = fs; // alias — fs already required above
23
+
24
+ const PREF_FILE_MCP = require("path").join(os.homedir(), ".qa-stlc", "integration");
25
+
26
+ function readIntegrationPref() {
27
+ try {
28
+ if (fs_.existsSync(PREF_FILE_MCP))
29
+ return fs_.readFileSync(PREF_FILE_MCP, "utf8").trim();
30
+ } catch (_) {}
31
+ return "ado";
32
+ }
33
+
21
34
  const CWD = process.cwd();
22
35
  const IS_WIN = process.platform === "win32";
23
36
  const EXT = IS_WIN ? ".exe" : "";
@@ -29,6 +42,14 @@ const AGENT_NAMES = [
29
42
  "qa-helix-writer",
30
43
  ];
31
44
 
45
+ // Jira agent — added separately because it needs env var injection
46
+ const JIRA_AGENT_NAME = "qa-jira-manager";
47
+ const JIRA_ENV_VARS = {
48
+ JIRA_CLIENT_ID: "${env:JIRA_CLIENT_ID}",
49
+ JIRA_CLIENT_SECRET: "${env:JIRA_CLIENT_SECRET}",
50
+ JIRA_CLOUD_ID: "${env:JIRA_CLOUD_ID}",
51
+ };
52
+
32
53
  /**
33
54
  * Locate the binary for an agent.
34
55
  * Priority: .venv → --python dir → system PATH → python sys.executable dir → macOS framework dirs
@@ -76,11 +97,18 @@ function findBinary(name, pythonBin) {
76
97
  return null;
77
98
  }
78
99
 
79
- function buildClaudeConfig(pythonBin, playwrightPort) {
100
+ function buildClaudeConfig(pythonBin, playwrightPort, integration) {
80
101
  const servers = {};
81
102
  const missing = [];
103
+ const needsAdo = !integration || integration === "ado" || integration === "both";
104
+ const needsJira = integration === "jira" || integration === "both";
82
105
 
83
- for (const name of AGENT_NAMES) {
106
+ // ADO agents — always included for ado/both; also included for jira because
107
+ // qa-gherkin-generator, qa-playwright-generator, and qa-helix-writer are shared
108
+ // by the Jira pipeline (Gherkin → Playwright → Helix steps run the same agents).
109
+ const activeNames = (needsAdo || needsJira) ? AGENT_NAMES : [];
110
+
111
+ for (const name of activeNames) {
84
112
  const bin = findBinary(name, pythonBin);
85
113
  if (bin) {
86
114
  servers[name] = { command: bin };
@@ -90,6 +118,21 @@ function buildClaudeConfig(pythonBin, playwrightPort) {
90
118
  }
91
119
  }
92
120
 
121
+ // Jira agent — only when integration includes jira
122
+ if (needsJira) {
123
+ const jiraBin = findBinary(JIRA_AGENT_NAME, pythonBin);
124
+ if (jiraBin) {
125
+ servers[JIRA_AGENT_NAME] = { command: jiraBin, env: JIRA_ENV_VARS };
126
+ } else {
127
+ missing.push(JIRA_AGENT_NAME);
128
+ servers[JIRA_AGENT_NAME] = {
129
+ command: `/path/to/.venv/bin/${JIRA_AGENT_NAME}`,
130
+ env: JIRA_ENV_VARS,
131
+ "_comment": "NOT FOUND — run: pip install qa-gentic-stlc-agents",
132
+ };
133
+ }
134
+ }
135
+
93
136
  servers["playwright"] = {
94
137
  type: "url",
95
138
  url: `ws://localhost:${playwrightPort}`,
@@ -98,11 +141,15 @@ function buildClaudeConfig(pythonBin, playwrightPort) {
98
141
  return { config: { mcpServers: servers }, missing };
99
142
  }
100
143
 
101
- function buildVscodeConfig(pythonBin, playwrightPort) {
144
+ function buildVscodeConfig(pythonBin, playwrightPort, integration) {
102
145
  const servers = {};
103
146
  const missing = [];
147
+ const needsAdo = !integration || integration === "ado" || integration === "both";
148
+ const needsJira = integration === "jira" || integration === "both";
149
+ // Shared agents (Gherkin, Playwright, Helix) are needed by both ADO and Jira pipelines.
150
+ const activeNames = (needsAdo || needsJira) ? AGENT_NAMES : [];
104
151
 
105
- for (const name of AGENT_NAMES) {
152
+ for (const name of activeNames) {
106
153
  const bin = findBinary(name, pythonBin);
107
154
  if (bin) {
108
155
  servers[name] = { command: bin };
@@ -112,6 +159,21 @@ function buildVscodeConfig(pythonBin, playwrightPort) {
112
159
  }
113
160
  }
114
161
 
162
+ // Jira agent — only when integration includes jira
163
+ if (needsJira) {
164
+ const jiraBinV = findBinary(JIRA_AGENT_NAME, pythonBin);
165
+ if (jiraBinV) {
166
+ servers[JIRA_AGENT_NAME] = { command: jiraBinV, env: JIRA_ENV_VARS };
167
+ } else {
168
+ missing.push(JIRA_AGENT_NAME);
169
+ servers[JIRA_AGENT_NAME] = {
170
+ command: `/path/to/.venv/bin/${JIRA_AGENT_NAME}`,
171
+ env: JIRA_ENV_VARS,
172
+ "_comment": "NOT FOUND — run: pip install qa-gentic-stlc-agents",
173
+ };
174
+ }
175
+ }
176
+
115
177
  servers["playwright"] = {
116
178
  type: "http",
117
179
  url: `http://localhost:${playwrightPort}/mcp`,
@@ -139,10 +201,11 @@ module.exports = async function mcpConfig(opts) {
139
201
  const printOnly = opts.print || false;
140
202
  const pythonBin = opts.python || "python3";
141
203
  const playwrightPort = opts.playwrightPort || "8931";
204
+ const integration = (opts.integration || readIntegrationPref() || "ado").toLowerCase();
142
205
 
143
206
  if (printOnly) {
144
- const { config: claudeCfg } = buildClaudeConfig(pythonBin, playwrightPort);
145
- const { config: vscodeCfg } = buildVscodeConfig(pythonBin, playwrightPort);
207
+ const { config: claudeCfg } = buildClaudeConfig(pythonBin, playwrightPort, integration);
208
+ const { config: vscodeCfg } = buildVscodeConfig(pythonBin, playwrightPort, integration);
146
209
  console.log("\n=== Claude Code (.mcp.json) ===");
147
210
  console.log(JSON.stringify(claudeCfg, null, 2));
148
211
  console.log("\n=== VS Code (.vscode/mcp.json) ===");
@@ -152,7 +215,7 @@ module.exports = async function mcpConfig(opts) {
152
215
  }
153
216
 
154
217
  if (useVscode) {
155
- const { config, missing } = buildVscodeConfig(pythonBin, playwrightPort);
218
+ const { config, missing } = buildVscodeConfig(pythonBin, playwrightPort, integration);
156
219
  const dir = path.join(CWD, ".vscode");
157
220
  fs.mkdirSync(dir, { recursive: true });
158
221
  const out = path.join(dir, "mcp.json");
@@ -164,7 +227,7 @@ module.exports = async function mcpConfig(opts) {
164
227
  }
165
228
  printNextSteps("vscode", playwrightPort);
166
229
  } else {
167
- const { config, missing } = buildClaudeConfig(pythonBin, playwrightPort);
230
+ const { config, missing } = buildClaudeConfig(pythonBin, playwrightPort, integration);
168
231
  const out = path.join(CWD, ".mcp.json");
169
232
  fs.writeFileSync(out, JSON.stringify(config, null, 2) + "\n", "utf8");
170
233
  ok(`Written → .mcp.json`);
@@ -174,4 +237,4 @@ module.exports = async function mcpConfig(opts) {
174
237
  }
175
238
  printNextSteps("claude", playwrightPort);
176
239
  }
177
- };
240
+ };
@@ -0,0 +1,209 @@
1
+ /**
2
+ * cmd-skills.js — `qa-stlc skills`
3
+ *
4
+ * Copies skill markdown files into the correct directory for each coding agent:
5
+ *
6
+ * claude → .claude/skills/ + .claude/AGENT-BEHAVIOR.md
7
+ * vscode → .github/copilot-instructions/
8
+ * cursor → .cursor/rules/ (one file per skill)
9
+ * windsurf → .windsurf/rules/
10
+ * both → claude + vscode
11
+ * print → stdout only
12
+ */
13
+ "use strict";
14
+
15
+ const path = require("path");
16
+ const fs = require("fs");
17
+
18
+ const C = {
19
+ reset: "\x1b[0m", bold: "\x1b[1m",
20
+ green: "\x1b[32m", yellow: "\x1b[33m", dim: "\x1b[2m",
21
+ };
22
+ const ok = (m) => console.log(`${C.green}✓${C.reset} ${m}`);
23
+ const info = (m) => console.log(` ${C.dim}${m}${C.reset}`);
24
+ const warn = (m) => console.log(`${C.yellow}⚠${C.reset} ${m}`);
25
+
26
+ const os = require("os");
27
+ const PREF_FILE_SK = require("path").join(os.homedir(), ".qa-stlc", "integration");
28
+
29
+ function readIntegrationPrefSk() {
30
+ try {
31
+ if (fs.existsSync(PREF_FILE_SK))
32
+ return fs.readFileSync(PREF_FILE_SK, "utf8").trim();
33
+ } catch (_) {}
34
+ return "ado";
35
+ }
36
+
37
+ // Resolve the skills bundled with this npm package
38
+ const PKG_ROOT = path.resolve(__dirname, "..");
39
+ const SKILLS_DIR = path.join(PKG_ROOT, "skills");
40
+ const BEHAVIOR_MD = path.join(SKILLS_DIR, "AGENT-BEHAVIOR.md");
41
+ const AGENTS_DIR = path.join(PKG_ROOT, ".github", "agents");
42
+
43
+ /** Copy a file, creating parent dirs as needed. */
44
+ function cp(src, dest) {
45
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
46
+ fs.copyFileSync(src, dest);
47
+ }
48
+
49
+ /**
50
+ * List skill directories (each containing a SKILL.md), filtered by integration.
51
+ * Returns objects: { name, skillMd, dir }
52
+ */
53
+ function skillEntries(integration) {
54
+ const integ = integration || readIntegrationPrefSk() || "ado";
55
+ const includeJira = integ === "jira" || integ === "both";
56
+ const adoOnlySkills = new Set(["generate-test-cases"]);
57
+ return fs.readdirSync(SKILLS_DIR)
58
+ .filter((entry) => {
59
+ // Only directories with a SKILL.md
60
+ const skillMd = path.join(SKILLS_DIR, entry, "SKILL.md");
61
+ if (!fs.existsSync(skillMd)) return false;
62
+ // Jira skill — only when integration includes jira
63
+ if (entry === "qa-jira-manager" && !includeJira) return false;
64
+ // ADO-only skills — exclude when integration is jira-only
65
+ if (adoOnlySkills.has(entry) && integ === "jira") return false;
66
+ return true;
67
+ })
68
+ .map((entry) => ({
69
+ name: entry,
70
+ skillMd: path.join(SKILLS_DIR, entry, "SKILL.md"),
71
+ dir: path.join(SKILLS_DIR, entry),
72
+ }));
73
+ }
74
+
75
+ /** Back-compat: returns array of SKILL.md paths (used for print/count). */
76
+ function skillFiles(integration) {
77
+ return skillEntries(integration).map((e) => e.skillMd);
78
+ }
79
+
80
+ const CWD = process.cwd();
81
+
82
+ /** List .agent.md files, filtered by integration. */
83
+ function agentFiles(integration) {
84
+ const integ = integration || readIntegrationPrefSk() || "ado";
85
+ const includeJira = integ === "jira" || integ === "both";
86
+ const adoOnlyAgents = new Set(["qa-test-case-manager.agent.md"]);
87
+ return fs.readdirSync(AGENTS_DIR)
88
+ .filter((f) => {
89
+ if (!f.endsWith(".agent.md")) return false;
90
+ if (f === "qa-jira-manager.agent.md" && !includeJira) return false;
91
+ if (adoOnlyAgents.has(f) && integ === "jira") return false;
92
+ return true;
93
+ })
94
+ .map((f) => path.join(AGENTS_DIR, f));
95
+ }
96
+
97
+ /** Install .agent.md files to .github/agents/ in the consumer project. */
98
+ function installAgents(integration) {
99
+ if (!fs.existsSync(AGENTS_DIR)) return;
100
+ const dest = path.join(CWD, ".github", "agents");
101
+ for (const src of agentFiles(integration)) {
102
+ cp(src, path.join(dest, path.basename(src)));
103
+ }
104
+ ok(`Agent files installed → .github/agents/`);
105
+ agentFiles(integration).forEach((f) => info(path.basename(f)));
106
+ }
107
+
108
+ function installClaude(integration) {
109
+ const dest = path.join(CWD, ".claude", "skills");
110
+ // Copy entire skill directory (preserves references/ subdirectory)
111
+ for (const entry of skillEntries(integration)) {
112
+ const targetDir = path.join(dest, entry.name);
113
+ fs.mkdirSync(targetDir, { recursive: true });
114
+ copyDirRecursive(entry.dir, targetDir);
115
+ }
116
+ cp(BEHAVIOR_MD, path.join(CWD, ".claude", "AGENT-BEHAVIOR.md"));
117
+ ok(`Skills installed → .claude/skills/`);
118
+ info("AGENT-BEHAVIOR.md → .claude/AGENT-BEHAVIOR.md");
119
+ skillEntries(integration).forEach((e) => info(`${e.name}/SKILL.md`));
120
+ installAgents(integration);
121
+ printPlaywrightHint();
122
+ }
123
+
124
+ function installVscode(integration) {
125
+ const dest = path.join(CWD, ".github", "copilot-instructions");
126
+ // Flat copy: SKILL.md → <name>.md
127
+ for (const entry of skillEntries(integration)) {
128
+ cp(entry.skillMd, path.join(dest, `${entry.name}.md`));
129
+ }
130
+ cp(BEHAVIOR_MD, path.join(dest, "AGENT-BEHAVIOR.md"));
131
+ ok(`Skills installed → .github/copilot-instructions/`);
132
+ info("Add to .github/copilot-instructions.md:");
133
+ info(" @.github/copilot-instructions/AGENT-BEHAVIOR.md");
134
+ installAgents(integration);
135
+ printPlaywrightHint();
136
+ }
137
+
138
+ function installCursor(integration) {
139
+ const dest = path.join(CWD, ".cursor", "rules");
140
+ // Flat copy: SKILL.md → <name>.md
141
+ for (const entry of skillEntries(integration)) {
142
+ cp(entry.skillMd, path.join(dest, `${entry.name}.md`));
143
+ }
144
+ cp(BEHAVIOR_MD, path.join(dest, "AGENT-BEHAVIOR.md"));
145
+ ok(`Skills installed → .cursor/rules/`);
146
+ printPlaywrightHint();
147
+ }
148
+
149
+ function installWindsurf(integration) {
150
+ const dest = path.join(CWD, ".windsurf", "rules");
151
+ // Flat copy: SKILL.md → <name>.md
152
+ for (const entry of skillEntries(integration)) {
153
+ cp(entry.skillMd, path.join(dest, `${entry.name}.md`));
154
+ }
155
+ cp(BEHAVIOR_MD, path.join(dest, "AGENT-BEHAVIOR.md"));
156
+ ok(`Skills installed → .windsurf/rules/`);
157
+ printPlaywrightHint();
158
+ }
159
+
160
+ function printSkills() {
161
+ console.log("\nAvailable skills:\n");
162
+ skillEntries().forEach((e) => console.log(` ${e.name}/SKILL.md`));
163
+ console.log(` AGENT-BEHAVIOR.md`);
164
+ }
165
+
166
+ /** Recursively copy a directory tree. */
167
+ function copyDirRecursive(src, dest) {
168
+ fs.mkdirSync(dest, { recursive: true });
169
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
170
+ const srcPath = path.join(src, entry.name);
171
+ const destPath = path.join(dest, entry.name);
172
+ if (entry.isDirectory()) {
173
+ copyDirRecursive(srcPath, destPath);
174
+ } else {
175
+ fs.copyFileSync(srcPath, destPath);
176
+ }
177
+ }
178
+ }
179
+
180
+ function printPlaywrightHint() {
181
+ console.log(`
182
+ ${C.dim}Start Playwright MCP before running generation workflows:${C.reset}
183
+ npx @playwright/mcp@latest --port 8931
184
+ `);
185
+ }
186
+
187
+ module.exports = async function skills(opts) {
188
+ const target = (opts.target || "claude").toLowerCase();
189
+
190
+ if (!fs.existsSync(SKILLS_DIR)) {
191
+ console.error(`Skills directory not found: ${SKILLS_DIR}`);
192
+ console.error("Try reinstalling: npm install -g @qa-stlc/agents");
193
+ process.exit(1);
194
+ }
195
+
196
+ const integ = opts.integration || readIntegrationPrefSk() || "ado";
197
+ switch (target) {
198
+ case "claude": installClaude(integ); break;
199
+ case "vscode": installVscode(integ); break;
200
+ case "cursor": installCursor(integ); break;
201
+ case "windsurf": installWindsurf(integ); break;
202
+ case "both": installClaude(integ); installVscode(integ); break;
203
+ case "print": printSkills(); break;
204
+ default:
205
+ warn(`Unknown target: ${target}`);
206
+ warn("Valid targets: claude, vscode, cursor, windsurf, both, print");
207
+ process.exit(1);
208
+ }
209
+ };
@@ -24,6 +24,16 @@ const fail = (m) => console.log(` ${C.red}✗${C.reset} ${m}`);
24
24
  const warn = (m) => console.log(` ${C.yellow}⚠${C.reset} ${m}`);
25
25
  const head = (m) => console.log(`\n${C.bold}${m}${C.reset}`);
26
26
 
27
+ const PREF_FILE_V = require("path").join(os.homedir(), ".qa-stlc", "integration");
28
+
29
+ function readIntegrationPrefV() {
30
+ try {
31
+ if (fs.existsSync(PREF_FILE_V))
32
+ return fs.readFileSync(PREF_FILE_V, "utf8").trim();
33
+ } catch (_) {}
34
+ return "ado";
35
+ }
36
+
27
37
  const CWD = process.cwd();
28
38
  const IS_WIN = process.platform === "win32";
29
39
  const EXT = IS_WIN ? ".exe" : "";
@@ -33,6 +43,7 @@ const AGENT_NAMES = [
33
43
  "qa-gherkin-generator",
34
44
  "qa-playwright-generator",
35
45
  "qa-helix-writer",
46
+ "qa-jira-manager",
36
47
  ];
37
48
 
38
49
  function findBinary(name) {
@@ -63,8 +74,17 @@ module.exports = async function verify(opts) {
63
74
  let allOk = true;
64
75
 
65
76
  // ── 1. Python agents ──────────────────────────────────────────────────────
66
- head("Python MCP Agents");
67
- for (const name of AGENT_NAMES) {
77
+ const integration = (opts.integration || readIntegrationPrefV() || "ado").toLowerCase();
78
+ const needsAdo = integration === "ado" || integration === "both";
79
+ const needsJira = integration === "jira" || integration === "both";
80
+ // Shared agents (Gherkin, Playwright, Helix) are required by both ADO and Jira pipelines.
81
+ const activeNames = [
82
+ ...((needsAdo || needsJira) ? AGENT_NAMES : []),
83
+ ...(needsJira ? ["qa-jira-manager"] : []),
84
+ ].filter((v, i, a) => a.indexOf(v) === i); // deduplicate for 'both' case
85
+
86
+ head(`Python MCP Agents (integration: ${C.bold}${integration}${C.reset})`);
87
+ for (const name of activeNames) {
68
88
  const bin = findBinary(name);
69
89
  if (bin) {
70
90
  ok(`${name} ${C.dim}${bin}${C.reset}`);
@@ -99,6 +119,18 @@ module.exports = async function verify(opts) {
99
119
  warn(`Set AZURE_CLIENT_ID + AZURE_CLIENT_SECRET + AZURE_TENANT_ID for CI/CD.`);
100
120
  }
101
121
 
122
+ // ── 3b. Jira auth cache ──────────────────────────────────────────────────────
123
+ head("Jira Auth (OAuth 2.0)");
124
+ const jiraCache = path.join(os.homedir(), ".jira-cache", "jira-token.json");
125
+ if (fs.existsSync(jiraCache)) {
126
+ const stat = fs.statSync(jiraCache);
127
+ ok(`Token cache found ${C.dim}${jiraCache} (${Math.round(stat.size / 1024)} KB)${C.reset}`);
128
+ } else {
129
+ warn(`No Jira token cache at ${jiraCache}`);
130
+ warn(`A browser will open the first time qa-jira-manager is called.`);
131
+ warn(`Set JIRA_CLIENT_ID + JIRA_CLIENT_SECRET + JIRA_CLOUD_ID in .env`);
132
+ }
133
+
102
134
  // ── 4. MCP config files ───────────────────────────────────────────────────
103
135
  head("MCP Config");
104
136
  const mcpJson = path.join(CWD, ".mcp.json");
@@ -126,4 +158,4 @@ module.exports = async function verify(opts) {
126
158
  }
127
159
 
128
160
  console.log();
129
- };
161
+ };
@@ -0,0 +1,87 @@
1
+ /**
2
+ * prompt-integration.js — Interactive integration picker
3
+ *
4
+ * Presents a numbered menu asking the user which integration(s) they want:
5
+ * 1. Azure DevOps only (Agents 1–4)
6
+ * 2. Jira Cloud only (Full pipeline: qa-jira-manager + shared Agents 2–4)
7
+ * 3. Both
8
+ *
9
+ * Works in interactive terminals (TTY). Falls back to a default (ado) when
10
+ * stdin is not a TTY (e.g. CI, piped installs) — callers can override the
11
+ * default via the `fallback` argument.
12
+ *
13
+ * Returns a Promise that resolves to: "ado" | "jira" | "both"
14
+ *
15
+ * Usage:
16
+ * const pickIntegration = require("./prompt-integration");
17
+ * const integration = await pickIntegration(); // "ado" | "jira" | "both"
18
+ */
19
+ "use strict";
20
+
21
+ const readline = require("readline");
22
+
23
+ const C = {
24
+ reset: "\x1b[0m", bold: "\x1b[1m",
25
+ green: "\x1b[32m", cyan: "\x1b[36m",
26
+ yellow: "\x1b[33m", dim: "\x1b[2m", blue: "\x1b[34m",
27
+ };
28
+
29
+ const MENU = `
30
+ ${C.bold}Which project management integration do you need?${C.reset}
31
+
32
+ ${C.cyan}1${C.reset} ${C.bold}Azure DevOps${C.reset} ${C.dim}(Agents 1–4: test cases, Gherkin, Playwright, Helix-QA)${C.reset}
33
+ ${C.cyan}2${C.reset} ${C.bold}Jira Cloud${C.reset} ${C.dim}(Full pipeline: fetch issues → test cases → Gherkin → Playwright → Helix-QA)${C.reset}
34
+ ${C.cyan}3${C.reset} ${C.bold}Both${C.reset} ${C.dim}(All five agents — use ADO and Jira side by side)${C.reset}
35
+
36
+ ${C.dim}Tip: you can change this later with qa-stlc init --integration <ado|jira|both>${C.reset}
37
+
38
+ Enter 1, 2, or 3 [default: 1]: `;
39
+
40
+ const CHOICES = { "1": "ado", "2": "jira", "3": "both", "": "ado" };
41
+
42
+ /**
43
+ * @param {string} [fallback="ado"] - Value to return when stdin is not a TTY.
44
+ * @returns {Promise<"ado"|"jira"|"both">}
45
+ */
46
+ module.exports = function pickIntegration(fallback = "ado") {
47
+ // Non-interactive environment — skip the prompt entirely
48
+ if (!process.stdin.isTTY) {
49
+ console.log(
50
+ `${C.dim}→ Non-interactive install — defaulting to integration: ${C.reset}${C.bold}${fallback}${C.reset}`
51
+ );
52
+ console.log(
53
+ `${C.dim} To change, run: qa-stlc init --integration <ado|jira|both>${C.reset}\n`
54
+ );
55
+ return Promise.resolve(fallback);
56
+ }
57
+
58
+ return new Promise((resolve) => {
59
+ const rl = readline.createInterface({
60
+ input: process.stdin,
61
+ output: process.stdout,
62
+ });
63
+
64
+ function ask() {
65
+ rl.question(MENU, (answer) => {
66
+ const trimmed = answer.trim();
67
+ const choice = CHOICES[trimmed];
68
+
69
+ if (choice) {
70
+ rl.close();
71
+ const label = choice === "ado" ? "Azure DevOps"
72
+ : choice === "jira" ? "Jira Cloud"
73
+ : "Azure DevOps + Jira Cloud";
74
+ console.log(`\n${C.green}✓${C.reset} Integration selected: ${C.bold}${label}${C.reset}\n`);
75
+ resolve(choice);
76
+ } else {
77
+ console.log(
78
+ `${C.yellow} Please enter 1, 2, or 3.${C.reset}`
79
+ );
80
+ ask();
81
+ }
82
+ });
83
+ }
84
+
85
+ ask();
86
+ });
87
+ };