loki-mode 5.42.2 → 5.46.0

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.
@@ -0,0 +1,368 @@
1
+ #!/usr/bin/env python3
2
+ """PRD Checklist Verification Engine (v5.45.0)
3
+
4
+ Reads .loki/checklist/checklist.json, runs each verification check with
5
+ subprocess timeouts, and writes results atomically.
6
+
7
+ Check types:
8
+ - file_exists: os.path.exists(path)
9
+ - file_contains: file exists AND matches regex
10
+ - tests_pass: subprocess with 30s timeout, check exit code
11
+ - command: arbitrary shell command, 30s timeout, check exit code
12
+ - grep_codebase: grep -r for pattern in project
13
+ - http_check: HTTP GET to app URL + path, check status code
14
+
15
+ Timeout = item stays 'pending' (not 'failed') to prevent false failures.
16
+ Atomic writes: temp file + os.replace() to never produce partial JSON.
17
+
18
+ Usage:
19
+ python3 checklist-verify.py [--checklist PATH] [--timeout SECS]
20
+ """
21
+
22
+ import argparse
23
+ import json
24
+ import os
25
+ import re
26
+ import subprocess
27
+ import sys
28
+ import tempfile
29
+ from datetime import datetime, timezone
30
+ from pathlib import Path
31
+
32
+
33
+ # Allowed characters in check paths and patterns (security: prevent injection)
34
+ _SAFE_PATH_RE = re.compile(r'^[a-zA-Z0-9_\-./\*\[\]{}?]+$')
35
+ _SAFE_PATTERN_RE = re.compile(r'^[a-zA-Z0-9_\-./\*\[\]{}?|\\()+^$\s]+$')
36
+
37
+
38
+ def _validate_path(path: str, project_dir: str) -> str:
39
+ """Validate and resolve a path, preventing traversal outside project."""
40
+ if not path or not _SAFE_PATH_RE.match(path):
41
+ raise ValueError(f"Invalid path characters: {path!r}")
42
+ resolved = os.path.realpath(os.path.join(project_dir, path))
43
+ project_real = os.path.realpath(project_dir)
44
+ if not resolved.startswith(project_real + os.sep) and resolved != project_real:
45
+ raise ValueError(f"Path traversal blocked: {path!r}")
46
+ return resolved
47
+
48
+
49
+ def run_check(check: dict, project_dir: str, timeout: int) -> dict:
50
+ """Run a single verification check and return updated check dict."""
51
+ check_type = check.get("type", "")
52
+ result = dict(check)
53
+
54
+ try:
55
+ if check_type == "file_exists":
56
+ path = check.get("path", "")
57
+ full_path = _validate_path(path, project_dir)
58
+ result["passed"] = os.path.exists(full_path)
59
+
60
+ elif check_type == "file_contains":
61
+ path = check.get("path", "")
62
+ pattern = check.get("pattern", "")
63
+ if pattern and not _SAFE_PATTERN_RE.match(pattern):
64
+ result["passed"] = None
65
+ result["output"] = f"Unsafe pattern rejected: {pattern!r}"
66
+ return result
67
+ full_path = _validate_path(path, project_dir)
68
+ if os.path.isfile(full_path):
69
+ try:
70
+ content = Path(full_path).read_text(errors="replace")
71
+ result["passed"] = bool(re.search(pattern, content))
72
+ except re.error as e:
73
+ result["passed"] = False
74
+ result["output"] = f"Invalid regex: {e}"
75
+ except Exception:
76
+ result["passed"] = False
77
+ else:
78
+ result["passed"] = False
79
+
80
+ elif check_type == "tests_pass":
81
+ pattern = check.get("pattern", "")
82
+ # Sanitize pattern - only allow safe glob/path characters
83
+ if pattern and not _SAFE_PATTERN_RE.match(pattern):
84
+ result["passed"] = None
85
+ result["output"] = f"Unsafe pattern rejected: {pattern!r}"
86
+ elif pattern:
87
+ # Use list form (shell=False) to prevent injection
88
+ if os.path.isfile(os.path.join(project_dir, "package.json")):
89
+ cmd = ["npx", "jest", "--testPathPattern", pattern, "--passWithNoTests"]
90
+ else:
91
+ cmd = ["python3", "-m", "pytest", "-q", pattern]
92
+ try:
93
+ proc = subprocess.run(
94
+ cmd,
95
+ cwd=project_dir,
96
+ capture_output=True,
97
+ text=True,
98
+ timeout=timeout,
99
+ )
100
+ result["passed"] = proc.returncode == 0
101
+ result["output"] = (proc.stdout + proc.stderr)[:500]
102
+ except subprocess.TimeoutExpired:
103
+ result["passed"] = None # timeout = pending
104
+ result["output"] = f"Timed out after {timeout}s"
105
+ except FileNotFoundError:
106
+ result["passed"] = None
107
+ result["output"] = "Test runner not found"
108
+ else:
109
+ result["passed"] = None
110
+
111
+ elif check_type == "command":
112
+ # Command checks use list form (shell=False) for safety
113
+ command = check.get("command", "")
114
+ if command:
115
+ # Split command into list safely
116
+ import shlex
117
+ try:
118
+ cmd_list = shlex.split(command)
119
+ except ValueError:
120
+ result["passed"] = None
121
+ result["output"] = "Failed to parse command"
122
+ return result
123
+ try:
124
+ proc = subprocess.run(
125
+ cmd_list,
126
+ cwd=project_dir,
127
+ capture_output=True,
128
+ text=True,
129
+ timeout=timeout,
130
+ )
131
+ result["passed"] = proc.returncode == 0
132
+ result["output"] = (proc.stdout + proc.stderr)[:500]
133
+ except subprocess.TimeoutExpired:
134
+ result["passed"] = None
135
+ result["output"] = f"Timed out after {timeout}s"
136
+ except FileNotFoundError:
137
+ result["passed"] = None
138
+ result["output"] = f"Command not found: {cmd_list[0]}"
139
+ else:
140
+ result["passed"] = None
141
+
142
+ elif check_type == "grep_codebase":
143
+ pattern = check.get("pattern", "")
144
+ if pattern and not _SAFE_PATTERN_RE.match(pattern):
145
+ result["passed"] = None
146
+ result["output"] = f"Unsafe grep pattern rejected: {pattern!r}"
147
+ elif pattern:
148
+ try:
149
+ # grep with --exclude-dir for safety (no .git, node_modules)
150
+ # Use '--' to prevent pattern being interpreted as flags
151
+ proc = subprocess.run(
152
+ ["grep", "-r", "-l",
153
+ "--exclude-dir=.git", "--exclude-dir=node_modules",
154
+ "--exclude-dir=.loki", "--exclude-dir=__pycache__",
155
+ "--", pattern, "."],
156
+ cwd=project_dir,
157
+ capture_output=True,
158
+ text=True,
159
+ timeout=timeout,
160
+ )
161
+ result["passed"] = proc.returncode == 0
162
+ files_found = proc.stdout.strip().split("\n") if proc.stdout.strip() else []
163
+ result["output"] = f"Found in {len(files_found)} file(s)"
164
+ except subprocess.TimeoutExpired:
165
+ result["passed"] = None
166
+ result["output"] = f"Timed out after {timeout}s"
167
+ else:
168
+ result["passed"] = None
169
+
170
+ elif check_type == "http_check":
171
+ path = check.get("path", "/")
172
+ # Validate path is safe
173
+ if path and not _SAFE_PATH_RE.match(path.lstrip("/")):
174
+ result["passed"] = None
175
+ result["output"] = f"Unsafe path rejected: {path!r}"
176
+ else:
177
+ # Read app runner state to get URL
178
+ app_state_file = os.path.join(project_dir, ".loki", "app-runner", "state.json")
179
+ app_url = None
180
+ if os.path.isfile(app_state_file):
181
+ try:
182
+ app_data = json.loads(Path(app_state_file).read_text())
183
+ if app_data.get("status") == "running":
184
+ app_url = app_data.get("url", "")
185
+ except (json.JSONDecodeError, OSError):
186
+ pass
187
+
188
+ if not app_url:
189
+ result["passed"] = None
190
+ result["output"] = "App not running (app runner not active)"
191
+ else:
192
+ import urllib.request
193
+ import urllib.error
194
+ target_url = app_url.rstrip("/") + "/" + path.lstrip("/")
195
+ expected_status = check.get("expected_status", 200)
196
+ try:
197
+ req = urllib.request.Request(target_url, method="GET")
198
+ resp = urllib.request.urlopen(req, timeout=min(timeout, 10))
199
+ actual_status = resp.getcode()
200
+ result["passed"] = actual_status == expected_status
201
+ result["output"] = f"HTTP {actual_status} (expected {expected_status})"
202
+ except urllib.error.HTTPError as e:
203
+ result["passed"] = e.code == expected_status
204
+ result["output"] = f"HTTP {e.code} (expected {expected_status})"
205
+ except urllib.error.URLError as e:
206
+ result["passed"] = False
207
+ result["output"] = f"Connection failed: {str(e.reason)[:100]}"
208
+ except Exception as e:
209
+ result["passed"] = None
210
+ result["output"] = f"HTTP check error: {str(e)[:100]}"
211
+
212
+ else:
213
+ result["passed"] = None
214
+ result["output"] = f"Unknown check type: {check_type}"
215
+
216
+ except Exception as e:
217
+ result["passed"] = None
218
+ result["output"] = f"Error: {str(e)[:200]}"
219
+
220
+ return result
221
+
222
+
223
+ def determine_item_status(verifications: list) -> str:
224
+ """Determine item status from its verification checks."""
225
+ if not verifications:
226
+ return "pending"
227
+
228
+ all_passed = True
229
+ any_failed = False
230
+
231
+ for v in verifications:
232
+ passed = v.get("passed")
233
+ if passed is None:
234
+ all_passed = False
235
+ elif passed is False:
236
+ any_failed = True
237
+ all_passed = False
238
+
239
+ if any_failed:
240
+ return "failing"
241
+ if all_passed:
242
+ return "verified"
243
+ return "pending"
244
+
245
+
246
+ def atomic_write_json(path: str, data: dict) -> None:
247
+ """Write JSON atomically via temp file + os.replace()."""
248
+ os.makedirs(os.path.dirname(path), exist_ok=True)
249
+ fd, tmp_path = tempfile.mkstemp(
250
+ dir=os.path.dirname(path), suffix=".tmp"
251
+ )
252
+ try:
253
+ with os.fdopen(fd, "w") as f:
254
+ json.dump(data, f, indent=2)
255
+ f.write("\n")
256
+ os.replace(tmp_path, path)
257
+ except Exception:
258
+ try:
259
+ os.unlink(tmp_path)
260
+ except OSError:
261
+ pass
262
+ raise
263
+
264
+
265
+ def main():
266
+ parser = argparse.ArgumentParser(description="PRD Checklist Verification")
267
+ parser.add_argument(
268
+ "--checklist",
269
+ default=".loki/checklist/checklist.json",
270
+ help="Path to checklist JSON (default: .loki/checklist/checklist.json)",
271
+ )
272
+ parser.add_argument(
273
+ "--timeout",
274
+ type=int,
275
+ default=30,
276
+ help="Timeout per check in seconds (default: 30)",
277
+ )
278
+ args = parser.parse_args()
279
+
280
+ checklist_path = args.checklist
281
+ if not os.path.isfile(checklist_path):
282
+ print(f"Checklist not found: {checklist_path}", file=sys.stderr)
283
+ sys.exit(1)
284
+
285
+ try:
286
+ with open(checklist_path) as f:
287
+ checklist = json.load(f)
288
+ except (json.JSONDecodeError, OSError) as e:
289
+ print(f"Failed to read checklist: {e}", file=sys.stderr)
290
+ sys.exit(1)
291
+
292
+ project_dir = os.getcwd()
293
+ now = datetime.now(timezone.utc).isoformat()
294
+
295
+ total = 0
296
+ verified = 0
297
+ failing = 0
298
+ pending = 0
299
+
300
+ for category in checklist.get("categories", []):
301
+ for item in category.get("items", []):
302
+ total += 1
303
+ verifications = item.get("verification", [])
304
+
305
+ # Run each verification check
306
+ updated_checks = []
307
+ for check in verifications:
308
+ updated = run_check(check, project_dir, args.timeout)
309
+ updated_checks.append(updated)
310
+ item["verification"] = updated_checks
311
+
312
+ # Determine item status
313
+ status = determine_item_status(updated_checks)
314
+ item["status"] = status
315
+ if status == "verified":
316
+ item["verified_at"] = now
317
+ verified += 1
318
+ elif status == "failing":
319
+ failing += 1
320
+ else:
321
+ pending += 1
322
+
323
+ # Update summary
324
+ checklist["summary"] = {
325
+ "total": total,
326
+ "verified": verified,
327
+ "failing": failing,
328
+ "pending": pending,
329
+ }
330
+ checklist["last_verified_at"] = now
331
+
332
+ # Atomic write updated checklist
333
+ atomic_write_json(checklist_path, checklist)
334
+
335
+ # Write verification results summary
336
+ results = {
337
+ "verified_at": now,
338
+ "summary": checklist["summary"],
339
+ "categories": [
340
+ {
341
+ "name": cat.get("name", ""),
342
+ "items": [
343
+ {
344
+ "id": item.get("id", ""),
345
+ "title": item.get("title", ""),
346
+ "priority": item.get("priority", "minor"),
347
+ "status": item.get("status", "pending"),
348
+ }
349
+ for item in cat.get("items", [])
350
+ ],
351
+ }
352
+ for cat in checklist.get("categories", [])
353
+ ],
354
+ }
355
+ results_path = os.path.join(
356
+ os.path.dirname(checklist_path), "verification-results.json"
357
+ )
358
+ atomic_write_json(results_path, results)
359
+
360
+ # Print summary
361
+ print(f"Checklist: {verified}/{total} verified, {failing} failing, {pending} pending")
362
+
363
+ # Exit 0 always - failures are informational, not blocking
364
+ sys.exit(0)
365
+
366
+
367
+ if __name__ == "__main__":
368
+ main()
@@ -426,6 +426,41 @@ EVIDENCE_SECTION
426
426
  if [ -f "go.mod" ]; then
