From 08f8e4d28a2ca2a39dc76990a8d6b971bc3264a8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 01:19:16 +0000 Subject: [PATCH 1/2] Add structured Install tab results from parsed /api/run logs Detect now shows mode, recommendation, markers, and adapters. Run/verify attach a summary (would/created/checks/next steps) so operators do not have to scrape the raw script log. Co-authored-by: John Menke --- CHANGELOG.md | 1 + console-ui/README.md | 2 +- console-ui/src/components/InstallTab.vue | 70 ++++++ console-ui/src/styles.css | 15 ++ docs/ops-console.md | 2 +- engine/src/sdlc_engine/installer/runner.py | 217 ++++++++++++++++-- .../tests_e2e/test_vue3_console_playwright.py | 5 + engine/tests_integration/test_installer.py | 5 + .../test_installer_runtime_units.py | 68 ++++++ 9 files changed, 364 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1057008..c6a750e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ All notable changes to this project will be documented in this file. - Vue Templates tab: markdown + ADF preview, and **Open in ADF Viewer after write** (`open_viewer` on `/api/templates/render`) - Vue SQLite work browser: `/api/sqlite/works` filter + jump to Templates / Issues / ADF with the Work ID filled - Vue SQLite work detail: click a Work ID to read requirement / canvas / analysis via `POST /api/sqlite/work` (git files, not sqlite blobs) +- Vue Install tab: detect cards + parsed `/api/run` summary (would/created/checks/next steps); raw log remains - Vue Dashboard jumps: active Work ID, suggestions, and activity open SQLite / Templates / Issues / ADF with that id filled - Vue3 ops console **Dashboard** + **Issues** tabs (parity with the Flask console) and Playwright coverage for refresh, tracker save/toggle, link preview, and sync dry-run - Vue3 Persistence **Check ledger parity** / **Parity + repair** buttons (same `/api/persistence/parity` as the Flask console) diff --git a/console-ui/README.md b/console-ui/README.md index 9eae530..32c9c40 100644 --- a/console-ui/README.md +++ b/console-ui/README.md @@ -7,7 +7,7 @@ Vite + Vue3 shell that talks to the existing Flask installer JSON API (`/api/*`) - **Dashboard** (default) → status / activity / suggestions; jumps to SQLite / Templates / Issues / ADF (`/api/dashboard/*`) - **Persistence** → status + save + ledger parity (`/api/persistence/*`) - **Templates** → list/render/write ADF with markdown preview; optional open-in-viewer (`/api/templates/*`) -- **Install** → detect + run/verify (`/api/detect`, `/api/run`) +- **Install** → detect cards + run/verify with parsed summary (`/api/detect`, `/api/run`) - **SQLite** → status + rebuild + filterable work browser + requirement/canvas/analysis detail (`/api/sqlite/*`) - **Rollback** → backups + restore (`/api/backups`, `/api/rollback`) - **Guide** → config/probe/lifecycle (`/api/guide/*`) diff --git a/console-ui/src/components/InstallTab.vue b/console-ui/src/components/InstallTab.vue index 7073efe..0a5848f 100644 --- a/console-ui/src/components/InstallTab.vue +++ b/console-ui/src/components/InstallTab.vue @@ -22,6 +22,7 @@ const statusClass = ref(""); const log = ref("Awaiting action…"); const loading = ref(false); const lastDetect = ref(null); +const lastResult = ref(null); function assistants() { if (asAll.value) return ["all"]; @@ -96,6 +97,7 @@ async function run(actionOverride) { no_backup: noBackup.value, with_python_engine: withEngine.value, }); + lastResult.value = data; const cmd = (data.command || []).join(" "); log.value = (cmd ? `$ ${cmd}\n\n` : "") + (data.log || data.error || ""); if (ok && data.ok !== false) { @@ -115,8 +117,14 @@ async function run(actionOverride) { } } +function assistantNames(info) { + const a = (info && info.assistants) || {}; + return ["cursor", "copilot", "claude"].filter((name) => a[name]); +} + function clearLog() { log.value = "Awaiting action…"; + lastResult.value = null; statusText.value = "Ready."; statusClass.value = ""; } @@ -139,6 +147,27 @@ function clearLog() { {{ modePill }}

{{ detectDetail }}

+
+
+
{{ lastDetect.mode }}
+
mode
+
+
+
{{ lastDetect.recommendation }}
+
recommend
+
+
+
{{ (lastDetect.markers || []).length }}
+
markers
+
+
+
{{ assistantNames(lastDetect).join(", ") || "none" }}
+
adapters
+
+
+
@@ -166,6 +195,47 @@ function clearLog() {

{{ statusText }}

+
+

{{ lastResult.summary.headline || lastResult.summary.action }}

+
+
+
{{ lastResult.summary.exit_code }}
+
exit
+
+
+
{{ lastResult.summary.would_count }}
+
would
+
+
+
{{ lastResult.summary.created_count }}
+
created
+
+
+
+ {{ lastResult.summary.check_ok_count }}/{{ lastResult.summary.check_ok_count + lastResult.summary.check_fail_count }} +
+
checks
+
+
+

{{ lastResult.summary.command }}

+

{{ lastResult.summary.framework_home }}

+

{{ lastResult.summary.checks_summary }}

+
    +
  • {{ step }}
  • +
+
    +
  • {{ w }}
  • +
+
    +
  • {{ item }}
  • +
+
    +
  • {{ item }}
  • +
+
    +
  • {{ item }}
  • +
+
{{ log }}
diff --git a/console-ui/src/styles.css b/console-ui/src/styles.css index 5246b46..489ef71 100644 --- a/console-ui/src/styles.css +++ b/console-ui/src/styles.css @@ -301,6 +301,21 @@ td { border: 1px solid rgba(230, 184, 77, 0.4); } +.result-list { + margin: 0 0 0.75rem; + padding-left: 1.15rem; + max-height: 12rem; + overflow: auto; + color: var(--ink); + font-family: var(--font-mono); + font-size: 0.8rem; +} + +.result-list li { + margin: 0.15rem 0; + word-break: break-word; +} + .mode-pill { display: inline-flex; align-items: center; diff --git a/docs/ops-console.md b/docs/ops-console.md index 7be2841..0a5dd56 100644 --- a/docs/ops-console.md +++ b/docs/ops-console.md @@ -48,7 +48,7 @@ the `--root` passed when starting the ADF Viewer. | Tab | What it does | |-----|----------------| | **Dashboard** | **Default landing tab.** Active Work ID, phase, gates, suggested next command, accepted vs staged lesson counts, backend status, integration shortcuts. Work ID / suggestions / activity jump to SQLite, Templates, Issues, or ADF with that id filled. | -| **Install / Upgrade** | Detect fresh vs upgrade; run setup/upgrade/verify (dry-run supported) | +| **Install / Upgrade** | Detect fresh vs upgrade (mode / markers / adapters). Run setup/upgrade/verify and show a parsed summary (would/created/checks/next steps) plus the raw log. Dry-run supported. | | **Persistence** | Toggle `CONTEXT_BACKENDS` backends (`git-pointers`, `sqlite`, `guide-dice`); optional Guide URL + notes → `.sdlc/persistence-config.json`. **Check ledger parity** and **Parity + repair** buttons call `sdlc-engine context parity`. Operator guide: [triple-path-context.md](triple-path-context.md) | | **Templates** | Render ADF combos for a Work ID with markdown + JSON preview; optional write to `adf/.adf.json` and open the ADF Viewer on that file | | **SQLite** | `.sdlc/index.sqlite` status + rebuild, plus a filterable work browser. Click a Work ID to read the requirement / canvas / analysis (from git files). Jump to Templates / Issues / ADF | diff --git a/engine/src/sdlc_engine/installer/runner.py b/engine/src/sdlc_engine/installer/runner.py index 0d03ec8..4012138 100644 --- a/engine/src/sdlc_engine/installer/runner.py +++ b/engine/src/sdlc_engine/installer/runner.py @@ -3,10 +3,185 @@ from __future__ import annotations import os +import re import subprocess from pathlib import Path from typing import Any +_LIST_CAP = 40 +_SECTION_HEADERS = ( + ("Created or updated", "created"), + ("Created (", "created"), + ("Skipped existing", "skipped"), + ("Updated framework files", "updated"), + ("Unchanged framework files", "unchanged"), + ("Preserved existing project content", "preserved"), + ("Consolidated", "consolidated"), + ("Backups", "backups"), +) +_HEADLINE_HINTS = ( + "initialization complete", + "upgrade complete", + "setup complete", + "verification passed", + "verification failed", +) +_DRY_WOULD = re.compile(r"^\[dry-run\] would (.+)$") +_VERIFY_OK = re.compile(r"^ok\s+(.+)$") +_VERIFY_FAIL = re.compile(r"^fail\s+(.+)$") +_NUMBERED = re.compile(r"^\d+\.\s+") + + +def summarize_run_log( + *, + action: str, + log: str, + command: list[str] | None = None, + dry_run: bool = False, + ok: bool = False, + exit_code: int = 0, +) -> dict[str, Any]: + """Parse install/upgrade/verify script output into console-friendly lists.""" + command = command or [] + buckets: dict[str, list[str]] = { + "would": [], + "created": [], + "skipped": [], + "updated": [], + "unchanged": [], + "preserved": [], + "consolidated": [], + "backups": [], + "checks_ok": [], + "checks_fail": [], + "warnings": [], + "next_steps": [], + } + headline = "" + home = "" + checks_summary = "" + section: str | None = None + + def _section_header(stripped: str) -> str | None: + for prefix, key in _SECTION_HEADERS: + if stripped.startswith(prefix): + return key + return None + + for raw in (log or "").splitlines(): + stripped = raw.strip() + if not stripped: + if section and section != "next_steps": + section = None + continue + + dry = _DRY_WOULD.match(stripped) + if dry: + buckets["would"].append(dry.group(1)) + section = None + continue + + if stripped.startswith("WARNING"): + buckets["warnings"].append(stripped) + section = None + continue + + if stripped == "Next steps:": + section = "next_steps" + continue + + header = _section_header(stripped) + if header: + section = header + continue + + if section == "next_steps": + if _NUMBERED.match(stripped): + buckets["next_steps"].append(stripped) + elif buckets["next_steps"] and (raw.startswith(" ") or stripped.startswith("/")): + buckets["next_steps"][-1] = f"{buckets['next_steps'][-1]} {stripped}" + elif stripped.startswith("For "): + section = None + continue + + if section and raw.startswith(" ") and stripped != "none": + buckets[section].append(stripped) + continue + if section and not raw.startswith(" "): + section = None + + if stripped.startswith("Framework home:"): + home = stripped.split(":", 1)[1].strip() + if stripped.startswith("Recommended next step:"): + buckets["next_steps"].append(stripped.split(":", 1)[1].strip()) + if stripped.startswith("Summary:") and "checks passed" in stripped: + checks_summary = stripped + if any(hint in stripped.lower() for hint in _HEADLINE_HINTS): + headline = stripped + + verify_ok = _VERIFY_OK.match(stripped) + if verify_ok and raw.lstrip().startswith("ok"): + buckets["checks_ok"].append(verify_ok.group(1)) + continue + verify_fail = _VERIFY_FAIL.match(stripped) + if verify_fail and raw.lstrip().startswith("fail"): + buckets["checks_fail"].append(verify_fail.group(1)) + + would_count = len(buckets["would"]) + return { + "action": action, + "ok": ok, + "exit_code": exit_code, + "dry_run": dry_run, + "headline": headline, + "framework_home": home, + "command": " ".join(str(part) for part in command), + "would": buckets["would"][:_LIST_CAP], + "created": buckets["created"][:_LIST_CAP], + "skipped": buckets["skipped"][:_LIST_CAP], + "updated": buckets["updated"][:_LIST_CAP], + "backups": buckets["backups"][:_LIST_CAP], + "checks_ok": buckets["checks_ok"][:_LIST_CAP], + "checks_fail": buckets["checks_fail"][:_LIST_CAP], + "warnings": buckets["warnings"][:20], + "next_steps": buckets["next_steps"][:12], + "checks_summary": checks_summary, + "would_count": would_count, + "created_count": len(buckets["created"]), + "check_ok_count": len(buckets["checks_ok"]), + "check_fail_count": len(buckets["checks_fail"]), + } + + +def _run_payload( + *, + action: str, + ok: bool, + exit_code: int, + command: list[str], + log: str, + dry_run: bool = False, + engine_log: str = "", +) -> dict[str, Any]: + text = (log or "").strip() + return { + "ok": ok, + "action": action, + "exit_code": exit_code, + "command": command, + "log": text, + "engine_log": (engine_log or "").strip(), + "dry_run": dry_run, + "summary": summarize_run_log( + action=action, + log=text, + command=command, + dry_run=dry_run, + ok=ok, + exit_code=exit_code, + ), + } + def orchestrator_root() -> Path: """Locate the SDLC-SPDD orchestrator repo (engine → repo root).""" @@ -77,20 +252,22 @@ def run_action( if want_all or name in selected: cmd.append(f"--require-{name}") else: - return { - "ok": False, - "exit_code": 2, - "command": [], - "log": f"Unknown action: {action}", - } + return _run_payload( + action=action, + ok=False, + exit_code=2, + command=[], + log=f"Unknown action: {action}", + ) if not script.is_file(): - return { - "ok": False, - "exit_code": 2, - "command": cmd, - "log": f"Script not found: {script}", - } + return _run_payload( + action=action, + ok=False, + exit_code=2, + command=cmd, + log=f"Script not found: {script}", + ) proc = subprocess.run( cmd, @@ -106,13 +283,15 @@ def run_action( engine_log = _install_python_engine(root, target_path, timeout_sec=timeout_sec) log = (log + "\n" + engine_log).strip() - return { - "ok": proc.returncode == 0, - "exit_code": proc.returncode, - "command": cmd, - "log": log.strip(), - "engine_log": engine_log.strip() if engine_log else "", - } + return _run_payload( + action=action, + ok=proc.returncode == 0, + exit_code=proc.returncode, + command=cmd, + log=log, + dry_run=dry_run, + engine_log=engine_log, + ) def _install_python_engine(root: Path, target: Path, *, timeout_sec: int) -> str: diff --git a/engine/tests_e2e/test_vue3_console_playwright.py b/engine/tests_e2e/test_vue3_console_playwright.py index 71019c4..1220ba1 100644 --- a/engine/tests_e2e/test_vue3_console_playwright.py +++ b/engine/tests_e2e/test_vue3_console_playwright.py @@ -520,6 +520,11 @@ def test_vue3_install_detect_and_dry_run(page, live_vue_console) -> None: # typ log = page.get_by_test_id("install-log").inner_text() assert log.strip() assert "dry" in log.lower() or "setup-agent-prompts" in log or "Would" in log + page.get_by_test_id("install-summary").wait_for(state="visible") + assert page.get_by_test_id("detect-mode").inner_text().strip() == "fresh" + assert int(page.get_by_test_id("install-would-count").inner_text()) >= 1 + next_steps = page.get_by_test_id("install-next-steps").inner_text() + assert "sdlc-spdd-init" in next_steps def test_vue3_sqlite_refresh_and_rebuild(page, live_vue_console) -> None: # type: ignore[no-untyped-def] diff --git a/engine/tests_integration/test_installer.py b/engine/tests_integration/test_installer.py index 64af68a..1561082 100644 --- a/engine/tests_integration/test_installer.py +++ b/engine/tests_integration/test_installer.py @@ -83,6 +83,11 @@ def test_api_run_dry_install(tmp_path: Path) -> None: assert data["exit_code"] == 0 assert "setup-agent-prompts.sh" in " ".join(data["command"]) assert "--dry-run" in data["command"] + summary = data["summary"] + assert summary["dry_run"] is True + assert summary["would_count"] >= 1 + assert summary["would"] + assert any("sdlc-spdd-init" in step for step in summary["next_steps"]) def test_api_run_dry_upgrade(tmp_path: Path) -> None: diff --git a/engine/tests_unit/test_installer_runtime_units.py b/engine/tests_unit/test_installer_runtime_units.py index 0de5379..c2b2460 100644 --- a/engine/tests_unit/test_installer_runtime_units.py +++ b/engine/tests_unit/test_installer_runtime_units.py @@ -224,6 +224,74 @@ def read(self, n: int = -1) -> bytes: assert written["ok"] is True +def test_summarize_run_log_parses_install_upgrade_verify() -> None: + install = rn.summarize_run_log( + action="install", + log=( + "[dry-run] would create /tmp/app/sdlc-spdd/ROADMAP.md\n" + "[dry-run] would copy templates/x -> /tmp/app/sdlc-spdd/x\n" + "SDLC-SPDD initialization complete for: /tmp/app\n" + "Created or updated (2):\n" + " sdlc-spdd/ROADMAP.md\n" + " none\n" + "Skipped existing (0):\n" + " none\n" + "Recommended next step: run /sdlc-spdd-init then /sdlc-spdd-plan\n" + "Framework home: /tmp/app/sdlc-spdd (docs at sdlc-spdd/docs/)\n" + "Next steps:\n" + " 1. Open the target project.\n" + " 2. Start or resume context:\n" + " /tmp/app/sdlc-spdd/scripts/start-agent-session.sh --phase init\n" + " 3. Invoke:\n" + " /sdlc-spdd-init\n" + ), + command=["setup-agent-prompts.sh", "--dry-run"], + dry_run=True, + ok=True, + exit_code=0, + ) + assert install["would_count"] == 2 + assert install["would"][0].startswith("create ") + assert install["headline"].startswith("SDLC-SPDD initialization complete") + assert "/sdlc-spdd-init" in install["next_steps"][0] + assert any("Open the target" in step for step in install["next_steps"]) + assert any("start-agent-session.sh" in step for step in install["next_steps"]) + assert install["dry_run"] is True + + upgrade = rn.summarize_run_log( + action="upgrade", + log=( + "WARNING: leftover agent-context/\n" + "SDLC-SPDD framework upgrade complete for: /tmp/app\n" + "Created (1):\n" + " sdlc-spdd/docs/README.md\n" + "Backups (1):\n" + " /tmp/app/.sdlc-spdd-upgrade-backups/20260816T000000Z/x\n" + ), + ok=True, + exit_code=0, + ) + assert upgrade["created"] == ["sdlc-spdd/docs/README.md"] + assert upgrade["backups"] + assert upgrade["warnings"][0].startswith("WARNING") + + verify = rn.summarize_run_log( + action="verify", + log=( + " ok Home: sdlc-spdd/\n" + " fail Cursor commands: .cursor/commands/sdlc-spdd-init.md\n" + "Summary: 1/2 checks passed\n" + "Install verification failed (1 missing or invalid items).\n" + ), + ok=False, + exit_code=1, + ) + assert verify["check_ok_count"] == 1 + assert verify["check_fail_count"] == 1 + assert "1/2 checks passed" in verify["checks_summary"] + assert "verification failed" in verify["headline"].lower() + + def test_runner_flags_and_engine_install( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: From 13b141ef051e3a7606c3da51d7d55a744b801354 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 01:20:00 +0000 Subject: [PATCH 2/2] Wait for ADF viewer process alive in Playwright The status probe always includes a url, so waiting on 'url http' returned before Start finished and flaked the alive assertion. Co-authored-by: John Menke --- engine/tests_e2e/test_vue3_console_playwright.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/tests_e2e/test_vue3_console_playwright.py b/engine/tests_e2e/test_vue3_console_playwright.py index 1220ba1..67dde79 100644 --- a/engine/tests_e2e/test_vue3_console_playwright.py +++ b/engine/tests_e2e/test_vue3_console_playwright.py @@ -880,7 +880,7 @@ def test_vue3_adf_start_status_stop(page, live_vue_console) -> None: # type: ig page.wait_for_function( """() => { const meta = document.querySelector('[data-testid="adf-meta"]')?.textContent || ''; - return meta.includes('process alive') || meta.includes('url http'); + return meta.includes('process alive'); }""" ) assert live_vue_console["state"]["alive"] is True