igel-qe-core 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/dist/cli/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { Command } from "commander";
3
3
  import chalk from "chalk";
4
4
  import { spawn, spawnSync, execSync } from "child_process";
5
- import { existsSync, mkdirSync, writeFileSync, readdirSync, statSync } from "fs";
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from "fs";
6
6
  import { join, dirname } from "path";
7
7
  import { fileURLToPath } from "url";
8
8
  import { setupPlatform } from "./platform.js";
@@ -263,24 +263,45 @@ function enrichWithCopilotCli(workspaceRoot, contextFiles) {
263
263
  const total = filesToProcess.length;
264
264
  for (let i = 0; i < filesToProcess.length; i++) {
265
265
  const absPath = filesToProcess[i];
266
- const relPath = absPath.replace(`${workspaceRoot}/`, "").replace(`${workspaceRoot}\\`, "");
266
+ // Normalise to forward slashes so the prompt is readable on all platforms.
267
+ const relPath = absPath
268
+ .replace(`${workspaceRoot}\\`, "")
269
+ .replace(`${workspaceRoot}/`, "")
270
+ .replace(/\\/g, "/");
267
271
  // Show per-file progress so the user knows work is happening.
268
272
  process.stdout.write(` [${i + 1}/${total}] Enriching ${relPath}…`);
273
+ // Read the current (template) content so Copilot can see the structure.
274
+ let currentContent = "";
275
+ try {
276
+ currentContent = readFileSync(absPath, "utf-8");
277
+ }
278
+ catch {
279
+ // If we can't read, skip this file.
280
+ process.stdout.write(" ✗ (unreadable)\n");
281
+ failed += 1;
282
+ continue;
283
+ }
284
+ // Build a prompt that asks Copilot to RETURN the improved markdown as plain
285
+ // text to stdout. We write the file ourselves so we know it was updated.
269
286
  const prompt = [
270
- `Update the markdown file at path '${relPath}' in place.`,
271
- "Goal: make it both human-understandable and LLM-ready for automation framework reasoning.",
272
- "Requirements:",
273
- "- Keep heading style consistent.",
274
- "- Include: purpose, architecture role, key assets, dependencies, conventions, risks, and update guidance.",
275
- "- Be concrete and repository-aware.",
276
- "- Do not modify any file other than the target file.",
277
- "- Preserve valid markdown.",
287
+ `You are enriching a context.md file for an automation test framework.`,
288
+ `The file is located at: ${relPath} (relative to the workspace root).`,
289
+ ``,
290
+ `Current content:`,
291
+ `\`\`\`markdown`,
292
+ currentContent,
293
+ `\`\`\``,
294
+ ``,
295
+ `Rewrite the content above to be concrete, human-understandable, and LLM-ready.`,
296
+ `Requirements:`,
297
+ `- Keep the same heading structure (# Context, ## Purpose, ## Key Assets, ## Conventions, ## Risks and Dependencies).`,
298
+ `- Replace placeholder text with specific details inferred from the folder path and framework context.`,
299
+ `- Be concise but informative. Each section: 2–5 bullet points or short sentences.`,
300
+ `- Output ONLY the improved markdown. Do NOT add commentary, code fences, or extra text around it.`,
278
301
  ].join("\n");
279
302
  const result = spawnSync("copilot", [
280
303
  "-C", workspaceRoot,
281
304
  "-p", prompt,
282
- "--allow-all-tools",
283
- "--allow-all-paths",
284
305
  "--no-color",
285
306
  "-s",
286
307
  ], {
@@ -288,10 +309,27 @@ function enrichWithCopilotCli(workspaceRoot, contextFiles) {
288
309
  shell: process.platform === "win32",
289
310
  timeout: 120_000, // 2 min per file — prevents indefinite hangs
290
311
  input: "", // close stdin immediately
312
+ maxBuffer: 4 * 1024 * 1024, // 4 MB stdout buffer
291
313
  });
292
314
  if (result.status === 0 && !result.error) {
293
- process.stdout.write(" ✓\n");
294
- updated += 1;
315
+ const enriched = (result.stdout || "").trim();
316
+ if (enriched.length > 50) {
317
+ // Only write back if Copilot returned meaningful content (>50 chars).
318
+ try {
319
+ writeFileSync(absPath, enriched, "utf-8");
320
+ process.stdout.write(" ✓\n");
321
+ updated += 1;
322
+ }
323
+ catch {
324
+ process.stdout.write(" ✗ (write failed)\n");
325
+ failed += 1;
326
+ }
327
+ }
328
+ else {
329
+ // Copilot ran but returned empty/trivial output.
330
+ process.stdout.write(" ✗ (empty response)\n");
331
+ failed += 1;
332
+ }
295
333
  }
296
334
  else {
297
335
  process.stdout.write(" ✗\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "igel-qe-core",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "IGEL QE Developer Experience Layer (CLI & MCP)",
5
5
  "type": "module",
6
6
  "publishConfig": {
package/src/cli/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  import { Command } from "commander";
4
4
  import chalk from "chalk";
5
5
  import { spawn, spawnSync, execSync } from "child_process";
6
- import { existsSync, mkdirSync, writeFileSync, readdirSync, statSync } from "fs";
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from "fs";
7
7
  import { join, dirname } from "path";
8
8
  import { fileURLToPath } from "url";
9
9
  import { setupPlatform } from "./platform.js";
@@ -286,20 +286,43 @@ function enrichWithCopilotCli(
286
286
 
287
287
  for (let i = 0; i < filesToProcess.length; i++) {
288
288
  const absPath = filesToProcess[i];
289
- const relPath = absPath.replace(`${workspaceRoot}/`, "").replace(`${workspaceRoot}\\`, "");
289
+ // Normalise to forward slashes so the prompt is readable on all platforms.
290
+ const relPath = absPath
291
+ .replace(`${workspaceRoot}\\`, "")
292
+ .replace(`${workspaceRoot}/`, "")
293
+ .replace(/\\/g, "/");
290
294
 
291
295
  // Show per-file progress so the user knows work is happening.
292
296
  process.stdout.write(` [${i + 1}/${total}] Enriching ${relPath}…`);
293
297
 
298
+ // Read the current (template) content so Copilot can see the structure.
299
+ let currentContent = "";
300
+ try {
301
+ currentContent = readFileSync(absPath, "utf-8");
302
+ } catch {
303
+ // If we can't read, skip this file.
304
+ process.stdout.write(" ✗ (unreadable)\n");
305
+ failed += 1;
306
+ continue;
307
+ }
308
+
309
+ // Build a prompt that asks Copilot to RETURN the improved markdown as plain
310
+ // text to stdout. We write the file ourselves so we know it was updated.
294
311
  const prompt = [
295
- `Update the markdown file at path '${relPath}' in place.`,
296
- "Goal: make it both human-understandable and LLM-ready for automation framework reasoning.",
297
- "Requirements:",
298
- "- Keep heading style consistent.",
299
- "- Include: purpose, architecture role, key assets, dependencies, conventions, risks, and update guidance.",
300
- "- Be concrete and repository-aware.",
301
- "- Do not modify any file other than the target file.",
302
- "- Preserve valid markdown.",
312
+ `You are enriching a context.md file for an automation test framework.`,
313
+ `The file is located at: ${relPath} (relative to the workspace root).`,
314
+ ``,
315
+ `Current content:`,
316
+ `\`\`\`markdown`,
317
+ currentContent,
318
+ `\`\`\``,
319
+ ``,
320
+ `Rewrite the content above to be concrete, human-understandable, and LLM-ready.`,
321
+ `Requirements:`,
322
+ `- Keep the same heading structure (# Context, ## Purpose, ## Key Assets, ## Conventions, ## Risks and Dependencies).`,
323
+ `- Replace placeholder text with specific details inferred from the folder path and framework context.`,
324
+ `- Be concise but informative. Each section: 2–5 bullet points or short sentences.`,
325
+ `- Output ONLY the improved markdown. Do NOT add commentary, code fences, or extra text around it.`,
303
326
  ].join("\n");
304
327
 
305
328
  const result = spawnSync(
@@ -307,8 +330,6 @@ function enrichWithCopilotCli(
307
330
  [
308
331
  "-C", workspaceRoot,
309
332
  "-p", prompt,
310
- "--allow-all-tools",
311
- "--allow-all-paths",
312
333
  "--no-color",
313
334
  "-s",
314
335
  ],
@@ -317,12 +338,27 @@ function enrichWithCopilotCli(
317
338
  shell: process.platform === "win32",
318
339
  timeout: 120_000, // 2 min per file — prevents indefinite hangs
319
340
  input: "", // close stdin immediately
341
+ maxBuffer: 4 * 1024 * 1024, // 4 MB stdout buffer
320
342
  },
321
343
  );
322
344
 
323
345
  if (result.status === 0 && !result.error) {
324
- process.stdout.write(" ✓\n");
325
- updated += 1;
346
+ const enriched = (result.stdout || "").trim();
347
+ if (enriched.length > 50) {
348
+ // Only write back if Copilot returned meaningful content (>50 chars).
349
+ try {
350
+ writeFileSync(absPath, enriched, "utf-8");
351
+ process.stdout.write(" ✓\n");
352
+ updated += 1;
353
+ } catch {
354
+ process.stdout.write(" ✗ (write failed)\n");
355
+ failed += 1;
356
+ }
357
+ } else {
358
+ // Copilot ran but returned empty/trivial output.
359
+ process.stdout.write(" ✗ (empty response)\n");
360
+ failed += 1;
361
+ }
326
362
  } else {
327
363
  process.stdout.write(" ✗\n");
328
364
  failed += 1;
@@ -27,18 +27,32 @@ _PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
27
27
  if str(_PROJECT_ROOT) not in sys.path:
28
28
  sys.path.insert(0, str(_PROJECT_ROOT))
29
29
 
30
- from src.db.igel_schema import ensure_igel_tables
31
- from src.connectors.jira_connector import JiraConnector
32
- from src.agents.workflow_engine import WorkflowEngine
33
- from src.intelligence.diff_analyzer import analyze_diff_scope
34
- from src.intelligence.routing_engine import RouteInputs, compute_weighted_score, classify_route
35
- from src.agents.framework_analyzer_agent import FrameworkAnalyzerAgent
36
- from src.agents.orchestrator.orchestrator import Orchestrator
37
- from src.agents.internal.approval_manager import ApprovalError
38
- from src.config import cfg
39
- from src.project.context_projection_builder import ContextProjectionBuilder
40
- from src.project.context_requirement_sync import sync_requirement_context
41
- from src.project.context_enricher import enrich_context_files
30
+ try:
31
+ from src.db.igel_schema import ensure_igel_tables
32
+ from src.connectors.jira_connector import JiraConnector
33
+ from src.agents.workflow_engine import WorkflowEngine
34
+ from src.intelligence.diff_analyzer import analyze_diff_scope
35
+ from src.intelligence.routing_engine import RouteInputs, compute_weighted_score, classify_route
36
+ from src.agents.framework_analyzer_agent import FrameworkAnalyzerAgent
37
+ from src.agents.orchestrator.orchestrator import Orchestrator
38
+ from src.agents.internal.approval_manager import ApprovalError
39
+ from src.config import cfg
40
+ from src.project.context_projection_builder import ContextProjectionBuilder
41
+ from src.project.context_requirement_sync import sync_requirement_context
42
+ from src.project.context_enricher import enrich_context_files
43
+ _DEPS_AVAILABLE = True
44
+ _DEPS_ERROR: str | None = None
45
+ except ImportError as _ie:
46
+ _DEPS_AVAILABLE = False
47
+ _DEPS_ERROR = (
48
+ f"Missing Python dependency: {_ie}.\n"
49
+ "Run: igel-qe init --with-copilot to install required packages."
50
+ )
51
+ # Define stubs so references below don't cause NameErrors
52
+ ensure_igel_tables = JiraConnector = WorkflowEngine = None # type: ignore[assignment,misc]
53
+ analyze_diff_scope = RouteInputs = compute_weighted_score = classify_route = None # type: ignore[assignment]
54
+ FrameworkAnalyzerAgent = Orchestrator = ApprovalError = cfg = None # type: ignore[assignment,misc]
55
+ ContextProjectionBuilder = sync_requirement_context = enrich_context_files = None # type: ignore[assignment]
42
56
 
43
57
  logging.basicConfig(level=logging.ERROR) # Only output strict JSON to stdout
44
58
 
@@ -99,6 +113,28 @@ def main():
99
113
  except json.JSONDecodeError:
100
114
  _err("Invalid JSON args")
101
115
 
116
+ # ── Dependency guard ───────────────────────────────────────────────────────
117
+ # If core Python packages (e.g. psycopg2) are missing, return a structured
118
+ # warning for init_check so the CLI can surface a helpful message rather
119
+ # than crashing with a raw traceback.
120
+ if not _DEPS_AVAILABLE:
121
+ if args.action in ("init_project", "init_check"):
122
+ print(json.dumps({
123
+ "status": "success",
124
+ "ready": False,
125
+ "required_failed": ["python_dependencies"],
126
+ "warnings": [],
127
+ "checks": {
128
+ "python_dependencies": {
129
+ "status": "error",
130
+ "message": _DEPS_ERROR or "Required Python packages not installed. Run: igel-qe init --with-copilot",
131
+ }
132
+ },
133
+ }))
134
+ sys.exit(0)
135
+ else:
136
+ _err(_DEPS_ERROR or "Required Python packages not installed. Run: igel-qe init --with-copilot")
137
+
102
138
  # ── DB init ────────────────────────────────────────────────────────────────
103
139
  if args.action not in ("submit_feedback",):
104
140
  try:
package/src/db/client.py CHANGED
@@ -3,15 +3,31 @@ from __future__ import annotations
3
3
  import logging
4
4
  import os
5
5
 
6
- import psycopg2
7
- import psycopg2.pool
6
+ try:
7
+ import psycopg2
8
+ import psycopg2.pool
9
+ _PSYCOPG2_AVAILABLE = True
10
+ except ImportError:
11
+ psycopg2 = None # type: ignore[assignment]
12
+ _PSYCOPG2_AVAILABLE = False
13
+
8
14
  from contextlib import contextmanager
9
15
 
10
16
  from src.config import cfg
11
17
 
12
18
  logger = logging.getLogger(__name__)
13
19
 
14
- _pool: psycopg2.pool.ThreadedConnectionPool | None = None
20
+ _pool: "psycopg2.pool.ThreadedConnectionPool | None" = None
21
+
22
+
23
+ def _require_psycopg2() -> None:
24
+ """Raise a clear error if psycopg2 is not installed."""
25
+ if not _PSYCOPG2_AVAILABLE:
26
+ raise ImportError(
27
+ "psycopg2 is not installed. Run: pip install psycopg2-binary\n"
28
+ "or re-run: igel-qe init --with-copilot (which will retry the install)"
29
+ )
30
+
15
31
 
16
32
 
17
33
  def _pool_kwargs() -> dict:
@@ -32,6 +48,7 @@ def _pool_kwargs() -> dict:
32
48
 
33
49
 
34
50
  def init_pool(minconn: int = 2, maxconn: int = 10) -> None:
51
+ _require_psycopg2()
35
52
  global _pool
36
53
  if _pool is not None:
37
54
  return
@@ -49,7 +49,8 @@ beautifulsoup4>=4.12.0
49
49
  lxml>=5.0.0
50
50
  duckduckgo-search>=6.0.0,<7.0.0; python_version < "3.9"
51
51
  firecrawl-py>=1.0.0
52
- playwright>=1.40.0,<1.49.0
52
+ # playwright: no upper bound — crawl4ai>=0.4 requires playwright>=1.49
53
+ playwright>=1.49.0
53
54
  crawl4ai>=0.4.0,<0.5.0; python_version >= "3.9"
54
55
 
55
56
  # REST API server (always-running service)