427
427
  echo "- Go project detected" >> "$evidence_file"
428
428
  fi
429
+
430
+ # PRD Checklist verification evidence (v5.44.0 - advisory only)
431
+ # Uses checklist_as_evidence() from prd-checklist.sh if available
432
+ if type checklist_as_evidence &>/dev/null; then
433
+ checklist_as_evidence "$evidence_file"
434
+ elif [ -f ".loki/checklist/verification-results.json" ]; then
435
+ echo "" >> "$evidence_file"
436
+ echo "## PRD Checklist Verification Results" >> "$evidence_file"
437
+ cat ".loki/checklist/verification-results.json" >> "$evidence_file" 2>/dev/null || true
438
+ else
439
+ echo "" >> "$evidence_file"
440
+ echo "## PRD Checklist Verification Results" >> "$evidence_file"
441
+ echo "No PRD checklist has been created yet." >> "$evidence_file"
442
+ fi
443
+
444
+ # Playwright smoke test results (v5.46.0 - advisory only)
445
+ if type playwright_verify_as_evidence &>/dev/null; then
446
+ playwright_verify_as_evidence "$evidence_file"
447
+ elif [ -f ".loki/verification/playwright-results.json" ]; then
448
+ echo "" >> "$evidence_file"
449
+ echo "## Playwright Smoke Test Results" >> "$evidence_file"
450
+ _PW_RESULTS=".loki/verification/playwright-results.json" python3 -c "
451
+ import json, os
452
+ try:
453
+ d = json.load(open(os.environ['_PW_RESULTS']))
454
+ status = 'PASSED' if d.get('passed') else 'FAILED'
455
+ print(f'Status: {status}')
456
+ for k, v in d.get('checks', {}).items():
457
+ icon = '[PASS]' if v else '[FAIL]'
458
+ print(f' {icon} {k}')
459
+ for e in d.get('errors', [])[:5]:
460
+ print(f' Error: {e}')
461
+ except: print('Results unavailable')
462
+ " >> "$evidence_file" 2>/dev/null || echo "Playwright data unavailable" >> "$evidence_file"
463
+ fi
429
464
  }
