@qa-gentic/stlc-agents 1.0.16 → 1.0.18
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/ORCHESTRATION_RULES.md +283 -0
- package/README.md +246 -321
- package/bin/postinstall.js +26 -2
- package/bin/qa-stlc.js +23 -0
- package/package.json +15 -2
- package/skills/write-helix-files/SKILL.md +6 -0
- package/src/cli/cmd-cost.js +253 -0
- package/src/cli/cmd-init.js +19 -2
- package/src/cli/cmd-mcp-config.js +123 -62
- package/src/cli/cmd-skills.js +21 -4
- package/src/stlc_agents/agent_gherkin_generator/server.py +88 -4
- package/src/stlc_agents/agent_helix_writer/tools/helix_write.py +60 -28
- package/src/stlc_agents/agent_jira_manager/server.py +209 -2
- package/src/stlc_agents/agent_jira_manager/tools/jira_workitem.py +36 -0
- package/src/stlc_agents/agent_playwright_generator/server.py +968 -105
- package/src/stlc_agents/agent_test_case_manager/server.py +121 -2
- package/src/stlc_agents/shared/cost_tracker.py +395 -0
- package/src/stlc_agents/shared/install_hook.py +154 -0
- package/src/stlc_agents/shared/pricing.py +72 -0
- package/src/stlc_agents/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/__pycache__/server.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/ado_gherkin.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/__pycache__/server.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/tools/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/tools/__pycache__/boilerplate.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/tools/__pycache__/helix_write.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_jira_manager/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_jira_manager/__pycache__/server.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_jira_manager/tools/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_jira_manager/tools/__pycache__/jira_workitem.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/__pycache__/server.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/ado_attach.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/__pycache__/server.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/ado_workitem.cpython-310.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/auth.cpython-310.pyc +0 -0
- package/src/stlc_agents/shared_jira/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/shared_jira/__pycache__/auth.cpython-310.pyc +0 -0
|
@@ -1,12 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* cmd-mcp-config.js — `qa-stlc mcp-config`
|
|
2
|
+
* cmd-mcp-config.js — `qa-stlc mcp-config` (updated with cost tracking env injection)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Change from original: both buildClaudeConfig() and buildVscodeConfig() now
|
|
5
|
+
* inject STLC cost tracking env vars into every MCP server entry.
|
|
6
|
+
*
|
|
7
|
+
* What gets injected and why:
|
|
8
|
+
*
|
|
9
|
+
* STLC_COST_TRACKING "true" — enables cost tracking (opt-out via "false")
|
|
10
|
+
*
|
|
11
|
+
* ANTHROPIC_MODEL "${env:ANTHROPIC_MODEL}" — Claude Code sets this
|
|
12
|
+
* automatically on the process that launches Claude.
|
|
13
|
+
* Passing it through to the MCP subprocess lets the
|
|
14
|
+
* cost tracker auto-detect the model without any
|
|
15
|
+
* manual configuration.
|
|
16
|
+
*
|
|
17
|
+
* GITHUB_COPILOT_MODEL "${env:GITHUB_COPILOT_MODEL}" — VS Code / Copilot
|
|
18
|
+
* may set this; passed through for the same reason.
|
|
19
|
+
*
|
|
20
|
+
* STLC_CODING_AGENT_MODEL "${env:STLC_CODING_AGENT_MODEL}" — explicit user
|
|
21
|
+
* override. Empty by default; set in shell profile
|
|
22
|
+
* or .env to hard-pin the model regardless of agent.
|
|
23
|
+
*
|
|
24
|
+
* The detection order inside cost_tracker.py is:
|
|
25
|
+
* STLC_CODING_AGENT_MODEL → ANTHROPIC_MODEL → CLAUDE_MODEL →
|
|
26
|
+
* GITHUB_COPILOT_MODEL → ~/.qa-stlc/agent-model → "claude-sonnet-4-6"
|
|
27
|
+
*
|
|
28
|
+
* RESULT: zero configuration for Claude Code users — the model is detected
|
|
29
|
+
* automatically. Copilot / Cursor / Windsurf users set STLC_CODING_AGENT_MODEL
|
|
30
|
+
* once in their shell profile or run `qa-stlc cost --set-model <model>`.
|
|
5
31
|
*/
|
|
32
|
+
|
|
6
33
|
"use strict";
|
|
7
34
|
|
|
8
|
-
const path
|
|
9
|
-
const fs
|
|
35
|
+
const path = require("path");
|
|
36
|
+
const fs = require("fs");
|
|
37
|
+
const os = require("os");
|
|
10
38
|
const { spawnSync } = require("child_process");
|
|
11
39
|
|
|
12
40
|
const C = {
|
|
@@ -15,45 +43,54 @@ const C = {
|
|
|
15
43
|
yellow: "\x1b[33m", red: "\x1b[31m", dim: "\x1b[2m",
|
|
16
44
|
};
|
|
17
45
|
const ok = (m) => console.log(`${C.green}✓${C.reset} ${m}`);
|
|
18
|
-
const info = (m) => console.log(`${C.cyan}→${C.reset} ${m}`);
|
|
19
46
|
const warn = (m) => console.log(`${C.yellow}⚠${C.reset} ${m}`);
|
|
20
47
|
|
|
21
|
-
const
|
|
22
|
-
const fs_ = fs; // alias — fs already required above
|
|
23
|
-
|
|
24
|
-
const PREF_FILE_MCP = require("path").join(os.homedir(), ".qa-stlc", "integration");
|
|
48
|
+
const PREF_FILE_MCP = path.join(os.homedir(), ".qa-stlc", "integration");
|
|
25
49
|
|
|
26
50
|
function readIntegrationPref() {
|
|
27
51
|
try {
|
|
28
|
-
if (
|
|
29
|
-
return
|
|
52
|
+
if (fs.existsSync(PREF_FILE_MCP))
|
|
53
|
+
return fs.readFileSync(PREF_FILE_MCP, "utf8").trim();
|
|
30
54
|
} catch (_) {}
|
|
31
55
|
return "ado";
|
|
32
56
|
}
|
|
33
57
|
|
|
34
|
-
const CWD
|
|
58
|
+
const CWD = process.cwd();
|
|
35
59
|
const IS_WIN = process.platform === "win32";
|
|
36
|
-
const EXT
|
|
60
|
+
const EXT = IS_WIN ? ".exe" : "";
|
|
37
61
|
|
|
38
|
-
const AGENT_NAMES
|
|
62
|
+
const AGENT_NAMES = [
|
|
39
63
|
"qa-test-case-manager",
|
|
40
64
|
"qa-gherkin-generator",
|
|
41
65
|
"qa-playwright-generator",
|
|
42
66
|
"qa-helix-writer",
|
|
43
67
|
];
|
|
44
|
-
|
|
45
|
-
// Jira agent — added separately because it needs env var injection
|
|
46
68
|
const JIRA_AGENT_NAME = "qa-jira-manager";
|
|
69
|
+
|
|
70
|
+
// ── Cost tracking env vars injected into every server entry ───────────────
|
|
71
|
+
//
|
|
72
|
+
// These are passed as env block items in both .mcp.json and .vscode/mcp.json.
|
|
73
|
+
// The "${env:X}" syntax is evaluated by Claude Code / VS Code at server start,
|
|
74
|
+
// substituting the actual value from the coding agent's own environment.
|
|
75
|
+
//
|
|
76
|
+
// ANTHROPIC_MODEL is the key one: Claude Code sets it automatically, so
|
|
77
|
+
// Claude Code users get correct pricing with zero manual configuration.
|
|
78
|
+
const COST_ENV = {
|
|
79
|
+
// Carry the coding agent's model ID into the MCP subprocess
|
|
80
|
+
"ANTHROPIC_MODEL": "${env:ANTHROPIC_MODEL}",
|
|
81
|
+
"GITHUB_COPILOT_MODEL": "${env:GITHUB_COPILOT_MODEL}",
|
|
82
|
+
// Explicit override — user sets this in shell profile if auto-detect is wrong
|
|
83
|
+
"STLC_CODING_AGENT_MODEL": "${env:STLC_CODING_AGENT_MODEL}",
|
|
84
|
+
// Opt-out flag — user sets STLC_COST_TRACKING=false in their shell to silence output
|
|
85
|
+
"STLC_COST_TRACKING": "${env:STLC_COST_TRACKING}",
|
|
86
|
+
};
|
|
87
|
+
|
|
47
88
|
const JIRA_ENV_VARS = {
|
|
48
89
|
JIRA_CLIENT_ID: "${env:JIRA_CLIENT_ID}",
|
|
49
90
|
JIRA_CLIENT_SECRET: "${env:JIRA_CLIENT_SECRET}",
|
|
50
91
|
JIRA_CLOUD_ID: "${env:JIRA_CLOUD_ID}",
|
|
51
92
|
};
|
|
52
93
|
|
|
53
|
-
/**
|
|
54
|
-
* Locate the binary for an agent.
|
|
55
|
-
* Priority: .venv → --python dir → system PATH → python sys.executable dir → macOS framework dirs
|
|
56
|
-
*/
|
|
57
94
|
function findBinary(name, pythonBin) {
|
|
58
95
|
// 1. .venv in cwd
|
|
59
96
|
const venvBin = IS_WIN
|
|
@@ -67,116 +104,137 @@ function findBinary(name, pythonBin) {
|
|
|
67
104
|
if (fs.existsSync(candidate)) return candidate;
|
|
68
105
|
}
|
|
69
106
|
|
|
70
|
-
// 3. system PATH
|
|
107
|
+
// 3. system PATH
|
|
71
108
|
const which = spawnSync(IS_WIN ? "where" : "which", [name], { encoding: "utf8" });
|
|
72
|
-
if (which.status === 0 && which.stdout.trim())
|
|
109
|
+
if (which.status === 0 && which.stdout.trim())
|
|
110
|
+
return which.stdout.trim().split("\n")[0].trim();
|
|
73
111
|
|
|
74
|
-
// 4.
|
|
112
|
+
// 4. Python's own bin dir (framework installs not on PATH)
|
|
75
113
|
for (const py of ["python3", "python"]) {
|
|
76
|
-
const r = spawnSync(py, ["-c", "import sys,
|
|
114
|
+
const r = spawnSync(py, ["-c", "import sys,os;print(os.path.dirname(sys.executable))"], { encoding: "utf8" });
|
|
77
115
|
if (r.status === 0 && r.stdout.trim()) {
|
|
78
116
|
const candidate = path.join(r.stdout.trim(), name + EXT);
|
|
79
117
|
if (fs.existsSync(candidate)) return candidate;
|
|
80
118
|
}
|
|
81
119
|
}
|
|
82
120
|
|
|
83
|
-
// 5. macOS
|
|
121
|
+
// 5. macOS framework fallback
|
|
84
122
|
if (!IS_WIN) {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
"/Library/Frameworks/Python.framework/Versions/3.12/bin",
|
|
88
|
-
"/Library/Frameworks/Python.framework/Versions/3.11/bin",
|
|
89
|
-
"/Library/Frameworks/Python.framework/Versions/3.10/bin",
|
|
90
|
-
];
|
|
91
|
-
for (const dir of frameworkDirs) {
|
|
92
|
-
const candidate = path.join(dir, name);
|
|
123
|
+
for (const v of ["3.13", "3.12", "3.11", "3.10"]) {
|
|
124
|
+
const candidate = `/Library/Frameworks/Python.framework/Versions/${v}/bin/${name}`;
|
|
93
125
|
if (fs.existsSync(candidate)) return candidate;
|
|
94
126
|
}
|
|
95
127
|
}
|
|
96
|
-
|
|
97
128
|
return null;
|
|
98
129
|
}
|
|
99
130
|
|
|
131
|
+
// ── Claude Code config (.mcp.json) ────────────────────────────────────────
|
|
132
|
+
|
|
100
133
|
function buildClaudeConfig(pythonBin, playwrightPort, integration) {
|
|
101
134
|
const servers = {};
|
|
102
135
|
const missing = [];
|
|
103
136
|
const needsAdo = !integration || integration === "ado" || integration === "both";
|
|
104
137
|
const needsJira = integration === "jira" || integration === "both";
|
|
105
|
-
|
|
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
138
|
const activeNames = (needsAdo || needsJira) ? AGENT_NAMES : [];
|
|
110
139
|
|
|
111
140
|
for (const name of activeNames) {
|
|
112
141
|
const bin = findBinary(name, pythonBin);
|
|
113
142
|
if (bin) {
|
|
114
|
-
|
|
143
|
+
// Every server gets the cost tracking env block
|
|
144
|
+
servers[name] = { command: bin, env: { ...COST_ENV } };
|
|
115
145
|
} else {
|
|
116
146
|
missing.push(name);
|
|
117
|
-
servers[name] = {
|
|
147
|
+
servers[name] = {
|
|
148
|
+
command: `/path/to/.venv/bin/${name}`,
|
|
149
|
+
env: { ...COST_ENV },
|
|
150
|
+
"_comment": "NOT FOUND — run: pip install qa-gentic-stlc-agents",
|
|
151
|
+
};
|
|
118
152
|
}
|
|
119
153
|
}
|
|
120
154
|
|
|
121
|
-
// Jira agent — only when integration includes jira
|
|
122
155
|
if (needsJira) {
|
|
123
156
|
const jiraBin = findBinary(JIRA_AGENT_NAME, pythonBin);
|
|
124
157
|
if (jiraBin) {
|
|
125
|
-
servers[JIRA_AGENT_NAME] = {
|
|
158
|
+
servers[JIRA_AGENT_NAME] = {
|
|
159
|
+
command: jiraBin,
|
|
160
|
+
env: { ...COST_ENV, ...JIRA_ENV_VARS },
|
|
161
|
+
};
|
|
126
162
|
} else {
|
|
127
163
|
missing.push(JIRA_AGENT_NAME);
|
|
128
164
|
servers[JIRA_AGENT_NAME] = {
|
|
129
165
|
command: `/path/to/.venv/bin/${JIRA_AGENT_NAME}`,
|
|
130
|
-
env: JIRA_ENV_VARS,
|
|
166
|
+
env: { ...COST_ENV, ...JIRA_ENV_VARS },
|
|
131
167
|
"_comment": "NOT FOUND — run: pip install qa-gentic-stlc-agents",
|
|
132
168
|
};
|
|
133
169
|
}
|
|
134
170
|
}
|
|
135
171
|
|
|
136
172
|
servers["playwright"] = {
|
|
137
|
-
|
|
138
|
-
|
|
173
|
+
command: "npx",
|
|
174
|
+
args: ["@playwright/mcp@latest", "--isolated"],
|
|
139
175
|
};
|
|
140
176
|
|
|
141
177
|
return { config: { mcpServers: servers }, missing };
|
|
142
178
|
}
|
|
143
179
|
|
|
180
|
+
// ── VS Code config (.vscode/mcp.json) ─────────────────────────────────────
|
|
181
|
+
|
|
144
182
|
function buildVscodeConfig(pythonBin, playwrightPort, integration) {
|
|
145
183
|
const servers = {};
|
|
146
184
|
const missing = [];
|
|
147
185
|
const needsAdo = !integration || integration === "ado" || integration === "both";
|
|
148
186
|
const needsJira = integration === "jira" || integration === "both";
|
|
149
|
-
// Shared agents (Gherkin, Playwright, Helix) are needed by both ADO and Jira pipelines.
|
|
150
187
|
const activeNames = (needsAdo || needsJira) ? AGENT_NAMES : [];
|
|
151
188
|
|
|
152
189
|
for (const name of activeNames) {
|
|
153
190
|
const bin = findBinary(name, pythonBin);
|
|
154
191
|
if (bin) {
|
|
155
|
-
servers[name] = {
|
|
192
|
+
servers[name] = {
|
|
193
|
+
type: "stdio",
|
|
194
|
+
command: bin,
|
|
195
|
+
args: [],
|
|
196
|
+
env: { ...COST_ENV },
|
|
197
|
+
};
|
|
156
198
|
} else {
|
|
157
199
|
missing.push(name);
|
|
158
|
-
servers[name] = {
|
|
200
|
+
servers[name] = {
|
|
201
|
+
type: "stdio",
|
|
202
|
+
command: `/path/to/.venv/bin/${name}`,
|
|
203
|
+
args: [],
|
|
204
|
+
env: { ...COST_ENV },
|
|
205
|
+
"_comment": "NOT FOUND — run: pip install qa-gentic-stlc-agents",
|
|
206
|
+
};
|
|
159
207
|
}
|
|
160
208
|
}
|
|
161
209
|
|
|
162
|
-
// Jira agent — only when integration includes jira
|
|
163
210
|
if (needsJira) {
|
|
164
|
-
const
|
|
165
|
-
if (
|
|
166
|
-
servers[JIRA_AGENT_NAME] = {
|
|
211
|
+
const jiraBin = findBinary(JIRA_AGENT_NAME, pythonBin);
|
|
212
|
+
if (jiraBin) {
|
|
213
|
+
servers[JIRA_AGENT_NAME] = {
|
|
214
|
+
type: "stdio",
|
|
215
|
+
command: jiraBin,
|
|
216
|
+
args: [],
|
|
217
|
+
env: {
|
|
218
|
+
...COST_ENV,
|
|
219
|
+
...JIRA_ENV_VARS,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
167
222
|
} else {
|
|
168
223
|
missing.push(JIRA_AGENT_NAME);
|
|
169
224
|
servers[JIRA_AGENT_NAME] = {
|
|
225
|
+
type: "stdio",
|
|
170
226
|
command: `/path/to/.venv/bin/${JIRA_AGENT_NAME}`,
|
|
171
|
-
|
|
227
|
+
args: [],
|
|
228
|
+
env: { ...COST_ENV, ...JIRA_ENV_VARS },
|
|
172
229
|
"_comment": "NOT FOUND — run: pip install qa-gentic-stlc-agents",
|
|
173
230
|
};
|
|
174
231
|
}
|
|
175
232
|
}
|
|
176
233
|
|
|
177
234
|
servers["playwright"] = {
|
|
178
|
-
type: "
|
|
179
|
-
|
|
235
|
+
type: "stdio",
|
|
236
|
+
command: "npx",
|
|
237
|
+
args: ["@playwright/mcp@latest", "--isolated"],
|
|
180
238
|
};
|
|
181
239
|
|
|
182
240
|
return { config: { servers }, missing };
|
|
@@ -185,14 +243,17 @@ function buildVscodeConfig(pythonBin, playwrightPort, integration) {
|
|
|
185
243
|
function printNextSteps(mode, playwrightPort) {
|
|
186
244
|
const isVscode = mode === "vscode";
|
|
187
245
|
console.log(`
|
|
188
|
-
${C.dim}
|
|
189
|
-
|
|
190
|
-
${C.dim}headless (CI): npx @playwright/mcp@latest --headless --port ${playwrightPort}${C.reset}
|
|
246
|
+
${C.dim}Playwright MCP is auto-started by the MCP framework (--isolated, no manual start needed).
|
|
247
|
+
For CI/headless: set PLAYWRIGHT_MCP_URL or start manually with --headless --isolated --port ${playwrightPort}${C.reset}
|
|
191
248
|
|
|
192
249
|
${isVscode
|
|
193
|
-
? `Reload VS Code window — all
|
|
194
|
-
: `In Claude Code, run /mcp to verify all
|
|
250
|
+
? `Reload VS Code window — all MCP servers will appear in the MCP panel.`
|
|
251
|
+
: `In Claude Code, run /mcp to verify all servers are loaded.`
|
|
195
252
|
}
|
|
253
|
+
|
|
254
|
+
${C.dim}Cost tracking is active on all servers.
|
|
255
|
+
Each tool call logs tokens + cost to ~/.qa-stlc/cost-<session>.jsonl
|
|
256
|
+
View reports: qa-stlc cost${C.reset}
|
|
196
257
|
`);
|
|
197
258
|
}
|
|
198
259
|
|
|
@@ -220,7 +281,7 @@ module.exports = async function mcpConfig(opts) {
|
|
|
220
281
|
fs.mkdirSync(dir, { recursive: true });
|
|
221
282
|
const out = path.join(dir, "mcp.json");
|
|
222
283
|
fs.writeFileSync(out, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
223
|
-
ok(`Written → .vscode/mcp.json`);
|
|
284
|
+
ok(`Written → .vscode/mcp.json (cost tracking env vars included)`);
|
|
224
285
|
if (missing.length) {
|
|
225
286
|
warn(`${missing.length} agent(s) not found — run: pip install qa-gentic-stlc-agents`);
|
|
226
287
|
missing.forEach((m) => warn(` missing: ${m}`));
|
|
@@ -230,7 +291,7 @@ module.exports = async function mcpConfig(opts) {
|
|
|
230
291
|
const { config, missing } = buildClaudeConfig(pythonBin, playwrightPort, integration);
|
|
231
292
|
const out = path.join(CWD, ".mcp.json");
|
|
232
293
|
fs.writeFileSync(out, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
233
|
-
ok(`Written → .mcp.json`);
|
|
294
|
+
ok(`Written → .mcp.json (cost tracking env vars included)`);
|
|
234
295
|
if (missing.length) {
|
|
235
296
|
warn(`${missing.length} agent(s) not found — run: pip install qa-gentic-stlc-agents`);
|
|
236
297
|
missing.forEach((m) => warn(` missing: ${m}`));
|
package/src/cli/cmd-skills.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* windsurf → .windsurf/rules/
|
|
10
10
|
* both → claude + vscode
|
|
11
11
|
* print → stdout only
|
|
12
|
+
*
|
|
13
|
+
* Also installs ORCHESTRATION_RULES.md to project root (multi-step workflow reference).
|
|
12
14
|
*/
|
|
13
15
|
"use strict";
|
|
14
16
|
|
|
@@ -35,10 +37,11 @@ function readIntegrationPrefSk() {
|
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
// Resolve the skills bundled with this npm package
|
|
38
|
-
const PKG_ROOT
|
|
39
|
-
const SKILLS_DIR
|
|
40
|
-
const BEHAVIOR_MD
|
|
41
|
-
const
|
|
40
|
+
const PKG_ROOT = path.resolve(__dirname, "../..");
|
|
41
|
+
const SKILLS_DIR = path.join(PKG_ROOT, "skills");
|
|
42
|
+
const BEHAVIOR_MD = path.join(SKILLS_DIR, "AGENT-BEHAVIOR.md");
|
|
43
|
+
const ORCHESTRATION_MD = path.join(PKG_ROOT, "ORCHESTRATION_RULES.md");
|
|
44
|
+
const AGENTS_DIR = path.join(PKG_ROOT, ".github", "agents");
|
|
42
45
|
|
|
43
46
|
/** Copy a file, creating parent dirs as needed. */
|
|
44
47
|
function cp(src, dest) {
|
|
@@ -105,6 +108,15 @@ function installAgents(integration) {
|
|
|
105
108
|
agentFiles(integration).forEach((f) => info(path.basename(f)));
|
|
106
109
|
}
|
|
107
110
|
|
|
111
|
+
/** Install ORCHESTRATION_RULES.md to project root for multi-step workflow reference. */
|
|
112
|
+
function installOrchestrationRules() {
|
|
113
|
+
if (!fs.existsSync(ORCHESTRATION_MD)) return;
|
|
114
|
+
const dest = path.join(CWD, "ORCHESTRATION_RULES.md");
|
|
115
|
+
cp(ORCHESTRATION_MD, dest);
|
|
116
|
+
ok(`ORCHESTRATION_RULES.md installed → project root`);
|
|
117
|
+
info("Reference this file for multi-step QA workflow best practices");
|
|
118
|
+
}
|
|
119
|
+
|
|
108
120
|
function installClaude(integration) {
|
|
109
121
|
const dest = path.join(CWD, ".claude", "skills");
|
|
110
122
|
// Copy entire skill directory (preserves references/ subdirectory)
|
|
@@ -118,6 +130,7 @@ function installClaude(integration) {
|
|
|
118
130
|
info("AGENT-BEHAVIOR.md → .claude/AGENT-BEHAVIOR.md");
|
|
119
131
|
skillEntries(integration).forEach((e) => info(`${e.name}/SKILL.md`));
|
|
120
132
|
installAgents(integration);
|
|
133
|
+
installOrchestrationRules();
|
|
121
134
|
printPlaywrightHint();
|
|
122
135
|
}
|
|
123
136
|
|
|
@@ -134,6 +147,7 @@ function installVscode(integration) {
|
|
|
134
147
|
info("AGENT-BEHAVIOR.md → .github/copilot-instructions/AGENT-BEHAVIOR.md");
|
|
135
148
|
skillEntries(integration).forEach((e) => info(`${e.name}/SKILL.md`));
|
|
136
149
|
installAgents(integration);
|
|
150
|
+
installOrchestrationRules();
|
|
137
151
|
printPlaywrightHint();
|
|
138
152
|
}
|
|
139
153
|
|
|
@@ -146,6 +160,7 @@ function installCursor(integration) {
|
|
|
146
160
|
cp(BEHAVIOR_MD, path.join(dest, "AGENT-BEHAVIOR.md"));
|
|
147
161
|
ok(`Skills installed → .cursor/rules/`);
|
|
148
162
|
installAgents(integration);
|
|
163
|
+
installOrchestrationRules();
|
|
149
164
|
printPlaywrightHint();
|
|
150
165
|
}
|
|
151
166
|
|
|
@@ -158,6 +173,7 @@ function installWindsurf(integration) {
|
|
|
158
173
|
cp(BEHAVIOR_MD, path.join(dest, "AGENT-BEHAVIOR.md"));
|
|
159
174
|
ok(`Skills installed → .windsurf/rules/`);
|
|
160
175
|
installAgents(integration);
|
|
176
|
+
installOrchestrationRules();
|
|
161
177
|
printPlaywrightHint();
|
|
162
178
|
}
|
|
163
179
|
|
|
@@ -165,6 +181,7 @@ function printSkills() {
|
|
|
165
181
|
console.log("\nAvailable skills:\n");
|
|
166
182
|
skillEntries().forEach((e) => console.log(` ${e.name}/SKILL.md`));
|
|
167
183
|
console.log(` AGENT-BEHAVIOR.md`);
|
|
184
|
+
console.log(` ORCHESTRATION_RULES.md (workflow reference)`);
|
|
168
185
|
}
|
|
169
186
|
|
|
170
187
|
/** Recursively copy a directory tree. */
|
|
@@ -323,6 +323,55 @@ async def list_tools() -> list[types.Tool]:
|
|
|
323
323
|
"required": ["gherkin_content"],
|
|
324
324
|
},
|
|
325
325
|
),
|
|
326
|
+
|
|
327
|
+
# ── NEW: Headless composite — fetch + validate + attach with retry ───
|
|
328
|
+
types.Tool(
|
|
329
|
+
name="generate_and_attach_gherkin",
|
|
330
|
+
description=(
|
|
331
|
+
"Headless composite tool: fetch work item data, validate Gherkin content, "
|
|
332
|
+
"and attach to ADO — retrying once internally if validation fails. "
|
|
333
|
+
"Use this from the pipeline or CI runner instead of calling "
|
|
334
|
+
"validate_gherkin_content + attach_gherkin_to_feature/work_item separately. "
|
|
335
|
+
"Accepts pre-generated gherkin_content from the LLM caller. "
|
|
336
|
+
"If validation fails on the first pass the errors are returned as "
|
|
337
|
+
"validation_failed (no retry is possible without a new LLM call — "
|
|
338
|
+
"the pipeline must re-generate and call this tool again). "
|
|
339
|
+
"Returns {status, gherkin_content, attached} on success, "
|
|
340
|
+
"or {status: validation_failed, errors, gherkin_content} on failure."
|
|
341
|
+
),
|
|
342
|
+
inputSchema={
|
|
343
|
+
"type": "object",
|
|
344
|
+
"properties": {
|
|
345
|
+
"work_item_id": {
|
|
346
|
+
"type": "integer",
|
|
347
|
+
"description": "ADO Feature, PBI, or Bug ID",
|
|
348
|
+
},
|
|
349
|
+
"work_item_title": {
|
|
350
|
+
"type": "string",
|
|
351
|
+
"description": "Title of the work item (used in filename)",
|
|
352
|
+
},
|
|
353
|
+
"organization_url": {"type": "string"},
|
|
354
|
+
"project_name": {"type": "string"},
|
|
355
|
+
"gherkin_content": {
|
|
356
|
+
"type": "string",
|
|
357
|
+
"description": "Pre-generated .feature file content from LLM caller",
|
|
358
|
+
},
|
|
359
|
+
"scope": {
|
|
360
|
+
"type": "string",
|
|
361
|
+
"enum": ["feature", "work_item"],
|
|
362
|
+
"description": (
|
|
363
|
+
"'feature' attaches to a Feature work item (5–10 scenarios). "
|
|
364
|
+
"'work_item' attaches to a PBI or Bug (3–9 scenarios). "
|
|
365
|
+
"Default: 'work_item'."
|
|
366
|
+
),
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
"required": [
|
|
370
|
+
"work_item_id", "work_item_title", "organization_url",
|
|
371
|
+
"project_name", "gherkin_content",
|
|
372
|
+
],
|
|
373
|
+
},
|
|
374
|
+
),
|
|
326
375
|
]
|
|
327
376
|
|
|
328
377
|
|
|
@@ -416,6 +465,44 @@ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
|
|
|
416
465
|
arguments.get("scope", "feature"),
|
|
417
466
|
)
|
|
418
467
|
|
|
468
|
+
elif name == "generate_and_attach_gherkin":
|
|
469
|
+
org = arguments["organization_url"]
|
|
470
|
+
project = arguments["project_name"]
|
|
471
|
+
wi_id = arguments["work_item_id"]
|
|
472
|
+
wi_title = arguments["work_item_title"]
|
|
473
|
+
gherkin = arguments["gherkin_content"]
|
|
474
|
+
scope = arguments.get("scope", "work_item")
|
|
475
|
+
|
|
476
|
+
# Step 1: validate
|
|
477
|
+
validation = await asyncio.to_thread(_validate_gherkin, gherkin, scope)
|
|
478
|
+
if not validation["valid"]:
|
|
479
|
+
result = {
|
|
480
|
+
"status": "validation_failed",
|
|
481
|
+
"errors": validation.get("errors", []),
|
|
482
|
+
"gherkin_content": gherkin,
|
|
483
|
+
"message": (
|
|
484
|
+
"Gherkin content failed structural validation. "
|
|
485
|
+
"The pipeline must re-generate and call this tool again "
|
|
486
|
+
"with corrected content. No file was attached to ADO."
|
|
487
|
+
),
|
|
488
|
+
}
|
|
489
|
+
else:
|
|
490
|
+
# Step 2: attach
|
|
491
|
+
if scope == "feature":
|
|
492
|
+
attach = await asyncio.to_thread(
|
|
493
|
+
_attach_feature, org, project, wi_id, wi_title, gherkin
|
|
494
|
+
)
|
|
495
|
+
else:
|
|
496
|
+
attach = await asyncio.to_thread(
|
|
497
|
+
_attach_wi_file, org, project, wi_id, wi_title, gherkin
|
|
498
|
+
)
|
|
499
|
+
attach["_validation"] = _validate_attach_response(attach, scope)
|
|
500
|
+
result = {
|
|
501
|
+
"status": "ok",
|
|
502
|
+
"gherkin_content": gherkin,
|
|
503
|
+
"attached": attach,
|
|
504
|
+
}
|
|
505
|
+
|
|
419
506
|
else:
|
|
420
507
|
result = {"error": f"Unknown tool: {name}"}
|
|
421
508
|
|
|
@@ -448,7 +535,4 @@ def main():
|
|
|
448
535
|
|
|
449
536
|
|
|
450
537
|
if __name__ == "__main__":
|
|
451
|
-
main()
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
538
|
+
main()
|
|
@@ -40,6 +40,10 @@ Within-file deduplication
|
|
|
40
40
|
src/test/features/ are scanned for existing scenario titles.
|
|
41
41
|
A title already present in any sibling file is treated as a
|
|
42
42
|
duplicate and dropped from the incoming content.
|
|
43
|
+
For cucumber.config.ts : new profiles are appended ONLY if the file already exists.
|
|
44
|
+
If the file does not exist, the profile is skipped entirely.
|
|
45
|
+
This prevents orphaned config files from being created when
|
|
46
|
+
no Cucumber config yet exists on disk.
|
|
43
47
|
|
|
44
48
|
Interface adapter
|
|
45
49
|
The generator emits repo.updateHealed / repo.incrementSuccess / repo.getBBox etc.
|
|
@@ -140,11 +144,26 @@ def _adapt_to_helix_interface(content: str) -> str:
|
|
|
140
144
|
'import { EnvironmentManager } from "@helper/environment/environmentManager.util";',
|
|
141
145
|
'import { environment } from "@config/environment";',
|
|
142
146
|
)
|
|
147
|
+
# If content uses environment.getConfig() but lacks the import, add it after the last import line
|
|
148
|
+
if "environment.getConfig()" in content and 'from "@config/environment"' not in content:
|
|
149
|
+
import_insert = 'import { environment } from "@config/environment";'
|
|
150
|
+
# Insert after the last import statement
|
|
151
|
+
last_import = content.rfind("\nimport ")
|
|
152
|
+
if last_import != -1:
|
|
153
|
+
end_of_line = content.find("\n", last_import + 1)
|
|
154
|
+
if end_of_line != -1:
|
|
155
|
+
content = content[:end_of_line + 1] + import_insert + "\n" + content[end_of_line + 1:]
|
|
143
156
|
content = re.sub(r"\s*this\.env\s*=\s*new EnvironmentManager\(\);?\s*\n", "\n", content)
|
|
144
157
|
content = content.replace("new EnvironmentManager()", "environment")
|
|
145
158
|
content = content.replace("this.env.getBaseUrl()", "environment.getConfig().baseUrl")
|
|
146
159
|
content = re.sub(r"this\.env\.getPath\(['\"]([^'\"]+)['\"]\)", r'"\1"', content)
|
|
147
160
|
|
|
161
|
+
# Fix super() — cannot access this before super() in TS
|
|
162
|
+
content = content.replace(
|
|
163
|
+
"super(page ?? this.page);",
|
|
164
|
+
"super(page ?? fixture().page);",
|
|
165
|
+
)
|
|
166
|
+
|
|
148
167
|
return content
|
|
149
168
|
|
|
150
169
|
|
|
@@ -162,11 +181,22 @@ def _resolve_destination(helix_root: Path, file_key: str) -> Path:
|
|
|
162
181
|
if _STEPS_RE.search(key):
|
|
163
182
|
return helix_root / "src" / "test" / "steps" / Path(key).name
|
|
164
183
|
if _LOCATOR_RE.search(key):
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
184
|
+
# Extract the page/feature name from the path
|
|
185
|
+
# Input: src/pages/login-page/locators.ts or src/locators/login-page.locators.ts
|
|
186
|
+
# Output stem: login-page
|
|
187
|
+
path_obj = Path(key)
|
|
188
|
+
|
|
189
|
+
# First try: if filename is exactly 'locators.ts', use parent directory name
|
|
190
|
+
if path_obj.name == "locators.ts" and path_obj.parent.name:
|
|
191
|
+
stem = path_obj.parent.name
|
|
192
|
+
else:
|
|
193
|
+
# Otherwise extract stem from filename
|
|
194
|
+
fname = path_obj.name
|
|
195
|
+
stem = re.sub(r"\.locators\.ts$", "", fname)
|
|
196
|
+
stem = re.sub(r"\.ts$", "", stem)
|
|
197
|
+
if not stem or stem in ("src", "locators", "pages", "utils"):
|
|
198
|
+
stem = "page"
|
|
199
|
+
|
|
170
200
|
return helix_root / "src" / "locators" / f"{stem}.locators.ts"
|
|
171
201
|
if _PAGE_RE.search(key):
|
|
172
202
|
return helix_root / "src" / "pages" / Path(key).name
|
|
@@ -242,7 +272,7 @@ def _collect_all_scenario_titles(features_dir: Path, exclude_file: Path | None =
|
|
|
242
272
|
return titles
|
|
243
273
|
|
|
244
274
|
|
|
245
|
-
|
|
275
|
+
def _collect_all_step_patterns(steps_dir: Path, exclude_file: Path | None = None) -> set[str]:
|
|
246
276
|
"""Return every /^pattern$/ defined in all *.steps.ts files in steps_dir,
|
|
247
277
|
optionally excluding one file (the one currently being written)."""
|
|
248
278
|
patterns: set[str] = set()
|
|
@@ -627,29 +657,31 @@ def write_files_to_helix(
|
|
|
627
657
|
skipped.append({"file_key": file_key, "dest": dest_rel, "reason": str(exc)})
|
|
628
658
|
continue
|
|
629
659
|
|
|
630
|
-
# ── Cucumber config: append profile,
|
|
660
|
+
# ── Cucumber config: append profile only if file exists, never create ────────────
|
|
631
661
|
if _CUCUMBER_RE.search(file_key):
|
|
662
|
+
if not dest.exists():
|
|
663
|
+
# RULE: if cucumber config does not exist, skip creation
|
|
664
|
+
skipped.append({
|
|
665
|
+
"file_key": file_key, "dest": dest_rel,
|
|
666
|
+
"reason": "cucumber.config.ts does not exist; only append to existing configs",
|
|
667
|
+
})
|
|
668
|
+
continue
|
|
632
669
|
try:
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
"bytes": len(content.encode()), "action": "appended"})
|
|
649
|
-
else:
|
|
650
|
-
dest.write_text(content, encoding="utf-8")
|
|
651
|
-
written.append({"file_key": file_key, "dest": dest_rel,
|
|
652
|
-
"bytes": len(content.encode()), "action": "created"})
|
|
670
|
+
existing_text = dest.read_text(encoding="utf-8")
|
|
671
|
+
profile_match = re.match(r"\s*(\w+)\s*:", content.strip())
|
|
672
|
+
profile_name = profile_match.group(1) if profile_match else None
|
|
673
|
+
if profile_name and profile_name in existing_text:
|
|
674
|
+
skipped.append({
|
|
675
|
+
"file_key": file_key, "dest": dest_rel,
|
|
676
|
+
"reason": f"profile '{profile_name}' already exists in cucumber.config.ts",
|
|
677
|
+
})
|
|
678
|
+
continue
|
|
679
|
+
dest.write_text(
|
|
680
|
+
existing_text.rstrip() + "\n\n// --- generated profile ---\n" + content,
|
|
681
|
+
encoding="utf-8",
|
|
682
|
+
)
|
|
683
|
+
written.append({"file_key": file_key, "dest": dest_rel,
|
|
684
|
+
"bytes": len(content.encode()), "action": "appended"})
|
|
653
685
|
except OSError as exc:
|
|
654
686
|
skipped.append({"file_key": file_key, "dest": dest_rel, "reason": str(exc)})
|
|
655
687
|
continue
|
|
@@ -867,4 +899,4 @@ def list_helix_tree(helix_root: str) -> dict[str, Any]:
|
|
|
867
899
|
else:
|
|
868
900
|
tree["other"].append(rel)
|
|
869
901
|
|
|
870
|
-
return {"success": True, "helix_root": str(root), "tree": tree}
|
|
902
|
+
return {"success": True, "helix_root": str(root), "tree": tree}
|