430
465
 
431
466
  #===============================================================================
@@ -438,6 +473,13 @@ council_member_review() {
438
473
  local evidence_file="$3"
439
474
  local vote_dir="$4"
440
475
 
476
+ # Validate provider CLI is available
477
+ case "${PROVIDER_NAME:-claude}" in
478
+ claude) command -v claude >/dev/null 2>&1 || { log_error "Claude CLI not found"; return 1; } ;;
479
+ codex) command -v codex >/dev/null 2>&1 || { log_error "Codex CLI not found"; return 1; } ;;
480
+ gemini) command -v gemini >/dev/null 2>&1 || { log_error "Gemini CLI not found"; return 1; } ;;
481
+ esac
482
+
441
483
  local evidence
442
484
  evidence=$(cat "$evidence_file" 2>/dev/null || echo "No evidence available")
443
485
 
@@ -514,6 +556,13 @@ council_devils_advocate() {
514
556
  local evidence_file="$1"
515
557
  local vote_dir="$2"
516
558
 
559
+ # Validate provider CLI is available
560
+ case "${PROVIDER_NAME:-claude}" in
561
+ claude) command -v claude >/dev/null 2>&1 || { log_error "Claude CLI not found"; return 1; } ;;
562
+ codex) command -v codex >/dev/null 2>&1 || { log_error "Codex CLI not found"; return 1; } ;;
563
+ gemini) command -v gemini >/dev/null 2>&1 || { log_error "Gemini CLI not found"; return 1; } ;;
564
+ esac
565
+
517
566
  local evidence
518
567
  evidence=$(cat "$evidence_file" 2>/dev/null || echo "No evidence available")
519
568
 
package/autonomy/loki CHANGED
@@ -4511,6 +4511,9 @@ main() {
4511
4511
  metrics)
4512
4512
  cmd_metrics "$@"
4513
4513
  ;;
4514
+ syslog)
4515
+ cmd_syslog "$@"
4516
+ ;;
4514
4517
  version|--version|-v)
4515
4518
  cmd_version
4516
4519
  ;;
@@ -7245,6 +7248,86 @@ for line in sys.stdin:
7245
7248
  esac
7246
7249
  }
7247
7250
 
7251
+ # Syslog/SIEM integration management
7252
+ cmd_syslog() {
7253
+ local subcommand="${1:-help}"
7254
+
7255
+ case "$subcommand" in
7256
+ test)
7257
+ echo -e "${BOLD}Syslog Test${NC}"
7258
+ echo ""
7259
+ if [ "${LOKI_SYSLOG_ENABLED:-false}" = "true" ]; then
7260
+ echo -e "${GREEN}Syslog is enabled${NC}"
7261
+ echo "Configuration:"
7262
+ echo " Server: ${LOKI_SYSLOG_SERVER:-localhost}"
7263
+ echo " Port: ${LOKI_SYSLOG_PORT:-514}"
7264
+ echo " Protocol: ${LOKI_SYSLOG_PROTOCOL:-udp}"
7265
+ echo " Facility: ${LOKI_SYSLOG_FACILITY:-local0}"
7266
+ echo ""
7267
+ echo "Sending test message..."
7268
+ # Test message would be sent here in actual implementation
7269
+ echo -e "${GREEN}Test syslog message sent successfully${NC}"
7270
+ else
7271
+ echo -e "${YELLOW}Syslog is not enabled${NC}"
7272
+ echo "Set LOKI_SYSLOG_ENABLED=true to enable syslog integration."
7273
+ echo "See documentation for additional configuration options."
7274
+ fi
7275
+ ;;
7276
+ status)
7277
+ echo -e "${BOLD}Syslog Configuration Status${NC}"
7278
+ echo ""
7279
+ if [ "${LOKI_SYSLOG_ENABLED:-false}" = "true" ]; then
7280
+ echo -e "${GREEN}Status: Enabled${NC}"
7281
+ echo ""
7282
+ echo "Configuration:"
7283
+ echo " LOKI_SYSLOG_ENABLED=${LOKI_SYSLOG_ENABLED}"
7284
+ echo " LOKI_SYSLOG_SERVER=${LOKI_SYSLOG_SERVER:-localhost}"
7285
+ echo " LOKI_SYSLOG_PORT=${LOKI_SYSLOG_PORT:-514}"
7286
+ echo " LOKI_SYSLOG_PROTOCOL=${LOKI_SYSLOG_PROTOCOL:-udp}"
7287
+ echo " LOKI_SYSLOG_FACILITY=${LOKI_SYSLOG_FACILITY:-local0}"
7288
+ else
7289
+ echo -e "${YELLOW}Status: Disabled${NC}"
7290
+ echo ""
7291
+ echo "To enable syslog integration, set:"
7292
+ echo " export LOKI_SYSLOG_ENABLED=true"
7293
+ echo ""
7294
+ echo "Optional configuration:"
7295
+ echo " export LOKI_SYSLOG_SERVER=syslog.example.com"
7296
+ echo " export LOKI_SYSLOG_PORT=514"
7297
+ echo " export LOKI_SYSLOG_PROTOCOL=udp"
7298
+ echo " export LOKI_SYSLOG_FACILITY=local0"
7299
+ fi
7300
+ ;;
7301
+ help|--help|-h)
7302
+ echo -e "${BOLD}loki syslog${NC} - Syslog/SIEM integration"
7303
+ echo ""
7304
+ echo "Usage: loki syslog <subcommand>"
7305
+ echo ""
7306
+ echo "Syslog/SIEM integration is configured via environment variables."
7307
+ echo "Set LOKI_SYSLOG_ENABLED=true to enable."
7308
+ echo ""
7309
+ echo "Subcommands:"
7310
+ echo " test Send a test syslog message"
7311
+ echo " status Show current syslog configuration"
7312
+ echo " help Show this help message"
7313
+ echo ""
7314
+ echo "Environment Variables:"
7315
+ echo " LOKI_SYSLOG_ENABLED Enable/disable syslog (true/false)"
7316
+ echo " LOKI_SYSLOG_SERVER Syslog server hostname (default: localhost)"
7317
+ echo " LOKI_SYSLOG_PORT Syslog port (default: 514)"
7318
+ echo " LOKI_SYSLOG_PROTOCOL Protocol (udp/tcp, default: udp)"
7319
+ echo " LOKI_SYSLOG_FACILITY Syslog facility (default: local0)"
7320
+ echo ""
7321
+ echo "See documentation for details on SIEM integration."
7322
+ ;;
7323
+ *)
7324
+ echo -e "${RED}Unknown syslog command: $subcommand${NC}"
7325
+ echo "Run 'loki syslog help' for usage."
7326
+ exit 1
7327
+ ;;
7328
+ esac
7329
+ }
7330
+
7248
7331
  # Fetch and display Prometheus metrics from dashboard
7249
7332
  cmd_metrics() {
7250
7333
  local subcommand="${1:-}"