From 00c4041cfc476995ecbe19fe961886260442904b Mon Sep 17 00:00:00 2001 From: HKX BDDEV001 Date: Thu, 18 Jun 2026 05:53:06 +0900 Subject: [PATCH 1/9] feat: Update README and ABAP utility to enhance selection-screen labels for runtime deployment --- .../sap-dev-core/skills/sap-update-addon/README.md | 8 +++++++- .../references/ZCMRUPDATE_ADDON_TABLE.abap | 12 ++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/plugins/sap-dev-core/skills/sap-update-addon/README.md b/plugins/sap-dev-core/skills/sap-update-addon/README.md index 3ff84e1..93283f1 100644 --- a/plugins/sap-dev-core/skills/sap-update-addon/README.md +++ b/plugins/sap-dev-core/skills/sap-update-addon/README.md @@ -62,10 +62,16 @@ Conversational forms: both releases (the old "S/4 = btn[18]" assumption was wrong) — and is live-verified end-to-end on both. The SM30 path still needs a maintenance view; SE16 *DELETE* is a stub on all releases (use SM30 or delete manually). +- **PROG selection-screen labels:** `ZCMRUPDATE_ADDON_TABLE` assigns its + selection texts (アップロード / ダウンロード / テーブル名 / ファイルパス) at + runtime in `INITIALIZATION` via the release-independent `%__%_app_%-text` + fields, because the program is deployed **source-only** (no text-pool upload). + Without that the screen renders the raw technical names (`RB_UP` / `RB_DOWN` / + `P_TABLE` / `P_FILE`). ## Version -- Skill Version: 1.1.0 +- Skill Version: 1.1.1 - Last Updated: 2026-06-17 ## License diff --git a/plugins/sap-dev-core/skills/sap-update-addon/references/ZCMRUPDATE_ADDON_TABLE.abap b/plugins/sap-dev-core/skills/sap-update-addon/references/ZCMRUPDATE_ADDON_TABLE.abap index db17a88..634a019 100644 --- a/plugins/sap-dev-core/skills/sap-update-addon/references/ZCMRUPDATE_ADDON_TABLE.abap +++ b/plugins/sap-dev-core/skills/sap-update-addon/references/ZCMRUPDATE_ADDON_TABLE.abap @@ -44,6 +44,18 @@ SELECTION-SCREEN END OF BLOCK b_param. INITIALIZATION. gv_tit1 = '処理モード'. gv_tit2 = 'パラメータ'. +* Selection texts are assigned at RUNTIME rather than maintained in the text +* pool, because this program is deployed SOURCE-ONLY (/sap-dev-init Step 8 + +* the /sap-update-addon PROG fallback) with NO text-pool upload. Without these +* the selection screen shows the raw technical names (RB_UP / RB_DOWN / +* P_TABLE / P_FILE) instead of readable labels. The %__%_app_%-text +* fields are implicitly provided by the selection-screen processor and are +* release-independent (activate cleanly on ECC 6.0 ~7.40 and S/4HANA alike) — +* do NOT declare them with DATA. + %_rb_up_%_app_%-text = 'アップロード'. + %_rb_down_%_app_%-text = 'ダウンロード'. + %_p_table_%_app_%-text = 'テーブル名'. + %_p_file_%_app_%-text = 'ファイルパス'. *&---------------------------------------------------------------------* *& F4 Help for File Path From cc30fd2e1d7bc8a9f4e11c23da024b955fb0edde Mon Sep 17 00:00:00 2001 From: HKX BDDEV001 Date: Fri, 19 Jun 2026 22:48:16 +0900 Subject: [PATCH 2/9] feat: Update date handling in scripts and documentation to use 8-digit YYYYMMDD format for SAP DATS fields --- .../scripts/sap_se11_post_activate_verify.vbs | 19 +++++++++++++++---- .../references/sap_atc_drill_findings.vbs | 5 ++++- .../references/sap_atc_get_results.vbs | 5 ++++- plugins/sap-dev-core/skills/sap-se01/SKILL.md | 4 +++- .../sap-se01/references/sap_se01_create.vbs | 10 ++++++++-- .../sap-dev-core/skills/sap-se16n/README.md | 7 +++++-- .../sap-dev-core/skills/sap-se16n/SKILL.md | 8 +++++--- 7 files changed, 44 insertions(+), 14 deletions(-) diff --git a/plugins/sap-dev-core/shared/scripts/sap_se11_post_activate_verify.vbs b/plugins/sap-dev-core/shared/scripts/sap_se11_post_activate_verify.vbs index 8724984..ac49b0e 100644 --- a/plugins/sap-dev-core/shared/scripts/sap_se11_post_activate_verify.vbs +++ b/plugins/sap-dev-core/shared/scripts/sap_se11_post_activate_verify.vbs @@ -43,10 +43,21 @@ Function RunPostActivateVerify(sPs1Path, sObjType, sObjName) If sPs1Path = "" Then RunPostActivateVerify = "SKIP" : Exit Function If sPs1Path = PaVerifySentinelPs1() Then RunPostActivateVerify = "SKIP" : Exit Function - ' Run hidden (window style 0). powershell.exe is on PATH; using the full - ' path would tie us to 64-bit vs 32-bit but the verify is .NET RFC and - ' Connect-SapRfc handles GAC discovery either way. - sCmd = "powershell.exe -ExecutionPolicy Bypass -NoProfile -File """ & _ + ' NCo 3.1 is registered ONLY in the 32-bit GAC, so the verify PS1 MUST run + ' under 32-bit PowerShell. A bare "powershell.exe" inherits the launching + ' cscript's bitness via WOW64 redirection: under a 64-bit cscript it spawns + ' 64-bit PowerShell, where Add-Type on the 32-bit sapnco.dll throws + ' BadImageFormatException, Connect-SapRfc returns no destination, and the + ' generic "no destination" masks the real cause (verified 2026-06-19 on + ' S/4HANA 754). Pin the literal SysWOW64 (32-bit) PowerShell -- the literal + ' path is NOT WOW64-redirected, so it resolves to 32-bit from either parent + ' bitness. Fall back to bare powershell.exe only on a 32-bit-only Windows + ' (no SysWOW64), where System32 PowerShell is already 32-bit. + Dim oFsoPs, sPsExe + Set oFsoPs = CreateObject("Scripting.FileSystemObject") + sPsExe = oFsoPs.BuildPath(oFsoPs.GetSpecialFolder(0), "SysWOW64\WindowsPowerShell\v1.0\powershell.exe") + If Not oFsoPs.FileExists(sPsExe) Then sPsExe = "powershell.exe" + sCmd = """" & sPsExe & """ -ExecutionPolicy Bypass -NoProfile -File """ & _ sPs1Path & """ -ObjectType " & sObjType & _ " -ObjectName """ & UCase(sObjName) & """" Set oShell = CreateObject("WScript.Shell") diff --git a/plugins/sap-dev-core/skills/sap-atc/references/sap_atc_drill_findings.vbs b/plugins/sap-dev-core/skills/sap-atc/references/sap_atc_drill_findings.vbs index dbc34d4..93448b4 100644 --- a/plugins/sap-dev-core/skills/sap-atc/references/sap_atc_drill_findings.vbs +++ b/plugins/sap-dev-core/skills/sap-atc/references/sap_atc_drill_findings.vbs @@ -98,7 +98,10 @@ If sScrNum = "1000" Then On Error Resume Next oSess.findById("wnd[0]/usr/ctxtS_RUNSR-LOW").Text = UCase(RUN_SERIES_NAME) Err.Clear - Dim sToday : sToday = Year(Date) & "." & Right("0" & Month(Date), 2) & "." & Right("0" & Day(Date), 2) + ' YYYYMMDD = locale-independent date input: SAP DATS fields accept an 8-digit + ' all-numeric value for any USR01-DATFM. A separator form (e.g. YYYY.MM.DD) is + ' only valid for the matching DATFM and otherwise rejected. Fix 2026-06-19. + Dim sToday : sToday = Year(Date) & Right("0" & Month(Date), 2) & Right("0" & Day(Date), 2) oSess.findById("wnd[0]/usr/ctxtS_SDLON-HIGH").Text = sToday Err.Clear oSess.findById("wnd[0]").sendVKey VKEY_F8 diff --git a/plugins/sap-dev-core/skills/sap-atc/references/sap_atc_get_results.vbs b/plugins/sap-dev-core/skills/sap-atc/references/sap_atc_get_results.vbs index bcd033a..0f54cdf 100644 --- a/plugins/sap-dev-core/skills/sap-atc/references/sap_atc_get_results.vbs +++ b/plugins/sap-dev-core/skills/sap-atc/references/sap_atc_get_results.vbs @@ -100,7 +100,10 @@ If sScrNum2 = "1000" Then ' Push the high-end of the started-on date range to today, so a run ' scheduled today is included even if S_RUNSR-LOW couldn't be set. On Error Resume Next - Dim sToday : sToday = Year(Date) & "." & Right("0" & Month(Date), 2) & "." & Right("0" & Day(Date), 2) + ' YYYYMMDD = locale-independent date input: SAP DATS fields accept an 8-digit + ' all-numeric value for any USR01-DATFM. A separator form (e.g. YYYY.MM.DD) is + ' only valid for the matching DATFM and otherwise rejected. Fix 2026-06-19. + Dim sToday : sToday = Year(Date) & Right("0" & Month(Date), 2) & Right("0" & Day(Date), 2) oSess.findById("wnd[0]/usr/ctxtS_SDLON-HIGH").Text = sToday Err.Clear On Error GoTo 0 diff --git a/plugins/sap-dev-core/skills/sap-se01/SKILL.md b/plugins/sap-dev-core/skills/sap-se01/SKILL.md index 526a040..2ae9d0c 100644 --- a/plugins/sap-dev-core/skills/sap-se01/SKILL.md +++ b/plugins/sap-dev-core/skills/sap-se01/SKILL.md @@ -232,7 +232,9 @@ following lookup itself (no `/sap-se16n` call required): technical field name and is locale-independent.) 3. Write `LOW` value (`ctxtGS_SELFIELDS-LOW[2,r]`) for the matched rows: - AS4USER row → `oSess.Info.User` (uppercased) - - AS4DATE row → workstation today as `YYYY.MM.DD` + - AS4DATE row → workstation today as 8-digit `YYYYMMDD` (locale-independent — + SAP DATS fields accept it for any `USR01-DATFM`; a separator form like + `YYYY.MM.DD` only works when it matches the user's date personalization) 4. Press F8 (`tbar[1]/btn[8]`) to execute. Dismiss any post-execute popup with Enter. 5. On the result grid (`wnd[0]/usr/cntlRESULT_LIST/shellcont/shell`): diff --git a/plugins/sap-dev-core/skills/sap-se01/references/sap_se01_create.vbs b/plugins/sap-dev-core/skills/sap-se01/references/sap_se01_create.vbs index 1059e87..2075cee 100644 --- a/plugins/sap-dev-core/skills/sap-se01/references/sap_se01_create.vbs +++ b/plugins/sap-dev-core/skills/sap-se01/references/sap_se01_create.vbs @@ -10,7 +10,7 @@ ' ' After creating the request the script then resolves the new TRKORR by ' navigating to SE16N on table E070, filtering by AS4USER = current SAP user -' and AS4DATE = workstation today (formatted YYYY.MM.DD), executing the query, +' and AS4DATE = workstation today (formatted YYYYMMDD), executing the query, ' sorting the result grid by AS4TIME descending, and reading the TRKORR of the ' first row whose TRFUNCTION is "K" (Workbench) or "W" (Customizing). ' @@ -110,8 +110,14 @@ WScript.Echo "INFO: AS4USER=" & oSess.Info.User ' TRFUNCTION K/W to filter top-level requests directly. Dim sUser : sUser = UCase(oSess.Info.User) +' AS4DATE filter: use the 8-digit YYYYMMDD form. SAP date fields interpret an +' all-numeric 8-char entry as YYYYMMDD regardless of the logon user's date +' personalization (USR01-DATFM), so it is locale-independent. A separator-bearing +' form (e.g. YYYY.MM.DD) is only valid for the matching DATFM and otherwise yields +' status-bar "Invalid date format" with no result grid (the pre-2026-06-19 bug; +' verified 2026-06-19 on S/4HANA 754 with a YYYY/MM/DD user). Dim sToday -sToday = Year(Now) & "." & Right("0" & Month(Now), 2) & "." & Right("0" & Day(Now), 2) +sToday = Year(Now) & Right("0" & Month(Now), 2) & Right("0" & Day(Now), 2) WScript.Echo "INFO: SE16N lookup user=" & sUser & " date=" & sToday oSess.findById("wnd[0]/tbar[0]/okcd").text = "/nse16n" diff --git a/plugins/sap-dev-core/skills/sap-se16n/README.md b/plugins/sap-dev-core/skills/sap-se16n/README.md index 88e0382..1d0e5ed 100644 --- a/plugins/sap-dev-core/skills/sap-se16n/README.md +++ b/plugins/sap-dev-core/skills/sap-se16n/README.md @@ -60,7 +60,7 @@ This skill activates when the user says: /sap-se16n T001 /sap-se16n T001 where LAND1 in CN,JP and WAERS = CNY /sap-se16n MARA where MATKL = 0001 select MATNR,MTART,MATKL,MEINS -/sap-se16n VBAK where ERDAT between 2024.01.01 and 2024.12.31 +/sap-se16n VBAK where ERDAT between 20240101 and 20241231 ``` Conversational forms: @@ -99,7 +99,10 @@ currency is CNY). `ROWS=` printed on the last line; output written to - Output column order follows SAP's natural ordering, not the SELECT list order - Cluster / pooled tables that SE16N rejects produce `NO_DATA` with the SAP status text — pass this back to the user verbatim -- Date and numeric literals must be in SAP internal format (`YYYY.MM.DD`) +- Date literals must be 8-digit `YYYYMMDD` (e.g. `20240131`) — SAP DATS fields + accept this for any user date format (`USR01-DATFM`), so it is locale-independent; + a separator form like `YYYY.MM.DD` only works when it matches the user's DATFM. + Numeric literals must have no thousand separators - Only the first 8 values per multi-select are guaranteed visible without the popup needing scrolling; the VBS scrolls automatically beyond that diff --git a/plugins/sap-dev-core/skills/sap-se16n/SKILL.md b/plugins/sap-dev-core/skills/sap-se16n/SKILL.md index da9ee31..98cb9cc 100644 --- a/plugins/sap-dev-core/skills/sap-se16n/SKILL.md +++ b/plugins/sap-dev-core/skills/sap-se16n/SKILL.md @@ -329,9 +329,11 @@ runtime and matches `Tab` or `preadsheet` substrings, then falls back to - Pooled / cluster tables that SE16N forbids reading return `NO_DATA` with a message like "Display of cluster table not allowed" — surface this to the user. -- Date / numeric values must be passed in SAP internal format - (`YYYY.MM.DD`, no thousand separators) unless the user has set their personal - display format to match. +- Date values must be passed as 8-digit `YYYYMMDD` (e.g. `20240131`) — SAP DATS + fields accept this regardless of the logon user's date personalization + (`USR01-DATFM`), so it is locale-independent. Do NOT use a separator form such + as `YYYY.MM.DD`, which is only accepted when it matches the user's DATFM. + Numeric values must have no thousand separators. - Some long-text / non-selectable fields (e.g. `AS4TEXT` on `E070`, `STEXT` on many tables) cannot be filtered via SE16N at all — SAP renders the row greyed-out (no operator, no input, no multi-select). The skill detects this From 16f0da8a705f90ad8d0def3abb43a4ff58b2473d Mon Sep 17 00:00:00 2001 From: HKX BDDEV001 Date: Sat, 20 Jun 2026 00:17:09 +0900 Subject: [PATCH 3/9] Refactor SAP skills to implement run-scoped temporary isolation - Updated `sap-transport-request` and `sap-update-addon` skills to utilize a new `RUN_TEMP` directory for per-run scratch files, ensuring concurrent executions do not collide. - Modified logging and file writing paths to reflect the new `RUN_TEMP` structure. - Added checks in `check-consistency.mjs` to warn about improper usage of `RUN_TEMP` and ensure skills are migrated correctly. - Introduced `sap_run_with_lock.ps1` to manage OS-level mutex for critical sections, preventing clipboard and foreground conflicts during GUI operations. - Created `test-run-temp-helpers.ps1` for offline testing of run-scoped temp isolation helpers and mutex functionality. --- CLAUDE.md | 19 ++- contributing/parallel_safe_session_attach.md | 13 +- .../shared/scripts/sap_connection_lib.ps1 | 70 ++++++++ .../shared/scripts/sap_run_with_lock.ps1 | 62 +++++++ .../skills/sap-activate-object/SKILL.md | 32 ++-- plugins/sap-dev-core/skills/sap-atc/SKILL.md | 38 +++-- .../skills/sap-change-package/SKILL.md | 28 +++- .../sap-dev-core/skills/sap-dev-init/SKILL.md | 34 +++- .../skills/sap-function-group/SKILL.md | 44 +++-- .../references/skill_md.template | 13 +- .../sap-dev-core/skills/sap-login/SKILL.md | 48 ++++-- .../skills/sap-run-abap-unit/SKILL.md | 22 ++- plugins/sap-dev-core/skills/sap-se01/SKILL.md | 36 ++-- plugins/sap-dev-core/skills/sap-se11/SKILL.md | 108 ++++++------ .../sap-dev-core/skills/sap-se16n/SKILL.md | 40 +++-- plugins/sap-dev-core/skills/sap-se21/SKILL.md | 46 ++++-- plugins/sap-dev-core/skills/sap-se24/SKILL.md | 108 ++++++------ plugins/sap-dev-core/skills/sap-se37/SKILL.md | 120 +++++++------- plugins/sap-dev-core/skills/sap-se38/SKILL.md | 154 ++++++++++-------- plugins/sap-dev-core/skills/sap-se91/SKILL.md | 64 +++++--- .../skills/sap-transport-request/SKILL.md | 33 ++-- .../skills/sap-update-addon/SKILL.md | 56 ++++--- scripts/check-consistency.mjs | 27 +++ scripts/test-run-temp-helpers.ps1 | 115 +++++++++++++ 24 files changed, 910 insertions(+), 420 deletions(-) create mode 100644 plugins/sap-dev-core/shared/scripts/sap_run_with_lock.ps1 create mode 100644 scripts/test-run-temp-helpers.ps1 diff --git a/CLAUDE.md b/CLAUDE.md index 98a2599..efa15c8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -272,7 +272,7 @@ Cross-plugin shared files live at `plugins/sap-dev-core/shared/`. All other plug | `shared/scripts/sap_gui_security_sidecar.ps1` | **`/sap-dev-init` Step 1b** + any skill that triggers a local-file IO via SAP GUI (sap-se16n / sap-se38 / sap-se37 / sap-se24 / sap-se11 / sap-sp02 / sap-atc) | OS-level (**Win32**) auto-dismiss for the SAP GUI Security dialog. Runs in a separate process because the Scripting COM API is fully suspended while the dialog is modal. The dialog is a standard `#32770` window titled "SAP GUI Security", **owned** by saplogon — invisible to `FindWindow`-exact (owned) and to UI Automation (SAP GUI doesn't expose it to UIA, which is why the **older UIA sidecar found nothing → silent TIMEOUT**). Detects it via `EnumWindows` (caption match, OR the locale-proof structural test "has both an Allow and a Deny child Button"), then ticks **Remember My Decision** (`SendMessage BM_SETCHECK`) + clicks **Allow** (`SendMessage BM_CLICK`) via `EnumChildWindows` — no focus/foreground dependency. Ticking Remember persists an Allow rule into `saprules.xml` live (no GUI restart). **Dismisses EVERY dialog that appears within its window — not just the first — verifying each one actually closed** (a dismissed `#32770` goes `IsWindowVisible`-false): a first-time source upload raises **two** dialogs in quick succession, so the pre-2026-06-17 single-shot loop (it `exit 0`'d on the first `BM_CLICK` without verifying) left the second one hanging the cscript for minutes; a no-op early click had the same effect. Args: `-TimeoutSeconds 30 [-PollIntervalMs 200] [-LogPath ]`. Stdout last line: `DISMISSED:WIN32` (≥1 closed; a preceding `INFO: closed N security dialog(s)` line gives the count) / `FOUND_BUT_STUCK` (dialog seen but the click never closed it — caller must surface, not assume OK) / `TIMEOUT` / `NO_SAP_GUI` / `ERROR: `. Launch via `Start-Process powershell -PassThru -WindowStyle Hidden -RedirectStandardOutput ` BEFORE the dialog-triggering action; then `Wait-Process` and check the captured verdict for `FOUND_BUT_STUCK`. Full process contract: `shared/rules/sap_gui_security_handling.md`. | | `shared/scripts/sap_gui_security_warmup.vbs` | `/sap-dev-init` Step 1b only | One-shot trigger that drives `oWnd.Hardcopy ` under `{work_dir}` to materialize the SAP GUI Security dialog. Tokens: `%%PROBE_FILE%%`. Stdout last line: `ALLOWED` / `NO_GUI` / `ERROR: `. Run in foreground; the **PowerShell sidecar** (above) runs in parallel to dismiss the dialog at the OS level. The Hardcopy call blocks until the sidecar acts. NOTE: Hardcopy is a **write**, so the warmup only ever persists a `w` rule — it does NOT cover reads (`GUI_UPLOAD`). Read coverage is handled by `sap_gui_security_grant.ps1` (below). | | `shared/scripts/sap_gui_security_grant.ps1` | `/sap-dev-init` Step 1b ("Cover read access" sub-step) + any skill that needs to pre-trust read/write of a directory for **arbitrary programs** | Idempotently merges one well-formed `` Allow rule into `%APPDATA%\SAP\Common\saprules.xml`, in SAP's native serialization (forward-slash path, rule-level + context-level ``/``, action 0 = Allow). Exists because SAP keys "Remember" rules on the per-program **dynpro**, so neither the warmup (write-only) nor the watcher (narrow per-context) can pre-cover reads from newly generated programs. Args: `-Path -Access [-AsDirectory] [-System ''] [-Client ''] [-Transaction ''] [-DynproName ''] [-DynproNum ''] [-RulesFile ]`. Empty context field = "any" (mirrors SAP's always-empty ``). For the operator's own `{work_dir}` sandbox the intended scope is **any-system** (empty `-System`/`-Client`, plus empty txn/program); pin `-System`/`-Client` only for a least-privilege policy. Any-system grants are a Security-Weaken action the auto-mode classifier guards, so they need explicit operator authorization on first write (idempotent `ALREADY` thereafter). **Context-aware self-heal**: idempotency keys on path **and** an effective any-context shape, so a same-path rule that is malformed (literal `*` contexts or backslash path — SAP silently ignores both) or narrow (per-program context) is purged and replaced rather than mistaken for coverage (the bug that made a single stale `*` rule return `ALREADY` forever while the dialog kept appearing). Minimal textual edit before the final `` — only stale same-path single-name rules are removed; the rest of the file is byte-preserved, UTF-8 **no BOM**, post-write `[xml]` sanity re-parse. Caller backs up `saprules.xml` first. Stdout last line: `GRANTED: id= …` (new) / `HEALED: … removed=` (stale same-path rule replaced) / `ALREADY: …` (exit 0) / `ERROR: ` (exit 2). Reload caveat: a SAP Logon already running must restart to pick up the externally-written rule (so after GRANTED/HEALED, restart SAP Logon once — permanent thereafter). | -| `shared/scripts/sap_gui_foreground_guard.ps1` | **All GUI-scripting VBS reference scripts that paste via SendKeys (mandatory — `sap-se38`, `sap-se37`, `sap-se24`, `sap-se91`, future paste-based skills)** | OS-level foreground forcer. Brings the SAP GUI main window to the front so SendKeys lands in SAP, not in whatever app the user is editing in (Notepad, VS Code, browser, Outlook). Uses the `AttachThreadInput` Win32 trick to bypass Windows 7+'s SetForegroundWindow suppression — which is why `WshShell.AppActivate` alone fails reliably even in a 20-retry loop (it returns success while Windows just flashes the taskbar button). Token: `%%FOREGROUND_GUARD_PS1%%`. Args: `-TargetTitle [-TimeoutSeconds 5] [-PollIntervalMs 100] [-LogPath ]`. Stdout last line: `FOREGROUND:OK:` (exit 0, safe to SendKeys) / `FOREGROUND:STILL_NOT_FG:` (exit 1) / `FOREGROUND:NO_MATCH` (exit 1) / `FOREGROUND:NO_SAP_GUI` (exit 1) / `FOREGROUND:ERROR:` (exit 1). Caller convention: run synchronously via `oWshSend.Run(, 0, True)` just BEFORE the SendKeys block; on non-zero exit, ABORT the paste with a clear error rather than risking the source landing in the user's editor. Pair with `sap_session_lock.vbs` (which locks SAP-side input but does nothing for OS-level focus). | +| `shared/scripts/sap_gui_foreground_guard.ps1` | **All GUI-scripting VBS reference scripts that paste via SendKeys (mandatory — `sap-se38`; this is the ONLY skill that still pastes via clipboard+SendKeys today — `sap-se37`/`sap-se24` upload source via `ctxtDY_FILENAME` GUI file-IO and `sap-se91` writes via the `.Text` API, so they need neither the foreground guard nor the paste mutex; any future paste-based skill)** | OS-level foreground forcer. Brings the SAP GUI main window to the front so SendKeys lands in SAP, not in whatever app the user is editing in (Notepad, VS Code, browser, Outlook). Uses the `AttachThreadInput` Win32 trick to bypass Windows 7+'s SetForegroundWindow suppression — which is why `WshShell.AppActivate` alone fails reliably even in a 20-retry loop (it returns success while Windows just flashes the taskbar button). Token: `%%FOREGROUND_GUARD_PS1%%`. Args: `-TargetTitle [-TimeoutSeconds 5] [-PollIntervalMs 100] [-LogPath ]`. Stdout last line: `FOREGROUND:OK:` (exit 0, safe to SendKeys) / `FOREGROUND:STILL_NOT_FG:` (exit 1) / `FOREGROUND:NO_MATCH` (exit 1) / `FOREGROUND:NO_SAP_GUI` (exit 1) / `FOREGROUND:ERROR:` (exit 1). Caller convention: run synchronously via `oWshSend.Run(, 0, True)` just BEFORE the SendKeys block; on non-zero exit, ABORT the paste with a clear error rather than risking the source landing in the user's editor. Pair with `sap_session_lock.vbs` (which locks SAP-side input but does nothing for OS-level focus). | | `shared/rules/settings_lookup.md` | **ALL skills that read or write a userConfig value (mandatory — Rule 7)** | Two-file model — schema in `settings.json` (tracked, blank values), per-developer overrides in `settings.local.json` (gitignored). Reads merge per-key on the `value` field; writes always target the local file. Path resolution from sap-dev-core skills vs. cross-plugin skills. Implementation paths for PowerShell (`sap_settings_lib.ps1`) and Claude-driven Read/Edit-tool flows. | | `shared/rules/skill_operating_rules.md` | **ALL skills (mandatory)** | Forbids direct SQL writes on SAP standard tables; forbids unsolicited program/report deployment | | `shared/rules/tr_resolution.md` | **All deploy skills (mandatory)** + `/sap-transport-request` + `/sap-se01` | Transport request resolution flow — `way_to_get_transport_request` (DEFAULT/ASK/CREATE_NEW), `rule_of_tr_description` (ASK/PATTERN/FIXED/RANDOM), 60-char compression | @@ -440,6 +440,14 @@ across updates. `work_dir` is the bootstrap pointer and is therefore NOT read from `userconfig.json` (which lives under it). Temp files go to `{work_dir}\temp` (referenced as `{WORK_TEMP}` in SKILL.md files). +Each skill invocation ALSO mints a fresh per-run scratch subdir +`{work_dir}\temp\run_` via `Get-SapRunTemp` (referenced as `{RUN_TEMP}`); the +skill writes its OWN generated wrappers / `_run.json` state / scratch there so +concurrent runs (parallel sub-agents, multi-connection deploys) never collide on +fixed names. `{WORK_TEMP}` stays the **base** dir, used for the session broker and +`Get-SapCurrentSessionPath -WorkTemp` (which derive `{work_dir}\runtime` from its +parent — passing them the run dir would relocate the registry). See +`shared/scripts/sap_connection_lib.ps1` (`Get-SapRunTemp` / `Remove-SapStaleRunTemp`). Every skill includes a **Step 0 — Resolve Work Directory**. It MUST resolve `work_dir` via `Get-SapWorkDir` (which applies the env-var → settings.local → @@ -451,7 +459,11 @@ one-liner (parse the `WORK_DIR=` line from stdout): powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir)); Write-Output ('CUSTOM_URL=' + (Get-SapSettingValue 'custom_url' ((Get-SapWorkDir) + '\custom')))" ``` -Then create `{WORK_TEMP}` = `{work_dir}\temp` if needed. +Then create `{WORK_TEMP}` = `{work_dir}\temp` if needed, and set `{RUN_TEMP}` = +the `RUN_TEMP=` value (`Get-SapRunTemp` mints + creates `{work_dir}\temp\run_`). +Use `{RUN_TEMP}` for the skill's OWN scratch (generated `*_run.vbs/.ps1`, the +`_run.json` state file, scratch `.txt`); keep `{WORK_TEMP}` (base) only for +`Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'`. **Custom naming rules override:** Skills that use `abap_naming_rules.tsv` check `{custom_url}\abap_naming_rules.tsv` first. If found, the custom file is used instead of the default in `sap-dev-core/shared/tables/`. @@ -524,7 +536,8 @@ JSONL records have shape `{ts, run_id, parent_run_id, skill, phase=start|step|en |---|---| | `` | Absolute path to the current skill's directory | | `` | Absolute path to `sap-dev-core/shared/` — go 3 levels up from ``, then into `sap-dev-core\shared` | -| `{WORK_TEMP}` | `{work_dir}\temp` — resolved from sap-dev-core `settings.json` `work_dir` (default `C:\sap_dev_work\temp`) | +| `{WORK_TEMP}` | `{work_dir}\temp` (base, shared) — resolved from `work_dir` (default `C:\sap_dev_work\temp`). Use ONLY for the session broker + `Get-SapCurrentSessionPath -WorkTemp` | +| `{RUN_TEMP}` | `{work_dir}\temp\run_` — a fresh per-invocation scratch dir minted + created by `Get-SapRunTemp` (`sap_connection_lib.ps1`). Where each skill writes its OWN generated wrappers / `_run.json` state / scratch, isolating concurrent runs. Swept by `Remove-SapStaleRunTemp` | | `{custom_url}` | Custom overrides directory — resolved from sap-dev-core `settings.json` | | `` | **Deprecated** — use `` instead | diff --git a/contributing/parallel_safe_session_attach.md b/contributing/parallel_safe_session_attach.md index fb80341..02c65a8 100644 --- a/contributing/parallel_safe_session_attach.md +++ b/contributing/parallel_safe_session_attach.md @@ -111,9 +111,20 @@ $content = $content -replace '%%ATTACH_LIB_VBS%%','\scr . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap___run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap___run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) ``` +- **`{WORK_TEMP}` (base) vs `{RUN_TEMP}` (per-run) — do not confuse them.** + `Get-SapCurrentSessionPath -WorkTemp` MUST stay on the base `{WORK_TEMP}` + (`{work_dir}\temp`): it derives the durable runtime dir (`{work_dir}\runtime`, + home of `session_registry.json` + the AI-session pin) from the parent of the + path you pass, so a run-scoped path would silently relocate the broker registry + and break parallel-session coordination. The generated runtime `.vbs` and every + other scratch file the wrapper writes go to `{RUN_TEMP}` (`{work_dir}\temp\run_`, + minted once in Step 0 by `Get-SapRunTemp`) so two concurrent runs never clobber + each other's `*_run.vbs` between write and `cscript` exec. See CLAUDE.md + "Work Directory Configuration". + - `$sessionPath = ''` is intentional default — the helper auto-resolves via `SAPDEV_SESSION_PATH` → sole-connection → refuse. - `Get-SapCurrentSessionPath` reads `session_registry.json`'s `ai_sessions[].connection_id` for this AI session (parent-PID walk), finds the matching connection block, returns a usable session path on it. Empty string when nothing resolves; the attach lib's sole-connection fallback or "refuse" path then takes over. - If the SKILL.md is wrapping a write-class skill that also includes `%%SESSION_LOCK_VBS%%`, leave that substitution in — it's complementary, not redundant. diff --git a/plugins/sap-dev-core/shared/scripts/sap_connection_lib.ps1 b/plugins/sap-dev-core/shared/scripts/sap_connection_lib.ps1 index b9c7c2f..7cd67e2 100644 --- a/plugins/sap-dev-core/shared/scripts/sap_connection_lib.ps1 +++ b/plugins/sap-dev-core/shared/scripts/sap_connection_lib.ps1 @@ -106,6 +106,76 @@ function Get-SapWorkRuntimeDir { return $dir } +function Get-SapRunTemp { + # Per-invocation scratch directory: {work_dir}\temp\run_. + # + # Mints a FRESH unique subdirectory each call and creates it. Each skill + # invocation -- INCLUDING each parallel sub-agent -- calls this ONCE in its + # Step 0 and reuses the returned path for the rest of the run, so two + # concurrent runs never share a scratch file name. This is the fix for the + # fixed-name {WORK_TEMP} collision class: the generate-then-execute TOCTOU on + # *_run.vbs (a concurrent run clobbering the file between write and cscript + # exec -> wrong object deployed), the sap__run.json log-state clobber, + # and scratch-txt clobber. + # + # CONTRACT: call once, in Step 0; reuse the value. Do NOT re-mint mid-skill + # (a second call returns a DIFFERENT dir and would fork a write-then-read + # handoff such as se38's "Write {RUN_TEMP}\.abap then read it"). + # + # Unlike the broker / Get-SapCurrentSessionPath / Get-SapCurrentConnectionProfile + # family, this builds DOWN from {work_dir}\temp and NEVER does + # `Split-Path -Parent`, so it cannot disturb the durable runtime-dir + # derivation ({work_dir}\runtime, home of session_registry.json + the + # AI-session pin). Callers MUST keep `-WorkTemp` on the base {work_dir}\temp + # for those helpers; only the skill's OWN scratch goes under this run dir. + param([string]$WorkDir = '') + if ([string]::IsNullOrWhiteSpace($WorkDir)) { $WorkDir = Get-SapWorkDir } + $base = Join-Path $WorkDir 'temp' + if (-not (Test-Path $base)) { New-Item -ItemType Directory -Force -Path $base | Out-Null } + # Retry a few times so an old, not-yet-GC'd run dir with the same 8-hex + # token cannot be silently reused (which would re-introduce sharing). + for ($i = 0; $i -lt 20; $i++) { + $token = 'run_' + ([guid]::NewGuid().ToString('N').Substring(0, 8)) + $dir = Join-Path $base $token + if (-not (Test-Path $dir)) { + New-Item -ItemType Directory -Force -Path $dir | Out-Null + return $dir + } + } + # Astronomically unlikely (20 GUID collisions). Fall back to a full GUID. + $dir = Join-Path $base ('run_' + [guid]::NewGuid().ToString('N')) + New-Item -ItemType Directory -Force -Path $dir | Out-Null + return $dir +} + +function Remove-SapStaleRunTemp { + # Best-effort GC of orphaned per-run scratch dirs ({work_dir}\temp\run_*). + # A crashed or abandoned run never reaches its `rmdir {RUN_TEMP}` cleanup, so + # sweep run_* dirs whose last-write is older than -MaxAgeHours (default 24h). + # Intended for /sap-login Step 6 alongside the broker `gc`. Never throws; + # returns the count removed. + param( + [string]$WorkDir = '', + [int]$MaxAgeHours = 24 + ) + try { + if ([string]::IsNullOrWhiteSpace($WorkDir)) { $WorkDir = Get-SapWorkDir } + $base = Join-Path $WorkDir 'temp' + if (-not (Test-Path $base)) { return 0 } + $cutoff = (Get-Date).AddHours(-1 * [math]::Abs($MaxAgeHours)) + $removed = 0 + Get-ChildItem -LiteralPath $base -Directory -Filter 'run_*' -ErrorAction SilentlyContinue | ForEach-Object { + if ($_.LastWriteTime -lt $cutoff) { + try { + Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction Stop + $removed++ + } catch { } + } + } + return $removed + } catch { return 0 } +} + function Get-SapConnectionStorePath { if ($script:SapConnStore_PathCache) { return $script:SapConnStore_PathCache } $script:SapConnStore_PathCache = Join-Path (Get-SapWorkRuntimeDir) 'connections.json' diff --git a/plugins/sap-dev-core/shared/scripts/sap_run_with_lock.ps1 b/plugins/sap-dev-core/shared/scripts/sap_run_with_lock.ps1 new file mode 100644 index 0000000..74a8135 --- /dev/null +++ b/plugins/sap-dev-core/shared/scripts/sap_run_with_lock.ps1 @@ -0,0 +1,62 @@ +# sap_run_with_lock.ps1 -- run a command while holding a machine-global named mutex. +# +# Serializes a critical section that contends on an OS-global singleton which no +# per-run folder can isolate. The motivating case is SE38's source paste: it +# stages ABAP source on the Windows CLIPBOARD (Set-Clipboard) and pastes it with +# SendKeys ^v behind an OS-FOREGROUND guard. Both the clipboard and the +# foreground/focus owner are process-global, machine-wide singletons -- two +# concurrent SE38 pastes would cross-paste each other's source. The session +# broker + {RUN_TEMP} fix per-session and per-file collisions, but NOT these OS +# singletons; this wrapper does, by serializing the whole paste-driving cscript +# behind a named mutex. GUI deploys serialize here BY DESIGN (they share one +# foreground/clipboard, so they cannot truly run in parallel regardless). +# +# Usage: +# powershell -NoProfile -ExecutionPolicy Bypass -File sap_run_with_lock.ps1 ` +# -MutexName SapDevGuiPaste_v1 -TimeoutMs 180000 ` +# -Command "cscript //NoLogo ""C:\...\run\sap_se38_create_run.vbs""" +# +# - Stdout/stderr of the wrapped command pass through unchanged so the caller's +# parser (SUCCESS:/ERROR:/PROGDIR: lines, etc.) is unaffected. The lock's own +# diagnostics go to STDERR to keep stdout clean. +# - Exit code = the wrapped command's exit code, EXCEPT: +# 2 = could not acquire the mutex within -TimeoutMs (the command did NOT run). +# +# Mirrors the With-...Lock idiom from sap_connection_lib.ps1 / sap_session_broker.ps1: +# WaitOne(timeout), tolerate AbandonedMutexException (a crashed prior holder), and +# ReleaseMutex + Dispose in finally. + +[CmdletBinding()] +param( + [Parameter(Mandatory)][string]$MutexName, + [Parameter(Mandatory)][string]$Command, + [int]$TimeoutMs = 180000 +) + +$mutex = [System.Threading.Mutex]::new($false, $MutexName) +$acquired = $false +$exitCode = 0 +try { + try { + $acquired = $mutex.WaitOne($TimeoutMs) + } catch [System.Threading.AbandonedMutexException] { + # A prior holder crashed before releasing; we now own the mutex. The + # protected resource is the OS clipboard/foreground, which is + # self-correcting on the next paste, so it is safe to proceed. + $acquired = $true + } + if (-not $acquired) { + [Console]::Error.WriteLine("ERROR: sap_run_with_lock could not acquire mutex '$MutexName' within ${TimeoutMs}ms; command NOT run") + exit 2 + } + # Success path is silent so the wrapped command's stdout/stderr reach the + # caller's parser unpolluted; only the failure path above is loud. + # Run the command line as given. cmd /c lets the caller pass a full + # "cscript //NoLogo """" args" string with its own quoting intact. + & cmd /c $Command + $exitCode = $LASTEXITCODE +} finally { + if ($acquired) { try { $mutex.ReleaseMutex() } catch { } } + try { $mutex.Dispose() } catch { } +} +exit $exitCode diff --git a/plugins/sap-dev-core/skills/sap-activate-object/SKILL.md b/plugins/sap-dev-core/skills/sap-activate-object/SKILL.md index 767187b..685bf2c 100644 --- a/plugins/sap-dev-core/skills/sap-activate-object/SKILL.md +++ b/plugins/sap-dev-core/skills/sap-activate-object/SKILL.md @@ -36,7 +36,7 @@ Task: $ARGUMENTS **Resolve `work_dir` via the env-aware helper** — do NOT take `work_dir` from a direct `settings.json` read (that ignores the `SAPDEV_AI_WORK_DIR` env var and `userconfig.json`). Use the `WORK_DIR=` value printed by: ```bash -powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir))" +powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir)); Write-Output ('RUN_TEMP=' + (Get-SapRunTemp))" ``` The settings note below still applies to the OTHER keys. @@ -52,6 +52,16 @@ Set `{WORK_TEMP}` = `{work_dir}\temp`. Ensure it exists: cmd /c if not exist "{WORK_TEMP}" mkdir "{WORK_TEMP}" ``` +Set `{RUN_TEMP}` = the `RUN_TEMP=` value printed above — a fresh per-run scratch +directory `{work_dir}\temp\run_`, already created by `Get-SapRunTemp`. +Resolve it **once here** and reuse the same value for the rest of this +invocation; it isolates this run's generated wrappers / state / scratch files so +concurrent runs (parallel sub-agents, multi-connection deploys) never collide. +**`{WORK_TEMP}` stays the base temp dir** and is used ONLY for +`Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'` (the session-attach plumbing +derives `{work_dir}\runtime` from its parent, so it must see the base path, not +the run dir). Everything the skill writes itself goes under `{RUN_TEMP}`. + `sap_user` is needed for the DWINACTIV pre/post checks. If blank, ask the user. @@ -66,10 +76,10 @@ helper silently no-ops. `` resolves to `plugins/sap-dev-core/shared/`. -State file: `{WORK_TEMP}\sap_activate_object_run.json` +State file: `{RUN_TEMP}\sap_activate_object_run.json` ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{WORK_TEMP}\sap_activate_object_run.json" -Skill sap-activate-object -ParamsJson "{\"object_type\":\"\",\"object_name\":\"\"}" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{RUN_TEMP}\sap_activate_object_run.json" -Skill sap-activate-object -ParamsJson "{\"object_type\":\"\",\"object_name\":\"\"}" ``` --- @@ -187,26 +197,26 @@ Pick the template by transaction: | SE24 | `./references/sap_activate_se24.vbs` | `%%OBJECT_NAME%%` | | SE11 | `./references/sap_activate_se11.vbs` | `%%OBJECT_NAME%%`, `%%OBJECT_TYPE%%`, `%%ACTIVATION_LOG_VBS%%`, `%%TEMP_DIR%%` | -Token-replace into `{WORK_TEMP}\sap_activate__run.ps1`: +Token-replace into `{RUN_TEMP}\sap_activate__run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_activate_.vbs', [System.Text.Encoding]::UTF8) $content = $content -replace '%%OBJECT_NAME%%','THE_NAME' $content = $content -replace '%%OBJECT_TYPE%%','THE_TYPE' # SE11 only $content = $content -replace '%%ACTIVATION_LOG_VBS%%','\scripts\sap_activation_log.vbs' # SE11 only -$content = $content -replace '%%TEMP_DIR%%','{WORK_TEMP}' # SE11 only +$content = $content -replace '%%TEMP_DIR%%','{RUN_TEMP}' # SE11 only # Phase 3.5 session-attach plumbing. $sessionPath = '' $content = $content -replace '%%SESSION_PATH%%', $sessionPath $content = $content -replace '%%ATTACH_LIB_VBS%%','\scripts\sap_attach_lib.vbs' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_activate__run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_activate__run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` > **Activation-log capture (SE11 only, by design)**: when SE11 activation > reports an error (`STATUS_TYPE = E` or `A`), the helper writes -> `{WORK_TEMP}\.activation_log.txt` and the script echoes +> `{RUN_TEMP}\.activation_log.txt` and the script echoes > `ACTIVATION_LOG: ` and `ACTIVATION_ERROR: `. This > turns the opaque "refer to log" SAP popup into actionable output. > @@ -220,8 +230,8 @@ Write-Host 'Done' Run via 32-bit cscript: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_activate__run.ps1" -C:/Windows/SysWOW64/cscript.exe //NoLogo {WORK_TEMP}\sap_activate__run.vbs +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_activate__run.ps1" +C:/Windows/SysWOW64/cscript.exe //NoLogo {RUN_TEMP}\sap_activate__run.vbs ``` Each VBS emits: @@ -316,13 +326,13 @@ Log the run-end record. Best-effort: silently no-ops if logging disabled. On success: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_activate_object_run.json" -Status SUCCESS -ExitCode 0 +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_activate_object_run.json" -Status SUCCESS -ExitCode 0 ``` On failure (substitute `` and short message): ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_activate_object_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_activate_object_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" ``` Suggested ``: `ACTIVATE_FAILED`, `GUI_TIMEOUT`. diff --git a/plugins/sap-dev-core/skills/sap-atc/SKILL.md b/plugins/sap-dev-core/skills/sap-atc/SKILL.md index 89a7b0a..4d44403 100644 --- a/plugins/sap-dev-core/skills/sap-atc/SKILL.md +++ b/plugins/sap-dev-core/skills/sap-atc/SKILL.md @@ -85,7 +85,7 @@ Final PASS / FAIL emitted with the findings.tsv path when available. **Resolve `work_dir` via the env-aware helper** — do NOT take `work_dir` from a direct `settings.json` read (that ignores the `SAPDEV_AI_WORK_DIR` env var and `userconfig.json`). Use the `WORK_DIR=` value printed by: ```bash -powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir))" +powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir)); Write-Output ('RUN_TEMP=' + (Get-SapRunTemp))" ``` The settings note below still applies to the OTHER keys. @@ -96,12 +96,22 @@ The settings note below still applies to the OTHER keys. cmd /c if not exist "{WORK_TEMP}" mkdir "{WORK_TEMP}" ``` +Set `{RUN_TEMP}` = the `RUN_TEMP=` value printed above — a fresh per-run scratch +directory `{work_dir}\temp\run_`, already created by `Get-SapRunTemp`. +Resolve it **once here** and reuse the same value for the rest of this +invocation; it isolates this run's generated wrappers / state / scratch files so +concurrent runs (parallel sub-agents, multi-connection deploys) never collide. +**`{WORK_TEMP}` stays the base temp dir** and is used ONLY for +`Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'` (the session-attach plumbing +derives `{work_dir}\runtime` from its parent, so it must see the base path, not +the run dir). Everything the skill writes itself goes under `{RUN_TEMP}`. + --- ## Step 0.5 — Start Logging ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{WORK_TEMP}\sap_atc_run.json" -Skill sap-atc -ParamsJson "{\"object_type\":\"\",\"object_name\":\"\"}" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{RUN_TEMP}\sap_atc_run.json" -Skill sap-atc -ParamsJson "{\"object_type\":\"\",\"object_name\":\"\"}" ``` --- @@ -130,7 +140,7 @@ If neither brief nor argument supplies a value, default to `2`. | `--run-series=` | no | auto | Run Series name. If omitted, generate `RUN__` to avoid collisions. | | `--poll-interval=` | no | `15` | Stage 3 polling cadence. | | `--max-wait=` | no | `600` (10 min) | Stage 3 timeout. | -| `--save-to=` | no | `{WORK_TEMP}\ATC_.txt` | Local path for the downloaded result TXT. | +| `--save-to=` | no | `{RUN_TEMP}\ATC_.txt` | Local path for the downloaded result TXT. | | `--drill` | no | (off by default; auto when gate FAILS) | Force Stage 4b — drill into the run-series row and export per-finding ALV as TSV to `.findings.tsv`. Use to see WHICH findings exist even when the gate passes (e.g. inspecting P3 informational findings). | | `--no-drill` | no | (off) | Disable Stage 4b even when the gate FAILS. Useful for CI runs where the operator only needs PASS/FAIL counts and will drill manually if needed. | @@ -183,7 +193,7 @@ $content = $content.Replace('%%SESSION_PATH%%', $sessionPath) $content = $content.Replace('%%ATTACH_LIB_VBS%%', "$shared\scripts\sap_attach_lib.vbs") . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_atc_stage1_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_atc_stage1_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) ``` > **Stages 2-4 use the same I/O pattern.** Always @@ -239,7 +249,7 @@ $content = $content.Replace('%%SESSION_PATH%%', $sessionPath) $content = $content.Replace('%%ATTACH_LIB_VBS%%', "$shared\scripts\sap_attach_lib.vbs") . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_atc_stage2_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_atc_stage2_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) ``` Run via cscript. Expected lines: @@ -272,12 +282,12 @@ $content = $content.Replace('%%SESSION_PATH%%', $sessionPath) $content = $content.Replace('%%ATTACH_LIB_VBS%%', "$shared\scripts\sap_attach_lib.vbs") . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_atc_stage3_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_atc_stage3_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) ``` Then loop: -1. Run `{WORK_TEMP}\sap_atc_stage3_run.vbs` via cscript. +1. Run `{RUN_TEMP}\sap_atc_stage3_run.vbs` via cscript. 2. Parse the last line: - `STATE=COMPLETED` → break, proceed to Stage 4. - `STATE=RUNNING` → wait `--poll-interval` seconds, retry. @@ -298,7 +308,7 @@ $pollInterval = 15 $maxWait = 600 $elapsed = 0 do { - $out = & cscript //NoLogo "{WORK_TEMP}\sap_atc_stage3_run.vbs" + $out = & cscript //NoLogo "{RUN_TEMP}\sap_atc_stage3_run.vbs" $state = ($out -match '^STATE=(.+)$') | Out-Null; $Matches[1] if ($state -eq 'COMPLETED') { break } if ($state -eq 'FAILED') { throw "ATC run failed" } @@ -330,7 +340,7 @@ $content = $content.Replace('%%SESSION_PATH%%', $sessionPath) $content = $content.Replace('%%ATTACH_LIB_VBS%%', "$shared\scripts\sap_attach_lib.vbs") . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_atc_stage4_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_atc_stage4_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) ``` **Run the Stage-4 VBS with the SAP GUI Security guard.** The result-file @@ -359,7 +369,7 @@ if (-not $allowed) { } # 3. Run the results read + download (32-bit cscript). If the dialog appears it # blocks here until the watcher dismisses it; then the download completes. -& 'C:/Windows/SysWOW64/cscript.exe' //NoLogo '{WORK_TEMP}\sap_atc_stage4_run.vbs' +& 'C:/Windows/SysWOW64/cscript.exe' //NoLogo '{RUN_TEMP}\sap_atc_stage4_run.vbs' # 4. Reap the watcher. if ($watcher) { $watcher | Wait-Process -Timeout 45 -ErrorAction SilentlyContinue } ``` @@ -431,7 +441,7 @@ $content = $content.Replace('%%SESSION_PATH%%', $sessionPath) $content = $content.Replace('%%ATTACH_LIB_VBS%%', "$shared\scripts\sap_attach_lib.vbs") . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_atc_stage4b_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_atc_stage4b_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) ``` **Run with the SAP GUI Security guard** — the findings-TSV export is SAP-GUI-side @@ -448,7 +458,7 @@ if (-not $allowed) { '-NoProfile','-ExecutionPolicy','Bypass','-File',"$shared\sap_gui_security_sidecar.ps1",'-TimeoutSeconds','40') Start-Sleep -Milliseconds 800 } -& 'C:/Windows/SysWOW64/cscript.exe' //NoLogo '{WORK_TEMP}\sap_atc_stage4b_run.vbs' +& 'C:/Windows/SysWOW64/cscript.exe' //NoLogo '{RUN_TEMP}\sap_atc_stage4b_run.vbs' if ($watcher) { $watcher | Wait-Process -Timeout 45 -ErrorAction SilentlyContinue } ``` @@ -547,7 +557,7 @@ ATC > Manage Results > screen. ## Step 8 — Clean Up ```bash -cmd /c del {WORK_TEMP}\sap_atc_stage1_run.vbs & del {WORK_TEMP}\sap_atc_stage2_run.vbs & del {WORK_TEMP}\sap_atc_stage3_run.vbs & del {WORK_TEMP}\sap_atc_stage4_run.vbs & del {WORK_TEMP}\sap_atc_stage4b_run.vbs +cmd /c del {RUN_TEMP}\sap_atc_stage1_run.vbs & del {RUN_TEMP}\sap_atc_stage2_run.vbs & del {RUN_TEMP}\sap_atc_stage3_run.vbs & del {RUN_TEMP}\sap_atc_stage4_run.vbs & del {RUN_TEMP}\sap_atc_stage4b_run.vbs ``` Keep the downloaded result TXT (`--save-to`) and, when Stage 4b ran, the @@ -558,7 +568,7 @@ findings TSV (`.findings.tsv`) — both are operator artefacts. ## Final — Log End ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_atc_run.json" -Status SUCCESS -ExitCode 0 -MetricsJson '{"gate":"ATC","verdict":"PASS","p1":0,"p2":0,"p3":0}' +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_atc_run.json" -Status SUCCESS -ExitCode 0 -MetricsJson '{"gate":"ATC","verdict":"PASS","p1":0,"p2":0,"p3":0}' ``` For gate FAIL, set `-Status FAILED -ExitCode 1 -ErrorClass ATC_GATE_FAIL` diff --git a/plugins/sap-dev-core/skills/sap-change-package/SKILL.md b/plugins/sap-dev-core/skills/sap-change-package/SKILL.md index 75a1730..79d2deb 100644 --- a/plugins/sap-dev-core/skills/sap-change-package/SKILL.md +++ b/plugins/sap-dev-core/skills/sap-change-package/SKILL.md @@ -38,7 +38,7 @@ Task: $ARGUMENTS **Resolve `work_dir` via the env-aware helper** — do NOT take `work_dir` from a direct `settings.json` read (that ignores the `SAPDEV_AI_WORK_DIR` env var and `userconfig.json`). Use the `WORK_DIR=` value printed by: ```bash -powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir))" +powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir)); Write-Output ('RUN_TEMP=' + (Get-SapRunTemp))" ``` The settings note below still applies to the OTHER keys. @@ -50,17 +50,27 @@ Set `{WORK_TEMP}` = `{work_dir}\temp`. Ensure it exists: cmd /c if not exist "{WORK_TEMP}" mkdir "{WORK_TEMP}" ``` +Set `{RUN_TEMP}` = the `RUN_TEMP=` value printed above — a fresh per-run scratch +directory `{work_dir}\temp\run_`, already created by `Get-SapRunTemp`. +Resolve it **once here** and reuse the same value for the rest of this +invocation; it isolates this run's generated wrappers / state / scratch files so +concurrent runs (parallel sub-agents, multi-connection deploys) never collide. +**`{WORK_TEMP}` stays the base temp dir** and is used ONLY for +`Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'` (the session-attach plumbing +derives `{work_dir}\runtime` from its parent, so it must see the base path, not +the run dir). Everything the skill writes itself goes under `{RUN_TEMP}`. + --- ## Step 0.5 — Start Logging Start a structured log run. The helper persists `run_id` in a state file -(`{WORK_TEMP}\sap_change_package_run.json`) so subsequent steps and the +(`{RUN_TEMP}\sap_change_package_run.json`) so subsequent steps and the final log-end call append to the same run. Best-effort: silently no-ops if `userConfig.log_enabled=false` or the lib can't load. ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{WORK_TEMP}\sap_change_package_run.json" -Skill sap-change-package -ParamsJson "{\"object_type\":\"\",\"object_name\":\"\",\"new_package\":\"\"}" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{RUN_TEMP}\sap_change_package_run.json" -Skill sap-change-package -ParamsJson "{\"object_type\":\"\",\"object_name\":\"\",\"new_package\":\"\"}" ``` --- @@ -234,7 +244,7 @@ Tokens: | `%%TRANSPORT%%` | `TMP_TO_TRANSPORT` mode (pre-resolved TR); empty otherwise | | `%%TR_DESCRIPTION%%` | `TMP_TO_TRANSPORT` mode (used if dialog asks for new-TR description) | -Token-replace into `{WORK_TEMP}\sap_change_package__run.ps1`: +Token-replace into `{RUN_TEMP}\sap_change_package__run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_change_package_.vbs', [System.Text.Encoding]::UTF8) $content = $content -replace '%%OBJECT_NAME%%','THE_NAME' @@ -248,14 +258,14 @@ $content = $content -replace '%%SESSION_PATH%%', $sessionPath $content = $content -replace '%%ATTACH_LIB_VBS%%','\scripts\sap_attach_lib.vbs' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_change_package__run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_change_package__run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Run via 32-bit cscript: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_change_package__run.ps1" -C:/Windows/SysWOW64/cscript.exe //NoLogo {WORK_TEMP}\sap_change_package__run.vbs +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_change_package__run.ps1" +C:/Windows/SysWOW64/cscript.exe //NoLogo {RUN_TEMP}\sap_change_package__run.vbs ``` Each VBS emits a stable contract: @@ -294,13 +304,13 @@ Log the run-end record. Best-effort: silently no-ops if logging disabled. On success: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_change_package_run.json" -Status SUCCESS -ExitCode 0 +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_change_package_run.json" -Status SUCCESS -ExitCode 0 ``` On failure (substitute `` and short message): ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_change_package_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_change_package_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" ``` Suggested ``: `CHANGE_PACKAGE_FAILED`, `OBJECT_LOCKED_IN_TR`, `TR_RESOLUTION_FAILED`, `GUI_TIMEOUT`. diff --git a/plugins/sap-dev-core/skills/sap-dev-init/SKILL.md b/plugins/sap-dev-core/skills/sap-dev-init/SKILL.md index 41b7c57..1348c68 100644 --- a/plugins/sap-dev-core/skills/sap-dev-init/SKILL.md +++ b/plugins/sap-dev-core/skills/sap-dev-init/SKILL.md @@ -65,6 +65,22 @@ Ensure the temp directory exists: cmd /c if not exist "{WORK_TEMP}" mkdir "{WORK_TEMP}" ``` +Set `{RUN_TEMP}` = the per-run scratch dir from `Get-SapRunTemp` (env bridge applied): + +```bash +powershell -NoProfile -ExecutionPolicy Bypass -Command "\$env:SAPDEV_AI_WORK_DIR='{work_dir}'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('RUN_TEMP=' + (Get-SapRunTemp))" +``` + +dev-init's OWN scratch (the `sap_dev_init_run.json` state file and the +`sap_gui_security_warmup_*.vbs` it generates) goes under `{RUN_TEMP}`. **The +cross-skill handoff files stay on `{WORK_TEMP}` (base), by design**: the `.def` / +`.abap` sources this skill copies for `/sap-se11`, `/sap-se37`, `/sap-se38` are +passed to those child skills as **absolute paths** and consumed there as +user-supplied files (the child never deletes them and writes its own scratch in +its own per-run dir). Keeping the handoffs on base avoids an rmdir-ordering hazard +— dev-init does **NOT** `rmdir {RUN_TEMP}`; its existing per-file cleanup and +`/sap-dev-clean` handle the handoff artifacts as before. + Validate `sap_dev_mode`. Allowed values: `GUI`, `RFC`, `BDC` (case-insensitive). Anything else → fall back to `GUI` and warn the user. ### Mode → fallback chain @@ -114,12 +130,12 @@ Plan: ## Step 0.5 — Start Logging Start a structured log run. The helper persists `run_id` in a state file -(`{WORK_TEMP}\sap_dev_init_run.json`) so subsequent steps and the final +(`{RUN_TEMP}\sap_dev_init_run.json`) so subsequent steps and the final log-end call append to the same run. Best-effort: silently no-ops if `userConfig.log_enabled=false` or the lib can't load. ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{WORK_TEMP}\sap_dev_init_run.json" -Skill sap-dev-init -ParamsJson "{}" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{RUN_TEMP}\sap_dev_init_run.json" -Skill sap-dev-init -ParamsJson "{}" ``` --- @@ -247,7 +263,7 @@ $warmupSrc = [System.IO.File]::ReadAllText('\scripts\sa # C:\sap_dev_work\sap_gui_warmup_20260511115106\.bmp # Plain .Replace() is literal and safe. $warmupSrc = $warmupSrc.Replace('%%PROBE_FILE%%', $probe) -[System.IO.File]::WriteAllText("{WORK_TEMP}\sap_gui_security_warmup_run.vbs", $warmupSrc, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText("{RUN_TEMP}\sap_gui_security_warmup_run.vbs", $warmupSrc, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Output 'Generated' ``` @@ -273,7 +289,7 @@ Start-Sleep -Milliseconds 800 # Foreground warmup. The Hardcopy call blocks until the sidecar dismisses # the dialog (or the customer clicks something manually). -& cscript.exe //NoLogo "{WORK_TEMP}\sap_gui_security_warmup_run.vbs" *> $warmupOut +& cscript.exe //NoLogo "{RUN_TEMP}\sap_gui_security_warmup_run.vbs" *> $warmupOut # Wait for the sidecar to exit (it exits as soon as it dismisses or times out). $sidecar | Wait-Process -Timeout 35 @@ -316,12 +332,12 @@ $probe2 = Join-Path "{work_dir}" ("sap_gui_warmup_verify_" + (Get-Date -Format y $warmupSrc2 = [System.IO.File]::ReadAllText('\scripts\sap_gui_security_warmup.vbs', [System.Text.Encoding]::UTF8) # .Replace() (literal), not -replace (regex). See the first warmup block for why. $warmupSrc2 = $warmupSrc2.Replace('%%PROBE_FILE%%', $probe2) -[System.IO.File]::WriteAllText("{WORK_TEMP}\sap_gui_security_warmup_verify.vbs", $warmupSrc2, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText("{RUN_TEMP}\sap_gui_security_warmup_verify.vbs", $warmupSrc2, [System.Text.UnicodeEncoding]::new($false, $true)) # No poller this time. If a dialog appears, this will hang for human input — # guard with a 5 s timeout (the second pass should complete in <1 s when trusted). $verify = Start-Process -FilePath "cscript.exe" ` - -ArgumentList "//NoLogo", "{WORK_TEMP}\sap_gui_security_warmup_verify.vbs" ` + -ArgumentList "//NoLogo", "{RUN_TEMP}\sap_gui_security_warmup_verify.vbs" ` -RedirectStandardOutput (Join-Path $env:TEMP "sap_gui_verify.out") ` -NoNewWindow -PassThru if (-not ($verify | Wait-Process -Timeout 5 -ErrorAction SilentlyContinue)) { @@ -355,7 +371,7 @@ If the verify step reports `DIALOG_STILL_APPEARS` or any branch reports `ERROR:` ### Cleanup ```bash -cmd /c del {WORK_TEMP}\sap_gui_security_warmup_run.vbs & del {WORK_TEMP}\sap_gui_security_warmup_verify.vbs +cmd /c del {RUN_TEMP}\sap_gui_security_warmup_run.vbs & del {RUN_TEMP}\sap_gui_security_warmup_verify.vbs ``` The sidecar log at `$env:TEMP\sap_gui_security_sidecar.log` is intentionally preserved for diagnostic purposes — delete manually if not investigating a failure. @@ -714,13 +730,13 @@ Log the run-end record. Best-effort: silently no-ops if logging disabled. On success: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_dev_init_run.json" -Status SUCCESS -ExitCode 0 +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_dev_init_run.json" -Status SUCCESS -ExitCode 0 ``` On failure (substitute `` and short message): ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_dev_init_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_dev_init_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" ``` Suggested ``: `DEV_INIT_FAILED`, `TR_RESOLUTION_FAILED`, `PACKAGE_FAILED`, `FUGR_FAILED`, `DEPLOY_FAILED`. diff --git a/plugins/sap-dev-core/skills/sap-function-group/SKILL.md b/plugins/sap-dev-core/skills/sap-function-group/SKILL.md index 0b2570a..8042214 100644 --- a/plugins/sap-dev-core/skills/sap-function-group/SKILL.md +++ b/plugins/sap-dev-core/skills/sap-function-group/SKILL.md @@ -57,7 +57,7 @@ priority over the fallback chain when set. **Resolve `work_dir` via the env-aware helper** — do NOT take `work_dir` from a direct `settings.json` read (that ignores the `SAPDEV_AI_WORK_DIR` env var and `userconfig.json`). Use the `WORK_DIR=` value printed by: ```bash -powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir))" +powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir)); Write-Output ('RUN_TEMP=' + (Get-SapRunTemp))" ``` The settings note below still applies to the OTHER keys. @@ -75,15 +75,25 @@ Set `{WORK_TEMP}` = `{work_dir}\temp`. Ensure it exists: cmd /c if not exist "{WORK_TEMP}" mkdir "{WORK_TEMP}" ``` +Set `{RUN_TEMP}` = the `RUN_TEMP=` value printed above — a fresh per-run scratch +directory `{work_dir}\temp\run_`, already created by `Get-SapRunTemp`. +Resolve it **once here** and reuse the same value for the rest of this +invocation; it isolates this run's generated wrappers / state / scratch files so +concurrent runs (parallel sub-agents, multi-connection deploys) never collide. +**`{WORK_TEMP}` stays the base temp dir** and is used ONLY for +`Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'` (the session-attach plumbing +derives `{work_dir}\runtime` from its parent, so it must see the base path, not +the run dir). Everything the skill writes itself goes under `{RUN_TEMP}`. + --- ## Step 0.5 — Start Logging ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{WORK_TEMP}\sap_function_group_run.json" -Skill sap-function-group -ParamsJson "{\"function_group\":\"\",\"mode\":\"\"}" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{RUN_TEMP}\sap_function_group_run.json" -Skill sap-function-group -ParamsJson "{\"function_group\":\"\",\"mode\":\"\"}" ``` -State file: `{WORK_TEMP}\sap_function_group_run.json`. Best-effort. +State file: `{RUN_TEMP}\sap_function_group_run.json`. Best-effort. --- @@ -167,7 +177,7 @@ Calls `RFC_READ_TABLE` on `TLIBG` to check existence; on miss, calls `RS_FUNCTION_POOL_INSERT`. Returns active FG in one round-trip — no separate activate step needed. -Generate `{WORK_TEMP}\sap_function_group_rfc_create_run.ps1`: +Generate `{RUN_TEMP}\sap_function_group_rfc_create_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_function_group_rfc_create.ps1', [System.Text.Encoding]::UTF8) @@ -182,14 +192,14 @@ $content = $content.Replace('%%FUNCTION_GROUP%%', 'THE_FG_NAME') $content = $content.Replace('%%SHORT_TEXT%%', 'THE_SHORT_TEXT') $content = $content.Replace('%%DEVCLASS%%', 'THE_DEVCLASS') $content = $content.Replace('%%CORRNUM%%', 'THE_CORRNUM') -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_function_group_rfc_create_run.ps1', $content, [System.Text.Encoding]::UTF8) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_function_group_rfc_create_run.ps1', $content, [System.Text.Encoding]::UTF8) Write-Host 'Done' ``` Execute via **32-bit PowerShell** (NCo 3.1 is in `GAC_32`): ```bash -C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_function_group_rfc_create_run.ps1" +C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_function_group_rfc_create_run.ps1" ``` **Output (parseable):** @@ -222,7 +232,7 @@ it via toolbar `btn[27]`, handling the Inactive Objects worklist popup | `%%PACKAGE%%` | Package name, or empty / `$TMP` for local | | `%%TRANSPORT%%` | Transport request, or empty for local | -Generate `{WORK_TEMP}\sap_function_group_gui_create_run.ps1`: +Generate `{RUN_TEMP}\sap_function_group_gui_create_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_function_group_gui_create.vbs', [System.Text.Encoding]::UTF8) @@ -236,15 +246,15 @@ $content = $content -replace '%%SESSION_PATH%%', $sessionPath $content = $content -replace '%%ATTACH_LIB_VBS%%','\scripts\sap_attach_lib.vbs' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_function_group_gui_create_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_function_group_gui_create_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Run via cscript: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_function_group_gui_create_run.ps1" -C:/Windows/SysWOW64/cscript.exe //NoLogo {WORK_TEMP}\sap_function_group_gui_create_run.vbs +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_function_group_gui_create_run.ps1" +C:/Windows/SysWOW64/cscript.exe //NoLogo {RUN_TEMP}\sap_function_group_gui_create_run.vbs ``` **Output:** the VBS prints `INFO: sbar [...]` and `INFO: activate sbar @@ -349,7 +359,7 @@ Template: `/references/sap_function_group_gui_delete.vbs`. | `%%TRANSPORT%%` | TR for the post-delete prompt — empty when local (`$TMP`) or already locked to a modifiable TR | | `%%SESSION_LOCK_VBS%%` | path to `sap_session_lock.vbs` | -Write `{WORK_TEMP}\sap_function_group_gui_delete_run.ps1`: +Write `{RUN_TEMP}\sap_function_group_gui_delete_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_function_group_gui_delete.vbs', [System.Text.Encoding]::UTF8) @@ -362,15 +372,15 @@ $content = $content -replace '%%SESSION_PATH%%', $sessionPath $content = $content -replace '%%ATTACH_LIB_VBS%%','\scripts\sap_attach_lib.vbs' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_function_group_gui_delete_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_function_group_gui_delete_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_function_group_gui_delete_run.ps1" -C:/Windows/SysWOW64/cscript.exe //NoLogo {WORK_TEMP}\sap_function_group_gui_delete_run.vbs +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_function_group_gui_delete_run.ps1" +C:/Windows/SysWOW64/cscript.exe //NoLogo {RUN_TEMP}\sap_function_group_gui_delete_run.vbs ``` ### Behaviour Notes @@ -487,7 +497,7 @@ For delete (Step 3e): ## Step 6 — Clean Up ```bash -cmd /c del "{WORK_TEMP}\sap_function_group_rfc_create_run.ps1" "{WORK_TEMP}\sap_function_group_gui_create_run.ps1" "{WORK_TEMP}\sap_function_group_gui_create_run.vbs" "{WORK_TEMP}\sap_function_group_gui_activate_run.ps1" "{WORK_TEMP}\sap_function_group_gui_activate_run.vbs" "{WORK_TEMP}\sap_function_group_check_state_run.ps1" +cmd /c del "{RUN_TEMP}\sap_function_group_rfc_create_run.ps1" "{RUN_TEMP}\sap_function_group_gui_create_run.ps1" "{RUN_TEMP}\sap_function_group_gui_create_run.vbs" "{RUN_TEMP}\sap_function_group_gui_activate_run.ps1" "{RUN_TEMP}\sap_function_group_gui_activate_run.vbs" "{RUN_TEMP}\sap_function_group_check_state_run.ps1" ``` (`del` ignores missing files; safe to run even when only some paths @@ -498,13 +508,13 @@ were used.) ## Final — Log End ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_function_group_run.json" -Status SUCCESS -ExitCode 0 +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_function_group_run.json" -Status SUCCESS -ExitCode 0 ``` On failure (substitute `` and short message): ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_function_group_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_function_group_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" ``` Suggested ``: `FUNCTION_GROUP_FAILED`, `FUNCTION_GROUP_DELETE_FAILED`, diff --git a/plugins/sap-dev-core/skills/sap-gui-skill-scaffold/references/skill_md.template b/plugins/sap-dev-core/skills/sap-gui-skill-scaffold/references/skill_md.template index 2256236..d31dc9b 100644 --- a/plugins/sap-dev-core/skills/sap-gui-skill-scaffold/references/skill_md.template +++ b/plugins/sap-dev-core/skills/sap-gui-skill-scaffold/references/skill_md.template @@ -28,10 +28,17 @@ Resolve `work_dir` via the shared helper — **do NOT read `settings.json` directly** (that ignores the `SAPDEV_AI_WORK_DIR` env var and `userconfig.json`). The helper applies env var > `settings.local.json` > `userconfig.json` > default `C:\sap_dev_work`. Take `{work_dir}` from the `WORK_DIR=` stdout line, then set -`{WORK_TEMP}` = `{work_dir}\temp`. +`{WORK_TEMP}` = `{work_dir}\temp`. Also take `{RUN_TEMP}` from the `RUN_TEMP=` +line — a fresh per-run scratch dir `{work_dir}\temp\run_` (already created by +`Get-SapRunTemp`). Resolve `{RUN_TEMP}` **once here** and reuse it: write all of +this skill's OWN scratch (generated `*_run.vbs` / `*_run.ps1`, the `_run.json` +state file, scratch `.txt`) under `{RUN_TEMP}` so concurrent runs never collide. +Keep `{WORK_TEMP}` (base) ONLY for `Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'` +— the session-attach plumbing derives `{work_dir}\runtime` from its parent and +must see the base path, not the run dir. ```bash -powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir))" +powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir)); Write-Output ('RUN_TEMP=' + (Get-SapRunTemp))" ``` ```powershell @@ -43,7 +50,7 @@ New-Item -Path '{WORK_TEMP}' -ItemType Directory -Force | Out-Null ## Step 0.5 — Start logging ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{WORK_TEMP}\{{SKILL_NAME}}_run.json" -Skill {{SKILL_NAME}} -ParamsJson "{\"mode\":\"\"}" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{RUN_TEMP}\{{SKILL_NAME}}_run.json" -Skill {{SKILL_NAME}} -ParamsJson "{\"mode\":\"\"}" ``` --- diff --git a/plugins/sap-dev-core/skills/sap-login/SKILL.md b/plugins/sap-dev-core/skills/sap-login/SKILL.md index 07ff6ed..9681b1c 100644 --- a/plugins/sap-dev-core/skills/sap-login/SKILL.md +++ b/plugins/sap-dev-core/skills/sap-login/SKILL.md @@ -74,6 +74,22 @@ Then set `{WORK_TEMP}` = `{work_dir}\temp` and ensure it exists: cmd /c if not exist "{WORK_TEMP}" mkdir "{WORK_TEMP}" ``` +Set `{RUN_TEMP}` = the per-run scratch dir from `Get-SapRunTemp` (env bridge +applied). This call also sweeps stale `run_*` dirs from crashed prior runs +(`Remove-SapStaleRunTemp`, best-effort) — login is the natural GC point: + +```bash +powershell -NoProfile -ExecutionPolicy Bypass -Command "\$env:SAPDEV_AI_WORK_DIR='{work_dir}'; . '\scripts\sap_connection_lib.ps1'; [void](Remove-SapStaleRunTemp); Write-Output ('RUN_TEMP=' + (Get-SapRunTemp))" +``` + +Login's OWN scratch — the generated `sap_login_run.vbs` / `sap_rfc_test_run.ps1` +(which hold **decrypted plaintext** credentials) and the `_run.json` state — goes +under `{RUN_TEMP}`, isolating concurrent logins and confining the plaintext to a +single short-lived per-run dir. **Keep `{WORK_TEMP}` (base)** for the +`-WorkTemp "{WORK_TEMP}"` calls to `sap_login_select.ps1` / the broker — those +write the DURABLE pin + registry under `{work_dir}\runtime`, derived from the +base path's parent. + --- ## Argument Modes (Phase 4) @@ -124,13 +140,13 @@ endpoint_summary}`. Run `AskUserQuestion`, then re-invoke with `-ProfileId \scripts\sap_log_helper.ps1" -Action start -StateFile "{WORK_TEMP}\sap_login_run.json" -Skill sap-login -ParamsJson "{\"system\":\"\",\"client\":\"\",\"user\":\"\"}" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{RUN_TEMP}\sap_login_run.json" -Skill sap-login -ParamsJson "{\"system\":\"\",\"client\":\"\",\"user\":\"\"}" ``` --- @@ -408,7 +424,7 @@ ciphertext) — in that case, prompt the operator to type the password fresh and offer to encrypt-and-save it after Step 3 succeeds. The plaintext password lives only in memory of the calling skill and -in the generated `{WORK_TEMP}\sap_login_run.vbs` (which Step 5 +in the generated `{RUN_TEMP}\sap_login_run.vbs` (which Step 5 deletes). Do NOT echo the decrypted value to your reply or to any log. @@ -418,7 +434,7 @@ log. The VBScript template is at `sap-dev-core/shared/scripts/sap_login.vbs`. -Write `{WORK_TEMP}\sap_login_run.ps1`: +Write `{RUN_TEMP}\sap_login_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\scripts\sap_login.vbs', [System.Text.Encoding]::UTF8) $content = $content.Replace('%%SAP_LOGON_DESCRIPTION%%','THE_LOGON_DESC') @@ -432,7 +448,7 @@ $content = $content.Replace('%%SAP_CLIENT%%','THE_CLIENT') $content = $content.Replace('%%SAP_USER%%','THE_USER') $content = $content.Replace('%%SAP_PASSWORD%%','THE_PASSWORD') $content = $content.Replace('%%SAP_LANGUAGE%%','THE_LANGUAGE') -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_login_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_login_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Replace all `THE_*` placeholders with actual values from Step 2. @@ -452,13 +468,13 @@ If a field is unused in the chosen method, substitute the empty string `""`. Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_login_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_login_run.ps1" ``` ### Execute ```bash -cscript //NoLogo {WORK_TEMP}\sap_login_run.vbs +cscript //NoLogo {RUN_TEMP}\sap_login_run.vbs ``` **The VBScript handles four scenarios** (first match wins): @@ -498,7 +514,7 @@ For load-balanced profiles, call `Connect-SapRfc` directly with the template (the template is direct-server only). Either path works for verification — pick the one matching the profile's endpoint shape. -Direct-server path (write `{WORK_TEMP}\sap_rfc_test_run.ps1`): +Direct-server path (write `{RUN_TEMP}\sap_rfc_test_run.ps1`): ```powershell $content = [System.IO.File]::ReadAllText('\scripts\sap_rfc_connect.ps1', [System.Text.Encoding]::UTF8) $content = $content.Replace('%%SAP_APPLICATION_SERVER%%','THE_SERVER') @@ -508,7 +524,7 @@ $content = $content.Replace('%%SAP_USER%%','THE_USER') $content = $content.Replace('%%SAP_PASSWORD%%','THE_PASSWORD') $content = $content.Replace('%%SAP_LANGUAGE%%','THE_LANGUAGE') $content = $content.Replace('%%RFC_LIB_PS1%%','\scripts\sap_rfc_lib.ps1') -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_rfc_test_run.ps1', $content, [System.Text.Encoding]::UTF8) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_rfc_test_run.ps1', $content, [System.Text.Encoding]::UTF8) Write-Host 'Done' ``` Replace all `THE_*` placeholders. @@ -529,7 +545,7 @@ if ($dest) { Write-Host 'RFC_OK' } else { Write-Host 'ERROR: RFC connect failed' Execute via **32-bit PowerShell** (SAP NCo 3.1 is registered in the 32-bit GAC): ```bash -C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_rfc_test_run.ps1" +C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_rfc_test_run.ps1" ``` **On success** (output contains `RFC_OK`): tell user RFC connection verified. @@ -549,7 +565,7 @@ C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypas ## Step 5 — Clean Up ```bash -cmd /c del {WORK_TEMP}\sap_login_run.vbs & del {WORK_TEMP}\sap_login_run.ps1 & del {WORK_TEMP}\sap_rfc_test_run.ps1 +cmd /c del {RUN_TEMP}\sap_login_run.vbs & del {RUN_TEMP}\sap_login_run.ps1 & del {RUN_TEMP}\sap_rfc_test_run.ps1 ``` --- @@ -715,13 +731,13 @@ Log the run-end record. Best-effort: silently no-ops if logging disabled. On success: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_login_run.json" -Status SUCCESS -ExitCode 0 +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_login_run.json" -Status SUCCESS -ExitCode 0 ``` On failure (substitute `` and short message): ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_login_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_login_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" ``` Suggested ``: `LOGIN_FAILED`, `RFC_LOGON_FAILED`, `GUI_TIMEOUT`. @@ -766,11 +782,11 @@ a copied `settings.json` is useless on another machine. See Step 2a still accepted for backward compatibility but trigger a warning that prompts the operator to re-save. -**In flight** — `{WORK_TEMP}\sap_login_run.vbs` and -`{WORK_TEMP}\sap_rfc_test_run.ps1` contain the **decrypted plaintext** +**In flight** — `{RUN_TEMP}\sap_login_run.vbs` and +`{RUN_TEMP}\sap_rfc_test_run.ps1` contain the **decrypted plaintext** during execution because SAP GUI / NCo need it that way. Step 5 deletes both files immediately after use. Never re-use these files -across runs and never copy them out of `{WORK_TEMP}`. +across runs and never copy them out of `{RUN_TEMP}`. **In logs** — `sap_log_lib.ps1` / `sap_log_lib.vbs` redact `sap_password` by key name (`log_redact_keys` setting). With DPAPI on top, the diff --git a/plugins/sap-dev-core/skills/sap-run-abap-unit/SKILL.md b/plugins/sap-dev-core/skills/sap-run-abap-unit/SKILL.md index a012df6..51e83aa 100644 --- a/plugins/sap-dev-core/skills/sap-run-abap-unit/SKILL.md +++ b/plugins/sap-dev-core/skills/sap-run-abap-unit/SKILL.md @@ -84,7 +84,7 @@ release model as `/sap-atc`). Resolve `work_dir` via the env-aware helper (NOT a direct `settings.json` read): ```bash -powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir))" +powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir)); Write-Output ('RUN_TEMP=' + (Get-SapRunTemp))" ``` `{WORK_TEMP} = work_dir\temp`. Settings reads follow `shared/rules/settings_lookup.md`. @@ -93,12 +93,22 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command ". '`, already created by `Get-SapRunTemp`. +Resolve it **once here** and reuse the same value for the rest of this +invocation; it isolates this run's generated wrappers / state / scratch files so +concurrent runs (parallel sub-agents, multi-connection deploys) never collide. +**`{WORK_TEMP}` stays the base temp dir** and is used ONLY for +`Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'` (the session-attach plumbing +derives `{work_dir}\runtime` from its parent, so it must see the base path, not +the run dir). Everything the skill writes itself goes under `{RUN_TEMP}`. + --- ## Step 0.5 — Start Logging ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{WORK_TEMP}\sap_run_abap_unit_run.json" -Skill sap-run-abap-unit -ParamsJson "{\"object\":\"\"}" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{RUN_TEMP}\sap_run_abap_unit_run.json" -Skill sap-run-abap-unit -ParamsJson "{\"object\":\"\"}" ``` --- @@ -113,7 +123,7 @@ powershell -ExecutionPolicy Bypass -File "\scripts\sap_ | `--min-coverage=` | no | brief `MODE_MIN_COVERAGE` / blank | Coverage threshold (percent). **Implies `--with-coverage`.** Gates per `aunit_coverage_gate` (warn / block). | | `--risk-level=` | no | `dangerous` | Cap on the AUnit risk level executed; the client's `SAUNIT_CLIENT_SETUP` is the real gate. | | `--mode=` | no | `GUI` | Phase 1 implements `GUI` only. `RFC` / `ADT` resolve to GUI with an INFO note until Phase 2 / 3. | -| `--save-to=` | no | `{WORK_TEMP}\aunit_.json` | JSON result file. | +| `--save-to=` | no | `{RUN_TEMP}\aunit_.json` | JSON result file. | --- @@ -146,13 +156,13 @@ $content = $content.Replace('%%SESSION_PATH%%', $sessionPath) $content = $content.Replace('%%ATTACH_LIB_VBS%%', "$shared\scripts\sap_attach_lib.vbs") . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_run_abap_unit.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_run_abap_unit.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) ``` Run via 32-bit cscript: ```bash -C:/Windows/SysWOW64/cscript.exe //NoLogo {WORK_TEMP}\sap_run_abap_unit.vbs +C:/Windows/SysWOW64/cscript.exe //NoLogo {RUN_TEMP}\sap_run_abap_unit.vbs ``` --- @@ -209,7 +219,7 @@ gate outcome (ok / below-min warn-or-block). ## Final — Log End ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_run_abap_unit_run.json" -Status SUCCESS -ExitCode 0 -MetricsJson '{"gate":"AUNIT","verdict":"PASS","methods":0,"passed":0,"failed":0,"coverage":-1}' +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_run_abap_unit_run.json" -Status SUCCESS -ExitCode 0 -MetricsJson '{"gate":"AUNIT","verdict":"PASS","methods":0,"passed":0,"failed":0,"coverage":-1}' ``` **Build-KPI enrichment (best-effort).** Include `-MetricsJson` on every end path, diff --git a/plugins/sap-dev-core/skills/sap-se01/SKILL.md b/plugins/sap-dev-core/skills/sap-se01/SKILL.md index 2ae9d0c..eb5d4a9 100644 --- a/plugins/sap-dev-core/skills/sap-se01/SKILL.md +++ b/plugins/sap-dev-core/skills/sap-se01/SKILL.md @@ -57,7 +57,7 @@ Steps 0 – 7 below are the CREATE flow. **Resolve `work_dir` via the env-aware helper** — do NOT take `work_dir` from a direct `settings.json` read (that ignores the `SAPDEV_AI_WORK_DIR` env var and `userconfig.json`). Use the `WORK_DIR=` value printed by: ```bash -powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir))" +powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir)); Write-Output ('RUN_TEMP=' + (Get-SapRunTemp))" ``` The settings note below still applies to the OTHER keys. @@ -80,6 +80,16 @@ Set `{WORK_TEMP}` = `{work_dir}\temp`. Ensure it exists: cmd /c if not exist "{WORK_TEMP}" mkdir "{WORK_TEMP}" ``` +Set `{RUN_TEMP}` = the `RUN_TEMP=` value printed above — a fresh per-run scratch +directory `{work_dir}\temp\run_`, already created by `Get-SapRunTemp`. +Resolve it **once here** and reuse the same value for the rest of this +invocation; it isolates this run's generated wrappers / state / scratch files so +concurrent runs (parallel sub-agents, multi-connection deploys) never collide. +**`{WORK_TEMP}` stays the base temp dir** and is used ONLY for +`Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'` (the session-attach plumbing +derives `{work_dir}\runtime` from its parent, so it must see the base path, not +the run dir). Everything the skill writes itself goes under `{RUN_TEMP}`. + The logged-in SAP user (`sap_user`) is needed for the E070 lookup. If blank, ask the user. @@ -91,10 +101,10 @@ defaults. ## Step 0.5 — Start Logging -Start a structured log run. State file: `{WORK_TEMP}\sap_se01_run.json`. Best-effort. +Start a structured log run. State file: `{RUN_TEMP}\sap_se01_run.json`. Best-effort. ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{WORK_TEMP}\sap_se01_run.json" -Skill sap-se01 -ParamsJson "{\"description\":\"\",\"request_type\":\"W\"}" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{RUN_TEMP}\sap_se01_run.json" -Skill sap-se01 -ParamsJson "{\"description\":\"\",\"request_type\":\"W\"}" ``` --- @@ -183,7 +193,7 @@ Template: `./references/sap_se01_create.vbs`. Tokens: `%%REQUEST_TYPE%%`, `%%DESCRIPTION%%`. The VBS performs both **create** and **lookup** in a single run. -Write `{WORK_TEMP}\sap_se01_create_run.ps1`: +Write `{RUN_TEMP}\sap_se01_create_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_se01_create.vbs', [System.Text.Encoding]::UTF8) $content = $content -replace '%%REQUEST_TYPE%%','THE_TYPE' @@ -195,7 +205,7 @@ $content = $content -replace '%%SESSION_PATH%%', $sessionPath $content = $content -replace '%%ATTACH_LIB_VBS%%','\scripts\sap_attach_lib.vbs' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se01_create_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se01_create_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Replace `THE_TYPE` (W or C), `THE_DESC` (short text), and `` / @@ -203,8 +213,8 @@ Replace `THE_TYPE` (W or C), `THE_DESC` (short text), and `` / Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se01_create_run.ps1" -C:/Windows/SysWOW64/cscript.exe //NoLogo {WORK_TEMP}\sap_se01_create_run.vbs +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se01_create_run.ps1" +C:/Windows/SysWOW64/cscript.exe //NoLogo {RUN_TEMP}\sap_se01_create_run.vbs ``` **Expected last line of stdout:** `DONE`. If the VBS prints `ERROR:`, abort @@ -341,7 +351,7 @@ Same as Step 2: requires an active SAP GUI session. Template: `./references/sap_se01_release.vbs`. Token: `%%TRANSPORT%%`. -Write `{WORK_TEMP}\sap_se01_release_run.ps1`: +Write `{RUN_TEMP}\sap_se01_release_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_se01_release.vbs', [System.Text.Encoding]::UTF8) $content = $content -replace '%%TRANSPORT%%','THE_TR' @@ -351,14 +361,14 @@ $content = $content -replace '%%SESSION_PATH%%', $sessionPath $content = $content -replace '%%ATTACH_LIB_VBS%%','\scripts\sap_attach_lib.vbs' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se01_release_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se01_release_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se01_release_run.ps1" -C:/Windows/SysWOW64/cscript.exe //NoLogo {WORK_TEMP}\sap_se01_release_run.vbs +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se01_release_run.ps1" +C:/Windows/SysWOW64/cscript.exe //NoLogo {RUN_TEMP}\sap_se01_release_run.vbs ``` ## R6 — Interpret VBS output @@ -413,13 +423,13 @@ Log the run-end record. Best-effort. On success: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_se01_run.json" -Status SUCCESS -ExitCode 0 +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_se01_run.json" -Status SUCCESS -ExitCode 0 ``` On failure: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_se01_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_se01_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" ``` Suggested ``: `TR_CREATE_FAILED`, `GUI_TIMEOUT`. diff --git a/plugins/sap-dev-core/skills/sap-se11/SKILL.md b/plugins/sap-dev-core/skills/sap-se11/SKILL.md index f0a2557..79a3001 100644 --- a/plugins/sap-dev-core/skills/sap-se11/SKILL.md +++ b/plugins/sap-dev-core/skills/sap-se11/SKILL.md @@ -47,7 +47,7 @@ Task: $ARGUMENTS **Resolve `work_dir` via the env-aware helper** — do NOT take `work_dir` from a direct `settings.json` read (that ignores the `SAPDEV_AI_WORK_DIR` env var and `userconfig.json`). Use the `WORK_DIR=` value printed by: ```bash -powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir))" +powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir)); Write-Output ('RUN_TEMP=' + (Get-SapRunTemp))" ``` The settings note below still applies to the OTHER keys. @@ -66,14 +66,24 @@ Ensure the temp directory exists: cmd /c if not exist "{WORK_TEMP}" mkdir "{WORK_TEMP}" ``` +Set `{RUN_TEMP}` = the `RUN_TEMP=` value printed above — a fresh per-run scratch +directory `{work_dir}\temp\run_`, already created by `Get-SapRunTemp`. +Resolve it **once here** and reuse the same value for the rest of this +invocation; it isolates this run's generated wrappers / state / scratch files so +concurrent runs (parallel sub-agents, multi-connection deploys) never collide. +**`{WORK_TEMP}` stays the base temp dir** and is used ONLY for +`Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'` (the session-attach plumbing +derives `{work_dir}\runtime` from its parent, so it must see the base path, not +the run dir). Everything the skill writes itself goes under `{RUN_TEMP}`. + --- ## Step 0.5 — Start Logging -Start a structured log run. State file: `{WORK_TEMP}\sap_se11_run.json`. Best-effort. +Start a structured log run. State file: `{RUN_TEMP}\sap_se11_run.json`. Best-effort. ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{WORK_TEMP}\sap_se11_run.json" -Skill sap-se11 -ParamsJson "{\"object_type\":\"\",\"object_name\":\"\"}" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{RUN_TEMP}\sap_se11_run.json" -Skill sap-se11 -ParamsJson "{\"object_type\":\"\",\"object_name\":\"\"}" ``` --- @@ -150,7 +160,7 @@ See `/rules/tr_resolution.md` for the full policy. ## Step 2 — Prepare Definition File Each object type uses a specific tab-delimited definition file format. If the user -pastes definition data directly, write it to `{WORK_TEMP}\.def`. +pastes definition data directly, write it to `{RUN_TEMP}\.def`. ### Definition File Formats @@ -311,7 +321,7 @@ CONSTANTS: ztyp_active TYPE ztyp_status VALUE 'A'. ### Write the definition file If the user pasted definition data: -1. Write the definition to: `{WORK_TEMP}\.def` +1. Write the definition to: `{RUN_TEMP}\.def` 2. Either **UTF-8** (the Write tool default) or **UTF-16 LE** is fine — the reference VBS auto-detects the BOM via the inline `EnsureUnicodeFile` helper and converts UTF-8 → UTF-16 LE in a temp file before reading. No @@ -354,7 +364,7 @@ cmd /c if exist "" (echo EXISTS) else (echo NOT FOUND) > "MANDT${T}X${T}X${T}MANDT", > "AMT${T}${T}${T}DZWERT${T}ZHKTBL001${T}WAERK" > ) -> [System.IO.File]::WriteAllText('{WORK_TEMP}\.def', +> [System.IO.File]::WriteAllText('{RUN_TEMP}\.def', > ($rows -join "`r`n"), > [System.Text.UTF8Encoding]::new($false)) > ``` @@ -376,12 +386,12 @@ upstream LLM pipeline). $ps = Get-Content '\references\sap_se11_normalize_def.ps1' -Raw -Encoding UTF8 $ps = $ps -replace '%%DEFINITION_FILE%%', 'THE_DEFINITION_FILE_PATH' $ps = $ps -replace '%%OBJECT_TYPE%%', 'THE_OBJECT_TYPE' -[System.IO.File]::WriteAllText("{WORK_TEMP}\sap_se11_normalize.ps1", $ps, [System.Text.Encoding]::UTF8) +[System.IO.File]::WriteAllText("{RUN_TEMP}\sap_se11_normalize.ps1", $ps, [System.Text.Encoding]::UTF8) ``` Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se11_normalize.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se11_normalize.ps1" ``` **Parse the last line of output:** @@ -414,7 +424,7 @@ The check VBScript template is at `./references/sap_se11_check.vbs`. It works fo ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se11_check_run.ps1`: +Write `{RUN_TEMP}\sap_se11_check_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_se11_check.vbs', [System.Text.Encoding]::UTF8) $content = $content -replace '%%OBJECT_TYPE%%','THE_OBJECT_TYPE' @@ -425,7 +435,7 @@ $content = $content -replace '%%SESSION_PATH%%', $sessionPath $content = $content -replace '%%ATTACH_LIB_VBS%%','\scripts\sap_attach_lib.vbs' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se11_check_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se11_check_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Replace `THE_OBJECT_TYPE` with one of: `TABLE`, `VIEW`, `DATATYPE`, `TYPEGROUP`, `DOMAIN`, `SEARCHHELP`, `LOCKOBJECT`. @@ -448,13 +458,13 @@ Note: DATAELEMENT, STRUCTURE, and TABLETYPE all use the `DATATYPE` radio button Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se11_check_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se11_check_run.ps1" ``` ### Execute ```bash -cscript //NoLogo {WORK_TEMP}\sap_se11_check_run.vbs +cscript //NoLogo {RUN_TEMP}\sap_se11_check_run.vbs ``` **Parse the last line of output:** @@ -486,11 +496,11 @@ Write a names file with one domain name per line, then fill and run the PS1: ```powershell # Write domain names to check (one per line) $names = "BUKRS`r`nWAERS`r`n" -[System.IO.File]::WriteAllText("{WORK_TEMP}\check_dom_names.txt", $names, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText("{RUN_TEMP}\check_dom_names.txt", $names, [System.Text.UnicodeEncoding]::new($false, $true)) # Fill PS1 template $ps = Get-Content '\references\sap_se11_check_domains.ps1' -Raw -Encoding UTF8 -$ps = $ps -replace '%%NAMES_FILE%%', '{WORK_TEMP}\check_dom_names.txt' +$ps = $ps -replace '%%NAMES_FILE%%', '{RUN_TEMP}\check_dom_names.txt' $ps = $ps -replace '%%SAP_SERVER%%', '' $ps = $ps -replace '%%SAP_SYSNR%%', '' $ps = $ps -replace '%%SAP_CLIENT%%', '' @@ -498,12 +508,12 @@ $ps = $ps -replace '%%SAP_USER%%', '' $ps = $ps -replace '%%SAP_PASSWORD%%', '' $ps = $ps -replace '%%SAP_LANGUAGE%%', '' $ps = $ps -replace '%%RFC_LIB_PS1%%', '\scripts\sap_rfc_lib.ps1' -[System.IO.File]::WriteAllText("{WORK_TEMP}\sap_check_dom.ps1", $ps, [System.Text.Encoding]::UTF8) +[System.IO.File]::WriteAllText("{RUN_TEMP}\sap_check_dom.ps1", $ps, [System.Text.Encoding]::UTF8) ``` Run: ```bash -C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_check_dom.ps1" +C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_check_dom.ps1" ``` Output per domain: `EXIST:` or `NOT_EXIST:`. If any domain shows `NOT_EXIST`, create it first (via this skill's domain create flow) before proceeding. @@ -516,10 +526,10 @@ Same pattern — write names file, fill PS1, run: ```powershell $names = "BUKRS`r`nMATNR`r`nWAERK`r`n" -[System.IO.File]::WriteAllText("{WORK_TEMP}\check_de_names.txt", $names, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText("{RUN_TEMP}\check_de_names.txt", $names, [System.Text.UnicodeEncoding]::new($false, $true)) $ps = Get-Content '\references\sap_se11_check_dataelements.ps1' -Raw -Encoding UTF8 -$ps = $ps -replace '%%NAMES_FILE%%', '{WORK_TEMP}\check_de_names.txt' +$ps = $ps -replace '%%NAMES_FILE%%', '{RUN_TEMP}\check_de_names.txt' $ps = $ps -replace '%%SAP_SERVER%%', '' $ps = $ps -replace '%%SAP_SYSNR%%', '' $ps = $ps -replace '%%SAP_CLIENT%%', '' @@ -527,12 +537,12 @@ $ps = $ps -replace '%%SAP_USER%%', '' $ps = $ps -replace '%%SAP_PASSWORD%%', '' $ps = $ps -replace '%%SAP_LANGUAGE%%', '' $ps = $ps -replace '%%RFC_LIB_PS1%%', '\scripts\sap_rfc_lib.ps1' -[System.IO.File]::WriteAllText("{WORK_TEMP}\sap_check_de.ps1", $ps, [System.Text.Encoding]::UTF8) +[System.IO.File]::WriteAllText("{RUN_TEMP}\sap_check_de.ps1", $ps, [System.Text.Encoding]::UTF8) ``` Run: ```bash -C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_check_de.ps1" +C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_check_de.ps1" ``` Output per data element: `EXIST::` or `NOT_EXIST:`. If any shows `NOT_EXIST`, create it first. The `DATATYPE` value (e.g., `CHAR`, `NUMC`, `CURR`, `QUAN`, `CUKY`, `UNIT`) is used for Ref.Field validation below. @@ -661,7 +671,7 @@ Select the appropriate update VBScript based on the object type: ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se11_update_run.ps1`: +Write `{RUN_TEMP}\sap_se11_update_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_se11__update.vbs', [System.Text.Encoding]::UTF8) $content = $content -replace '%%OBJECT_NAME%%','THE_OBJECT_NAME' @@ -669,7 +679,7 @@ $content = $content -replace '%%DEFINITION_FILE%%','THE_DEFINITION_FILE_PATH' $content = $content -replace '%%PACKAGE%%','THE_PACKAGE' $content = $content -replace '%%TRANSPORT%%','THE_TRANSPORT' $content = $content -replace '%%ACTIVATION_LOG_VBS%%','\scripts\sap_activation_log.vbs' -$content = $content -replace '%%TEMP_DIR%%','{WORK_TEMP}' +$content = $content -replace '%%TEMP_DIR%%','{RUN_TEMP}' # Enhancement-category proactive-set (TABLE / STRUCTURE only — other types # ignore the token if it isn't present in their template). $content = $content -replace '%%ENHANCEMENT_CATEGORY%%','THE_ENH_CATEGORY' @@ -680,7 +690,7 @@ $content = $content -replace '%%SESSION_PATH%%', $sessionPath $content = $content -replace '%%ATTACH_LIB_VBS%%','\scripts\sap_attach_lib.vbs' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se11_update_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se11_update_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Replace `` with the lowercase object type (table, view, dataelement, structure, tabletype, typegroup, domain, searchhelp, lockobject). Replace all `THE_*` placeholders and ``. `THE_PACKAGE` and `THE_TRANSPORT` are optional — use empty string for local object. @@ -710,7 +720,7 @@ ZCMST_RFC_PARAM create via `/sap-dev-init` Step 5. Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se11_update_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se11_update_run.ps1" ``` ### Execute (with SAP GUI Security guard) @@ -728,7 +738,7 @@ watcher is skipped once a rule has been persisted. Run as one PowerShell block ```powershell $shared = '\scripts' -$log = '{WORK_TEMP}\THE_OBJECT_NAME.activation_log.txt' # path the activation-log save would write +$log = '{RUN_TEMP}\THE_OBJECT_NAME.activation_log.txt' # path the activation-log save would write # 1. Pre-check the allow-list (read-only; lets us skip the watcher once a rule exists). & "$shared\sap_gui_security_precheck.ps1" -Path $log -Access w -System 'THE_SID' -Client 'THE_CLIENT' -Transaction 'SE11' | Out-Host $allowed = ($LASTEXITCODE -eq 0) @@ -744,7 +754,7 @@ if (-not $allowed) { # 3. Run the update + activate (32-bit cscript). If activation errors and the # activation-log save raises the dialog, it blocks here until the watcher # dismisses it; then the log is saved and the run completes. -& 'C:/Windows/SysWOW64/cscript.exe' //NoLogo '{WORK_TEMP}\sap_se11_update_run.vbs' +& 'C:/Windows/SysWOW64/cscript.exe' //NoLogo '{RUN_TEMP}\sap_se11_update_run.vbs' # 4. Reap the watcher. if ($watcher) { $watcher | Wait-Process -Timeout 45 -ErrorAction SilentlyContinue } ``` @@ -774,7 +784,7 @@ Select the appropriate create VBScript based on the object type: ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se11_create_run.ps1`: +Write `{RUN_TEMP}\sap_se11_create_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_se11__create.vbs', [System.Text.Encoding]::UTF8) $content = $content -replace '%%OBJECT_NAME%%','THE_OBJECT_NAME' @@ -784,7 +794,7 @@ $content = $content -replace '%%PACKAGE%%','THE_PACKAGE' $content = $content -replace '%%TRANSPORT%%','THE_TRANSPORT' $content = $content -replace '%%SESSION_LOCK_VBS%%','\scripts\sap_session_lock.vbs' $content = $content -replace '%%ACTIVATION_LOG_VBS%%','\scripts\sap_activation_log.vbs' -$content = $content -replace '%%TEMP_DIR%%','{WORK_TEMP}' +$content = $content -replace '%%TEMP_DIR%%','{RUN_TEMP}' # Enhancement-category proactive-set (TABLE / STRUCTURE only — other types # ignore the token if it isn't present in their template). See the # `THE_ENH_CATEGORY` table in Step 5a for accepted values. @@ -795,7 +805,7 @@ $content = $content -replace '%%ENH_CATEGORY_VBS%%','\references\sap_ > **`%%ACTIVATION_LOG_VBS%%` and `%%TEMP_DIR%%`** are required by the > activation-log capture helper invoked when `Activate (Ctrl+F3)` reports > errors. The helper writes `.activation_log.txt` to -> `{WORK_TEMP}` and echoes both the file path and the top error line so +> `{RUN_TEMP}` and echoes both the file path and the top error line so > the operator sees the *specific* failing field/rule (e.g. "X-AMT > (specify reference table AND reference field)") instead of the generic > "refer to log" popup. Currently wired into `sap_se11_table_create.vbs` @@ -845,7 +855,7 @@ $content = $content -replace '%%POST_ACTIVATE_VERIFY_VBS%%','\scripts\sap_se11_post_activate_verify.ps1' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se11_create_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se11_create_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` @@ -858,7 +868,7 @@ Replace all `THE_*` placeholders, ``, and ``. `THE_PACKAGE` and Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se11_create_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se11_create_run.ps1" ``` ### Execute (with SAP GUI Security guard) @@ -876,7 +886,7 @@ watcher is skipped once a rule has been persisted. Run as one PowerShell block ```powershell $shared = '\scripts' -$log = '{WORK_TEMP}\THE_OBJECT_NAME.activation_log.txt' # path the activation-log save would write +$log = '{RUN_TEMP}\THE_OBJECT_NAME.activation_log.txt' # path the activation-log save would write # 1. Pre-check the allow-list (read-only; lets us skip the watcher once a rule exists). & "$shared\sap_gui_security_precheck.ps1" -Path $log -Access w -System 'THE_SID' -Client 'THE_CLIENT' -Transaction 'SE11' | Out-Host $allowed = ($LASTEXITCODE -eq 0) @@ -892,7 +902,7 @@ if (-not $allowed) { # 3. Run the create + activate (32-bit cscript). If activation errors and the # activation-log save raises the dialog, it blocks here until the watcher # dismisses it; then the log is saved and the run completes. -& 'C:/Windows/SysWOW64/cscript.exe' //NoLogo '{WORK_TEMP}\sap_se11_create_run.vbs' +& 'C:/Windows/SysWOW64/cscript.exe' //NoLogo '{RUN_TEMP}\sap_se11_create_run.vbs' # 4. Reap the watcher. if ($watcher) { $watcher | Wait-Process -Timeout 45 -ErrorAction SilentlyContinue } ``` @@ -960,12 +970,12 @@ $ps = $ps -replace '%%SAP_LANGUAGE%%', '' $ps = $ps -replace '%%RFC_LIB_PS1%%', '\scripts\sap_rfc_lib.ps1' $ps = $ps -replace '%%OBJECT_TYPE%%', 'STRUCTURE' $ps = $ps -replace '%%OBJECT_NAME%%', 'THE_OBJECT_NAME' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se11_verify.ps1', $ps, [System.Text.Encoding]::UTF8) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se11_verify.ps1', $ps, [System.Text.Encoding]::UTF8) ``` Run via 32-bit PowerShell: ```bash -C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se11_verify.ps1" +C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se11_verify.ps1" ``` **Parse the last line:** @@ -1030,7 +1040,7 @@ The VBScript template is at `./references/sap_se11_change_package.vbs`. ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se11_chgpkg_run.ps1`: +Write `{RUN_TEMP}\sap_se11_chgpkg_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_se11_change_package.vbs', [System.Text.Encoding]::UTF8) $content = $content -replace '%%OBJECT_TYPE%%','THE_OBJECT_TYPE' @@ -1044,7 +1054,7 @@ $content = $content -replace '%%SESSION_PATH%%', $sessionPath $content = $content -replace '%%ATTACH_LIB_VBS%%','\scripts\sap_attach_lib.vbs' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se11_chgpkg_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se11_chgpkg_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Replace `THE_OBJECT_TYPE` (one of TABL/VIEW/DTEL/TTYP/DOMA/SHLP/ENQU), `THE_OBJECT_NAME` @@ -1053,13 +1063,13 @@ Replace `THE_OBJECT_TYPE` (one of TABL/VIEW/DTEL/TTYP/DOMA/SHLP/ENQU), `THE_OBJE Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se11_chgpkg_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se11_chgpkg_run.ps1" ``` ### Execute ```bash -cscript //NoLogo {WORK_TEMP}\sap_se11_chgpkg_run.vbs +cscript //NoLogo {RUN_TEMP}\sap_se11_chgpkg_run.vbs ``` **On success** (output contains `SUCCESS:`): report the package change to the user. @@ -1133,7 +1143,7 @@ prompt (also Yes) and the post-delete TR popup (`ctxtKO008-TRKORR`). ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se11_delete_run.ps1`: +Write `{RUN_TEMP}\sap_se11_delete_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_se11_delete.vbs', [System.Text.Encoding]::UTF8) $content = $content -replace '%%OBJECT_TYPE%%','THE_OBJECT_TYPE' @@ -1146,20 +1156,20 @@ $content = $content -replace '%%SESSION_PATH%%', $sessionPath $content = $content -replace '%%ATTACH_LIB_VBS%%','\scripts\sap_attach_lib.vbs' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se11_delete_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se11_delete_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Replace `THE_OBJECT_TYPE`, `THE_OBJECT_NAME`, `THE_TRANSPORT`, and ``. Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se11_delete_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se11_delete_run.ps1" ``` ### Execute ```bash -cscript //NoLogo {WORK_TEMP}\sap_se11_delete_run.vbs +cscript //NoLogo {RUN_TEMP}\sap_se11_delete_run.vbs ``` ### Behaviour Notes @@ -1234,10 +1244,10 @@ manual cleanup via SE03. Delete all temporary files: ```bash -cmd /c del {WORK_TEMP}\sap_se11_check_run.vbs & del {WORK_TEMP}\sap_se11_check_run.ps1 & del {WORK_TEMP}\sap_se11_create_run.vbs & del {WORK_TEMP}\sap_se11_create_run.ps1 & del {WORK_TEMP}\sap_se11_update_run.vbs & del {WORK_TEMP}\sap_se11_update_run.ps1 & del {WORK_TEMP}\sap_se11_chgpkg_run.vbs & del {WORK_TEMP}\sap_se11_chgpkg_run.ps1 & del {WORK_TEMP}\sap_se11_delete_run.vbs & del {WORK_TEMP}\sap_se11_delete_run.ps1 & del {WORK_TEMP}\sap_se11_normalize.ps1 +cmd /c del {RUN_TEMP}\sap_se11_check_run.vbs & del {RUN_TEMP}\sap_se11_check_run.ps1 & del {RUN_TEMP}\sap_se11_create_run.vbs & del {RUN_TEMP}\sap_se11_create_run.ps1 & del {RUN_TEMP}\sap_se11_update_run.vbs & del {RUN_TEMP}\sap_se11_update_run.ps1 & del {RUN_TEMP}\sap_se11_chgpkg_run.vbs & del {RUN_TEMP}\sap_se11_chgpkg_run.ps1 & del {RUN_TEMP}\sap_se11_delete_run.vbs & del {RUN_TEMP}\sap_se11_delete_run.ps1 & del {RUN_TEMP}\sap_se11_normalize.ps1 ``` -Also delete `{WORK_TEMP}\.def` if the user pasted definition data (not a user-supplied file). +Also delete `{RUN_TEMP}\.def` if the user pasted definition data (not a user-supplied file). --- @@ -1248,13 +1258,13 @@ Log the run-end record. Best-effort. On success: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_se11_run.json" -Status SUCCESS -ExitCode 0 +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_se11_run.json" -Status SUCCESS -ExitCode 0 ``` On failure: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_se11_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_se11_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" ``` Suggested ``: `SE11_FAILED`, `SE11_INACTIVE`, `SE11_LOCKED`, `TR_RESOLUTION_FAILED`, `GUI_TIMEOUT`. @@ -1402,8 +1412,8 @@ re-encodes to a UTF-16 LE temp file before opening. No manual ```powershell # Both work — pick whichever is convenient. -Set-Content '{WORK_TEMP}\mytable.def' $content # UTF-8 (default) -Set-Content '{WORK_TEMP}\mytable.def' $content -Encoding Unicode # UTF-16 LE +Set-Content '{RUN_TEMP}\mytable.def' $content # UTF-8 (default) +Set-Content '{RUN_TEMP}\mytable.def' $content -Encoding Unicode # UTF-16 LE ``` --- diff --git a/plugins/sap-dev-core/skills/sap-se16n/SKILL.md b/plugins/sap-dev-core/skills/sap-se16n/SKILL.md index 98cb9cc..c2ef3da 100644 --- a/plugins/sap-dev-core/skills/sap-se16n/SKILL.md +++ b/plugins/sap-dev-core/skills/sap-se16n/SKILL.md @@ -39,7 +39,7 @@ Task: $ARGUMENTS **Resolve `work_dir` via the env-aware helper** — do NOT take `work_dir` from a direct `settings.json` read (that ignores the `SAPDEV_AI_WORK_DIR` env var and `userconfig.json`). Use the `WORK_DIR=` value printed by: ```bash -powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir))" +powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir)); Write-Output ('RUN_TEMP=' + (Get-SapRunTemp))" ``` The settings note below still applies to the OTHER keys. @@ -58,14 +58,24 @@ Ensure the temp directory exists: cmd /c if not exist "{WORK_TEMP}" mkdir "{WORK_TEMP}" ``` +Set `{RUN_TEMP}` = the `RUN_TEMP=` value printed above — a fresh per-run scratch +directory `{work_dir}\temp\run_`, already created by `Get-SapRunTemp`. +Resolve it **once here** and reuse the same value for the rest of this +invocation; it isolates this run's generated wrappers / state / scratch files so +concurrent runs (parallel sub-agents, multi-connection deploys) never collide. +**`{WORK_TEMP}` stays the base temp dir** and is used ONLY for +`Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'` (the session-attach plumbing +derives `{work_dir}\runtime` from its parent, so it must see the base path, not +the run dir). Everything the skill writes itself goes under `{RUN_TEMP}`. + --- ## Step 0.5 — Start Logging -Start a structured log run. State file: `{WORK_TEMP}\sap_se16n_run.json`. Best-effort. +Start a structured log run. State file: `{RUN_TEMP}\sap_se16n_run.json`. Best-effort. ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{WORK_TEMP}\sap_se16n_run.json" -Skill sap-se16n -ParamsJson "{\"table\":\"\"}" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{RUN_TEMP}\sap_se16n_run.json" -Skill sap-se16n -ParamsJson "{\"table\":\"
\"}" ``` --- @@ -77,7 +87,7 @@ powershell -ExecutionPolicy Bypass -File "\scripts\sap_ | Table name | SAP transparent / pooled / cluster table | `T001` | | Filter fields | Zero or more `field op value(s)` triples — see operator table | `LAND1 IN CN,JP` | | Select fields | Optional comma-separated list of output columns; empty = all fields | `BUKRS,BUTXT,LAND1,WAERS` | -| Output file | Absolute path of the resulting `.txt` file (default: `{WORK_TEMP}\se16n_
.txt`) | | +| Output file | Absolute path of the resulting `.txt` file (default: `{RUN_TEMP}\se16n_
.txt`) | | **Operators** (column 2 of each FILTER row): @@ -116,7 +126,7 @@ the `/sap-login` skill first, then return here. ## Step 3 — Write the PARAMS_FILE -Write `{WORK_TEMP}\se16n_params.txt` with two literal section headers, **`SELECT`** +Write `{RUN_TEMP}\se16n_params.txt` with two literal section headers, **`SELECT`** and **`FILTER`**, each followed by its rows. Either section may be empty. Format: @@ -166,12 +176,12 @@ The VBS template is at `./references/sap_se16n.vbs`. ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se16n_run.ps1`: +Write `{RUN_TEMP}\sap_se16n_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_se16n.vbs', [System.Text.Encoding]::UTF8) $content = $content -replace '%%TABLE_NAME%%','THE_TABLE' -$content = $content -replace '%%PARAMS_FILE%%','{WORK_TEMP}\se16n_params.txt' -$content = $content -replace '%%OUTPUT_FILE%%','{WORK_TEMP}\se16n_THE_TABLE.txt' +$content = $content -replace '%%PARAMS_FILE%%','{RUN_TEMP}\se16n_params.txt' +$content = $content -replace '%%OUTPUT_FILE%%','{RUN_TEMP}\se16n_THE_TABLE.txt' # Session-attach plumbing (Phase 4.2: pin file eliminated). The shared # AttachSapSession helper resolves the target session in this order: # 1. SESSION_PATH constant (set from the parsed --session argument) @@ -186,7 +196,7 @@ $content = $content -replace '%%ATTACH_LIB_VBS%%','\scr # default for the single-conn case. . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se16n_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se16n_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Replace `THE_TABLE` with the actual table name (UPPERCASE) and `` / @@ -194,7 +204,7 @@ Replace `THE_TABLE` with the actual table name (UPPERCASE) and `` / Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se16n_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se16n_run.ps1" ``` ### Execute (with SAP GUI Security guard) @@ -209,7 +219,7 @@ system / client: ```powershell $shared = '\scripts' -$out = '{WORK_TEMP}\se16n_THE_TABLE.txt' +$out = '{RUN_TEMP}\se16n_THE_TABLE.txt' # 1. Pre-check the allow-list (read-only; informational + lets us skip the watcher). & "$shared\sap_gui_security_precheck.ps1" -Path $out -Access w -System 'THE_SID' -Client 'THE_CLIENT' -Transaction 'SE16N' | Out-Host $allowed = ($LASTEXITCODE -eq 0) @@ -224,7 +234,7 @@ if (-not $allowed) { } # 3. Run the export (32-bit cscript). If the dialog appears it blocks here until # the watcher dismisses it; then the export completes. -& 'C:/Windows/SysWOW64/cscript.exe' //NoLogo '{WORK_TEMP}\sap_se16n_run.vbs' +& 'C:/Windows/SysWOW64/cscript.exe' //NoLogo '{RUN_TEMP}\sap_se16n_run.vbs' # 4. Reap the watcher. if ($watcher) { $watcher | Wait-Process -Timeout 45 -ErrorAction SilentlyContinue } ``` @@ -241,7 +251,7 @@ if ($watcher) { $watcher | Wait-Process -Timeout 45 -ErrorAction SilentlyContinu | `ROWS=0 (NO_DATA)` | "No values found" or empty result set. Output file contains a single line `NO_DATA`. | | `ERROR: …` | Failure — show the full output and stop. | -**Output file** (`{WORK_TEMP}\se16n_
.txt`): +**Output file** (`{RUN_TEMP}\se16n_
.txt`): - First line = header (technical field names) - Subsequent lines = data rows - Field separator = TAB @@ -268,13 +278,13 @@ Log the run-end record. Best-effort. On success: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_se16n_run.json" -Status SUCCESS -ExitCode 0 +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_se16n_run.json" -Status SUCCESS -ExitCode 0 ``` On failure: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_se16n_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_se16n_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" ``` Suggested ``: `SE16N_FAILED`, `TABLE_NOT_FOUND`, `GUI_TIMEOUT`. diff --git a/plugins/sap-dev-core/skills/sap-se21/SKILL.md b/plugins/sap-dev-core/skills/sap-se21/SKILL.md index e86fb19..20e0153 100644 --- a/plugins/sap-dev-core/skills/sap-se21/SKILL.md +++ b/plugins/sap-dev-core/skills/sap-se21/SKILL.md @@ -48,7 +48,7 @@ Task: $ARGUMENTS **Resolve `work_dir` via the env-aware helper** — do NOT take `work_dir` from a direct `settings.json` read (that ignores the `SAPDEV_AI_WORK_DIR` env var and `userconfig.json`). Use the `WORK_DIR=` value printed by: ```bash -powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir))" +powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir)); Write-Output ('RUN_TEMP=' + (Get-SapRunTemp))" ``` The settings note below still applies to the OTHER keys. @@ -67,14 +67,24 @@ Ensure the temp directory exists: cmd /c if not exist "{WORK_TEMP}" mkdir "{WORK_TEMP}" ``` +Set `{RUN_TEMP}` = the `RUN_TEMP=` value printed above — a fresh per-run scratch +directory `{work_dir}\temp\run_`, already created by `Get-SapRunTemp`. +Resolve it **once here** and reuse the same value for the rest of this +invocation; it isolates this run's generated wrappers / state / scratch files so +concurrent runs (parallel sub-agents, multi-connection deploys) never collide. +**`{WORK_TEMP}` stays the base temp dir** and is used ONLY for +`Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'` (the session-attach plumbing +derives `{work_dir}\runtime` from its parent, so it must see the base path, not +the run dir). Everything the skill writes itself goes under `{RUN_TEMP}`. + --- ## Step 0.5 — Start Logging -Start a structured log run. State file: `{WORK_TEMP}\sap_se21_run.json`. Best-effort. +Start a structured log run. State file: `{RUN_TEMP}\sap_se21_run.json`. Best-effort. ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{WORK_TEMP}\sap_se21_run.json" -Skill sap-se21 -ParamsJson "{\"package\":\"\"}" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{RUN_TEMP}\sap_se21_run.json" -Skill sap-se21 -ParamsJson "{\"package\":\"\"}" ``` --- @@ -120,7 +130,7 @@ they configure settings.json for future use. Use RFC_READ_TABLE via the PowerShell template at `/references/sap_check_package.ps1`. -**Write `{WORK_TEMP}\sap_check_package_run.ps1`:** +**Write `{RUN_TEMP}\sap_check_package_run.ps1`:** ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_check_package.ps1', [System.Text.Encoding]::UTF8) $content = $content.Replace('%%SAP_APPLICATION_SERVER%%', '') @@ -131,19 +141,19 @@ $content = $content.Replace('%%SAP_PASSWORD%%', '') $content = $content.Replace('%%SAP_LANGUAGE%%', '') $content = $content.Replace('%%RFC_LIB_PS1%%', '\scripts\sap_rfc_lib.ps1') $content = $content.Replace('%%PACKAGE%%', 'THE_PACKAGE') -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_check_package_filled.ps1', $content, [System.Text.Encoding]::UTF8) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_check_package_filled.ps1', $content, [System.Text.Encoding]::UTF8) Write-Host 'Done' ``` Replace all `THE_*` placeholders and `` with actual values. Run it: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_check_package_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_check_package_run.ps1" ``` Execute via 32-bit PowerShell (SAP NCo 3.1 is registered in the 32-bit GAC): ```bash -C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_check_package_filled.ps1" +C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_check_package_filled.ps1" ``` **Parse output:** @@ -194,7 +204,7 @@ fills in the package name + description, and assigns the TR. **Prerequisite:** The user must already be logged in (run `/sap-login` first). This script does **not** open a new connection. -**Write `{WORK_TEMP}\sap_se21_run.ps1`:** +**Write `{RUN_TEMP}\sap_se21_run.ps1`:** ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_se21_create.vbs', [System.Text.Encoding]::UTF8) $content = $content.Replace('%%PACKAGE%%', 'THE_PACKAGE') @@ -207,7 +217,7 @@ $content = $content.Replace('%%SESSION_PATH%%', $sessionPath) $content = $content.Replace('%%ATTACH_LIB_VBS%%', '\scripts\sap_attach_lib.vbs') . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se21_filled.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se21_filled.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` @@ -225,12 +235,12 @@ includes the shared session-lock helpers via `ExecuteGlobal CreateObject( Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se21_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se21_run.ps1" ``` Execute via regular cscript (SAP GUI Scripting works in either bitness): ```bash -cscript //NoLogo "{WORK_TEMP}\sap_se21_filled.vbs" +cscript //NoLogo "{RUN_TEMP}\sap_se21_filled.vbs" ``` --- @@ -304,7 +314,7 @@ flow is: ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se21_delete_run.ps1`: +Write `{RUN_TEMP}\sap_se21_delete_run.ps1`: ```powershell $skillDir = '' @@ -319,15 +329,15 @@ $content = $content.Replace('%%SESSION_PATH%%', $sessionPath) $content = $content.Replace('%%ATTACH_LIB_VBS%%', '\scripts\sap_attach_lib.vbs') . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se21_delete_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se21_delete_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se21_delete_run.ps1" -cscript //NoLogo "{WORK_TEMP}\sap_se21_delete_run.vbs" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se21_delete_run.ps1" +cscript //NoLogo "{RUN_TEMP}\sap_se21_delete_run.vbs" ``` ### Behaviour Notes @@ -378,7 +388,7 @@ create reporting applies. ## Step 7 — Clean Up ```bash -cmd /c del "{WORK_TEMP}\sap_check_package_run.ps1" "{WORK_TEMP}\sap_check_package_filled.ps1" "{WORK_TEMP}\sap_se21_run.ps1" "{WORK_TEMP}\sap_se21_filled.vbs" "{WORK_TEMP}\sap_se21_delete_run.vbs" "{WORK_TEMP}\sap_se21_delete_run.ps1" +cmd /c del "{RUN_TEMP}\sap_check_package_run.ps1" "{RUN_TEMP}\sap_check_package_filled.ps1" "{RUN_TEMP}\sap_se21_run.ps1" "{RUN_TEMP}\sap_se21_filled.vbs" "{RUN_TEMP}\sap_se21_delete_run.vbs" "{RUN_TEMP}\sap_se21_delete_run.ps1" ``` --- @@ -390,13 +400,13 @@ Log the run-end record. Best-effort. On success: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_se21_run.json" -Status SUCCESS -ExitCode 0 +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_se21_run.json" -Status SUCCESS -ExitCode 0 ``` On failure: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_se21_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_se21_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" ``` Suggested ``: `SE21_FAILED`, `TR_RESOLUTION_FAILED`, `GUI_TIMEOUT`. diff --git a/plugins/sap-dev-core/skills/sap-se24/SKILL.md b/plugins/sap-dev-core/skills/sap-se24/SKILL.md index 09c153c..7fc634f 100644 --- a/plugins/sap-dev-core/skills/sap-se24/SKILL.md +++ b/plugins/sap-dev-core/skills/sap-se24/SKILL.md @@ -59,7 +59,7 @@ Task: $ARGUMENTS **Resolve `work_dir` via the env-aware helper** — do NOT take `work_dir` from a direct `settings.json` read (that ignores the `SAPDEV_AI_WORK_DIR` env var and `userconfig.json`). Use the `WORK_DIR=` value printed by: ```bash -powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir))" +powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir)); Write-Output ('RUN_TEMP=' + (Get-SapRunTemp))" ``` The settings note below still applies to the OTHER keys. @@ -78,14 +78,24 @@ Ensure the temp directory exists: cmd /c if not exist "{WORK_TEMP}" mkdir "{WORK_TEMP}" ``` +Set `{RUN_TEMP}` = the `RUN_TEMP=` value printed above — a fresh per-run scratch +directory `{work_dir}\temp\run_`, already created by `Get-SapRunTemp`. +Resolve it **once here** and reuse the same value for the rest of this +invocation; it isolates this run's generated wrappers / state / scratch files so +concurrent runs (parallel sub-agents, multi-connection deploys) never collide. +**`{WORK_TEMP}` stays the base temp dir** and is used ONLY for +`Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'` (the session-attach plumbing +derives `{work_dir}\runtime` from its parent, so it must see the base path, not +the run dir). Everything the skill writes itself goes under `{RUN_TEMP}`. + --- ## Step 0.5 — Start Logging -Start a structured log run. State file: `{WORK_TEMP}\sap_se24_run.json`. Best-effort. +Start a structured log run. State file: `{RUN_TEMP}\sap_se24_run.json`. Best-effort. ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{WORK_TEMP}\sap_se24_run.json" -Skill sap-se24 -ParamsJson "{\"class\":\"\"}" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{RUN_TEMP}\sap_se24_run.json" -Skill sap-se24 -ParamsJson "{\"class\":\"\"}" ``` --- @@ -166,12 +176,12 @@ CLASS zcl_example IMPLEMENTATION. ENDMETHOD. ENDCLASS. "@ -[System.IO.File]::WriteAllText("{WORK_TEMP}\zcl_example.abap", $content, (New-Object System.Text.UTF8Encoding $false)) +[System.IO.File]::WriteAllText("{RUN_TEMP}\zcl_example.abap", $content, (New-Object System.Text.UTF8Encoding $false)) ``` **If the user pasted source code directly:** -1. Write the source using the BOM-free method above to: `{WORK_TEMP}\.abap` +1. Write the source using the BOM-free method above to: `{RUN_TEMP}\.abap` 2. Confirm the file by reading back the first 5 lines. **If the user provided a file path:** @@ -199,7 +209,7 @@ The check VBScript template is at `./references/sap_se24_check.vbs`. ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se24_check_run.ps1`: +Write `{RUN_TEMP}\sap_se24_check_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_se24_check.vbs', [System.Text.Encoding]::UTF8) $content = $content -replace '%%CLASS_NAME%%','THE_CLASS_NAME' @@ -209,20 +219,20 @@ $content = $content -replace '%%SESSION_PATH%%', $sessionPath $content = $content -replace '%%ATTACH_LIB_VBS%%','\scripts\sap_attach_lib.vbs' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se24_check_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se24_check_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Replace `THE_CLASS_NAME` with the actual class name (UPPERCASE) and `` with the absolute path to this skill directory. Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se24_check_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se24_check_run.ps1" ``` ### Execute ```bash -cscript //NoLogo {WORK_TEMP}\sap_se24_check_run.vbs +cscript //NoLogo {RUN_TEMP}\sap_se24_check_run.vbs ``` **Parse the last line of output:** @@ -283,7 +293,7 @@ per class. ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se24_update_run.ps1`: +Write `{RUN_TEMP}\sap_se24_update_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_se24_update.vbs', [System.Text.Encoding]::UTF8) $content = $content -replace '%%CLASS_NAME%%','THE_CLASS_NAME' @@ -298,7 +308,7 @@ $content = $content -replace '%%SESSION_PATH%%', $sessionPath $content = $content -replace '%%ATTACH_LIB_VBS%%','\scripts\sap_attach_lib.vbs' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se24_update_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se24_update_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Replace `THE_CLASS_NAME` (UPPERCASE), `THE_SOURCE_PATH` (absolute path with backslashes), @@ -306,7 +316,7 @@ Replace `THE_CLASS_NAME` (UPPERCASE), `THE_SOURCE_PATH` (absolute path with back Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se24_update_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se24_update_run.ps1" ``` ### Execute (with SAP GUI Security guard) @@ -339,7 +349,7 @@ if (-not $allowed) { } # 3. Run the upload + save + activate + syntax check (32-bit cscript). If the dialog # appears it blocks here until the watcher dismisses it; then the upload completes. -& 'C:/Windows/SysWOW64/cscript.exe' //NoLogo '{WORK_TEMP}\sap_se24_update_run.vbs' +& 'C:/Windows/SysWOW64/cscript.exe' //NoLogo '{RUN_TEMP}\sap_se24_update_run.vbs' # 4. Reap the watcher. if ($watcher) { $watcher | Wait-Process -Timeout 50 -ErrorAction SilentlyContinue } ``` @@ -363,7 +373,7 @@ It does NOT upload source code. After creation, you must: ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se24_create_run.ps1`: +Write `{RUN_TEMP}\sap_se24_create_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_se24_create.vbs', [System.Text.Encoding]::UTF8) $content = $content -replace '%%CLASS_NAME%%','THE_CLASS_NAME' @@ -376,20 +386,20 @@ $content = $content -replace '%%SESSION_PATH%%', $sessionPath $content = $content -replace '%%ATTACH_LIB_VBS%%','\scripts\sap_attach_lib.vbs' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se24_create_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se24_create_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Replace all `THE_*` placeholders (PACKAGE/TRANSPORT blank if local $TMP) and ``. Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se24_create_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se24_create_run.ps1" ``` ### Execute ```bash -cscript //NoLogo {WORK_TEMP}\sap_se24_create_run.vbs +cscript //NoLogo {RUN_TEMP}\sap_se24_create_run.vbs ``` **On success** (output contains `SUCCESS:`): @@ -439,7 +449,7 @@ The reference VBS is at `./references/sap_se24_test_classes.vbs`. ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se24_test_classes_run.ps1`: +Write `{RUN_TEMP}\sap_se24_test_classes_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_se24_test_classes.vbs', [System.Text.Encoding]::UTF8) $content = $content -replace '%%CLASS_NAME%%','THE_CLASS_NAME' @@ -452,7 +462,7 @@ $content = $content -replace '%%SESSION_PATH%%', $sessionPath $content = $content -replace '%%ATTACH_LIB_VBS%%','\scripts\sap_attach_lib.vbs' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se24_test_classes_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se24_test_classes_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Replace `THE_CLASS_NAME` (UPPERCASE), `THE_TEST_SOURCE_PATH` (absolute path with @@ -477,7 +487,7 @@ if (-not $allowed) { '-NoProfile','-ExecutionPolicy','Bypass','-File',"$shared\sap_gui_security_sidecar.ps1",'-TimeoutSeconds','45') Start-Sleep -Milliseconds 800 } -& 'C:/Windows/SysWOW64/cscript.exe' //NoLogo '{WORK_TEMP}\sap_se24_test_classes_run.vbs' +& 'C:/Windows/SysWOW64/cscript.exe' //NoLogo '{RUN_TEMP}\sap_se24_test_classes_run.vbs' if ($watcher) { $watcher | Wait-Process -Timeout 50 -ErrorAction SilentlyContinue } ``` @@ -556,18 +566,18 @@ literal non-ASCII description in the script would mojibake. The generator reads it back as UTF-8 and doubles any `"` for the VBS string literal: ```powershell -[System.IO.File]::WriteAllText('{WORK_TEMP}\se24_desc.txt', 'THE_DESCRIPTION', (New-Object System.Text.UTF8Encoding $false)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\se24_desc.txt', 'THE_DESCRIPTION', (New-Object System.Text.UTF8Encoding $false)) ``` (If `DESCRIPTION` is empty, write an empty file or skip — the generator treats a missing file as empty.) -Write `{WORK_TEMP}\sap_se24_change_props_run.ps1`: +Write `{RUN_TEMP}\sap_se24_change_props_run.ps1`: ```powershell $skillDir = '' $tpl = "$skillDir\references\sap_se24_change_props.vbs" $content = [System.IO.File]::ReadAllText($tpl, [System.Text.Encoding]::UTF8) # Description: read UTF-8 file, escape VBS quotes ("->"") so non-ASCII + quotes survive. -$descFile = '{WORK_TEMP}\se24_desc.txt' +$descFile = '{RUN_TEMP}\se24_desc.txt' $desc = if (Test-Path $descFile) { [System.IO.File]::ReadAllText($descFile, [System.Text.Encoding]::UTF8) } else { '' } $desc = $desc.Replace('"','""') $content = $content.Replace('%%CLASS_NAME%%', 'THE_CLASS_NAME') @@ -582,7 +592,7 @@ $content = $content.Replace('%%SESSION_PATH%%', $sessionPath) $content = $content.Replace('%%ATTACH_LIB_VBS%%', '\scripts\sap_attach_lib.vbs') . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se24_change_props_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se24_change_props_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Use `.Replace()` (literal) for `STATUS`/`CATEGORY`/`TRANSPORT` — description text @@ -593,13 +603,13 @@ blank when unused). Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se24_change_props_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se24_change_props_run.ps1" ``` ### Execute ```bash -cscript //NoLogo {WORK_TEMP}\sap_se24_change_props_run.vbs +cscript //NoLogo {RUN_TEMP}\sap_se24_change_props_run.vbs ``` ### Behaviour Notes @@ -711,7 +721,7 @@ name, so a single VBS handles both. ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se24_delete_run.ps1`: +Write `{RUN_TEMP}\sap_se24_delete_run.ps1`: ```powershell $skillDir = '' $tpl = "$skillDir\references\sap_se24_delete.vbs" @@ -724,20 +734,20 @@ $content = $content.Replace('%%SESSION_PATH%%', $sessionPath) $content = $content.Replace('%%ATTACH_LIB_VBS%%', '\scripts\sap_attach_lib.vbs') . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se24_delete_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se24_delete_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Replace `` and the `THE_*` placeholders. Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se24_delete_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se24_delete_run.ps1" ``` ### Execute ```bash -cscript //NoLogo {WORK_TEMP}\sap_se24_delete_run.vbs +cscript //NoLogo {RUN_TEMP}\sap_se24_delete_run.vbs ``` ### Behaviour Notes @@ -861,7 +871,7 @@ The check-and-download VBScript template is at `./references/sap_se24_check_and_ ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se24_check_and_download_run.ps1`: +Write `{RUN_TEMP}\sap_se24_check_and_download_run.ps1`: ```powershell $className = 'THE_CLASS_NAME' $outFile = 'THE_OUTPUT_FILE' @@ -878,20 +888,20 @@ $content = $content -replace '%%ATTACH_LIB_VBS%%', '\sc $content = $content -replace '%%SYNTAX_CHECK_LIB_VBS%%', '\scripts\sap_syntax_check_lib.vbs' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp $workTemp -[System.IO.File]::WriteAllText("$workTemp\sap_se24_check_and_download_run.vbs", $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText("{RUN_TEMP}\sap_se24_check_and_download_run.vbs", $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` | Placeholder | Value | |---|---| | `THE_CLASS_NAME` | Class name (UPPERCASE) | -| `THE_OUTPUT_FILE` | `{WORK_TEMP}\_from_sap.txt` | +| `THE_OUTPUT_FILE` | `{RUN_TEMP}\_from_sap.txt` | | `THE_SKILL_DIR` | Absolute path to this skill directory | | `THE_WORK_TEMP` | `{WORK_TEMP}` resolved value | Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se24_check_and_download_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se24_check_and_download_run.ps1" ``` ### Execute (with SAP GUI Security guard) @@ -907,7 +917,7 @@ system / client: ```powershell $shared = '\scripts' -$out = '{WORK_TEMP}\THE_CLASS_NAME_from_sap.txt' # the path SAP GUI will write +$out = '{RUN_TEMP}\THE_CLASS_NAME_from_sap.txt' # the path SAP GUI will write # 1. Pre-check the allow-list (read-only; informational + lets us skip the watcher). & "$shared\sap_gui_security_precheck.ps1" -Path $out -Access w -System 'THE_SID' -Client 'THE_CLIENT' -Transaction 'SE24' | Out-Host $allowed = ($LASTEXITCODE -eq 0) @@ -922,7 +932,7 @@ if (-not $allowed) { } # 3. Run the check + download (32-bit cscript). If the dialog appears it blocks # here until the watcher dismisses it; then the download completes. -& 'C:/Windows/SysWOW64/cscript.exe' //NoLogo '{WORK_TEMP}\sap_se24_check_and_download_run.vbs' +& 'C:/Windows/SysWOW64/cscript.exe' //NoLogo '{RUN_TEMP}\sap_se24_check_and_download_run.vbs' # 4. Reap the watcher. if ($watcher) { $watcher | Wait-Process -Timeout 45 -ErrorAction SilentlyContinue } ``` @@ -940,13 +950,13 @@ if ($watcher) { $watcher | Wait-Process -Timeout 45 -ErrorAction SilentlyContinu ## Step B — Analyze and Fix Source -The source was downloaded to `{WORK_TEMP}\_from_sap.txt` (**UTF-8, no BOM** — SE24 Download menu saves in UTF-8). +The source was downloaded to `{RUN_TEMP}\_from_sap.txt` (**UTF-8, no BOM** — SE24 Download menu saves in UTF-8). **Important — file format:** SE24 Download saves the class in "class pool" format (compact lowercase ABAP OO syntax), not the full `CLASS ... DEFINITION PUBLIC ...` form. This is normal and correct for SE24. **1. Read the file:** ```powershell -$srcFile = '{WORK_TEMP}\_from_sap.txt' +$srcFile = '{RUN_TEMP}\_from_sap.txt' $text = [System.IO.File]::ReadAllText($srcFile, [System.Text.Encoding]::UTF8) Write-Host $text ``` @@ -956,8 +966,8 @@ Write this to a `.ps1` file and run it — do not pass inline to `powershell -Co **3. Apply fixes and write fixed file (UTF-8 without BOM):** ```powershell -$srcFile = '{WORK_TEMP}\_from_sap.txt' -$fixedFile = '{WORK_TEMP}\_fixed.txt' +$srcFile = '{RUN_TEMP}\_from_sap.txt' +$fixedFile = '{RUN_TEMP}\_fixed.txt' $text = [System.IO.File]::ReadAllText($srcFile, [System.Text.Encoding]::UTF8) # Apply fixes — example: $text = $text -replace '(?i)bad_pattern', 'correct_replacement' @@ -973,7 +983,7 @@ After all fixes are applied, proceed to Step C. ## Step C — Re-upload Fixed Source -Run the **Step 5a (Update)** flow with `{WORK_TEMP}\_fixed.txt` as `THE_SOURCE_PATH`. +Run the **Step 5a (Update)** flow with `{RUN_TEMP}\_fixed.txt` as `THE_SOURCE_PATH`. The update VBS uploads the fixed source, saves, activates (Ctrl+F3), and runs the syntax check. @@ -991,15 +1001,15 @@ The update VBS uploads the fixed source, saves, activates (Ctrl+F3), and runs th Delete all temporary files: ```bash -cmd /c del {WORK_TEMP}\sap_se24_check_run.vbs & del {WORK_TEMP}\sap_se24_check_run.ps1 & del {WORK_TEMP}\sap_se24_create_run.vbs & del {WORK_TEMP}\sap_se24_create_run.ps1 & del {WORK_TEMP}\sap_se24_update_run.vbs & del {WORK_TEMP}\sap_se24_update_run.ps1 & del {WORK_TEMP}\sap_se24_check_and_download_run.vbs & del {WORK_TEMP}\sap_se24_check_and_download_run.ps1 & del {WORK_TEMP}\sap_se24_change_props_run.vbs & del {WORK_TEMP}\sap_se24_change_props_run.ps1 & del {WORK_TEMP}\sap_se24_delete_run.vbs & del {WORK_TEMP}\sap_se24_delete_run.ps1 +cmd /c del {RUN_TEMP}\sap_se24_check_run.vbs & del {RUN_TEMP}\sap_se24_check_run.ps1 & del {RUN_TEMP}\sap_se24_create_run.vbs & del {RUN_TEMP}\sap_se24_create_run.ps1 & del {RUN_TEMP}\sap_se24_update_run.vbs & del {RUN_TEMP}\sap_se24_update_run.ps1 & del {RUN_TEMP}\sap_se24_check_and_download_run.vbs & del {RUN_TEMP}\sap_se24_check_and_download_run.ps1 & del {RUN_TEMP}\sap_se24_change_props_run.vbs & del {RUN_TEMP}\sap_se24_change_props_run.ps1 & del {RUN_TEMP}\sap_se24_delete_run.vbs & del {RUN_TEMP}\sap_se24_delete_run.ps1 ``` For fix mode, also delete: ```bash -cmd /c del {WORK_TEMP}\_from_sap.txt & del {WORK_TEMP}\_fixed.txt +cmd /c del {RUN_TEMP}\_from_sap.txt & del {RUN_TEMP}\_fixed.txt ``` -Also delete `{WORK_TEMP}\.abap` if the user pasted code (not a user-supplied file). +Also delete `{RUN_TEMP}\.abap` if the user pasted code (not a user-supplied file). --- @@ -1010,7 +1020,7 @@ Log the run-end record. Best-effort. On success: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_se24_run.json" -Status SUCCESS -ExitCode 0 -MetricsJson '{"gate":"DEPLOY","verdict":"PASS","syntax_errors":0,"activated":true}' +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_se24_run.json" -Status SUCCESS -ExitCode 0 -MetricsJson '{"gate":"DEPLOY","verdict":"PASS","syntax_errors":0,"activated":true}' ``` **Build-KPI enrichment (best-effort).** Populate `-MetricsJson` from this deploy: @@ -1022,7 +1032,7 @@ omit if you cannot read the markers. On failure: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_se24_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_se24_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" ``` Suggested ``: `SE24_FAILED`, `SE24_INACTIVE`, `SE24_LOCKED`, `TR_RESOLUTION_FAILED`, `GUI_TIMEOUT`. @@ -1041,10 +1051,10 @@ class's own method declaration is filed under the class with the METHOD in source was deployed. 1. Write the captured VBS stdout (per-finding `... Line N: ` lines) to - `{WORK_TEMP}\se24_output.txt`. + `{RUN_TEMP}\se24_output.txt`. 2. Run: ```bash - powershell -NoProfile -ExecutionPolicy Bypass -File "\scripts\sap_error_hints.ps1" -Action record -Source SE24 -CustomUrl "{custom_url}" -SourceFile "" -RawOutputFile "{WORK_TEMP}\se24_output.txt" -Program "" + powershell -NoProfile -ExecutionPolicy Bypass -File "\scripts\sap_error_hints.ps1" -Action record -Source SE24 -CustomUrl "{custom_url}" -SourceFile "" -RawOutputFile "{RUN_TEMP}\se24_output.txt" -Program "" ``` Report `STATUS: RECORDED ...` as INFO. Non-zero exit is non-fatal. @@ -1071,7 +1081,7 @@ The VBS update template automatically handles ABAP source file encoding: When writing ABAP source files, always use **UTF-8 without BOM**: ```powershell -[System.IO.File]::WriteAllText("{WORK_TEMP}\file.abap", $content, (New-Object System.Text.UTF8Encoding $false)) +[System.IO.File]::WriteAllText("{RUN_TEMP}\file.abap", $content, (New-Object System.Text.UTF8Encoding $false)) ``` Do NOT use `Set-Content -Encoding UTF8` — it adds a BOM that causes SAP activation errors. diff --git a/plugins/sap-dev-core/skills/sap-se37/SKILL.md b/plugins/sap-dev-core/skills/sap-se37/SKILL.md index c6f9369..e19fd8b 100644 --- a/plugins/sap-dev-core/skills/sap-se37/SKILL.md +++ b/plugins/sap-dev-core/skills/sap-se37/SKILL.md @@ -59,7 +59,7 @@ Task: $ARGUMENTS **Resolve `work_dir` via the env-aware helper** — do NOT take `work_dir` from a direct `settings.json` read (that ignores the `SAPDEV_AI_WORK_DIR` env var and `userconfig.json`). Use the `WORK_DIR=` value printed by: ```bash -powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir))" +powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir)); Write-Output ('RUN_TEMP=' + (Get-SapRunTemp))" ``` The settings note below still applies to the OTHER keys. @@ -78,14 +78,24 @@ Ensure the temp directory exists: cmd /c if not exist "{WORK_TEMP}" mkdir "{WORK_TEMP}" ``` +Set `{RUN_TEMP}` = the `RUN_TEMP=` value printed above — a fresh per-run scratch +directory `{work_dir}\temp\run_`, already created by `Get-SapRunTemp`. +Resolve it **once here** and reuse the same value for the rest of this +invocation; it isolates this run's generated wrappers / state / scratch files so +concurrent runs (parallel sub-agents, multi-connection deploys) never collide. +**`{WORK_TEMP}` stays the base temp dir** and is used ONLY for +`Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'` (the session-attach plumbing +derives `{work_dir}\runtime` from its parent, so it must see the base path, not +the run dir). Everything the skill writes itself goes under `{RUN_TEMP}`. + --- ## Step 0.5 — Start Logging -Start a structured log run. State file: `{WORK_TEMP}\sap_se37_run.json`. Best-effort. +Start a structured log run. State file: `{RUN_TEMP}\sap_se37_run.json`. Best-effort. ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{WORK_TEMP}\sap_se37_run.json" -Skill sap-se37 -ParamsJson "{\"function_module\":\"\"}" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{RUN_TEMP}\sap_se37_run.json" -Skill sap-se37 -ParamsJson "{\"function_module\":\"\"}" ``` --- @@ -193,7 +203,7 @@ ENDFUNCTION. - Add `FUNCTION .` as the first line - Add the `*"---` Local Interface comment block (copy from existing FM or generate minimal) - Add `ENDFUNCTION.` as the last line -2. Write the source to: `{WORK_TEMP}\.abap` +2. Write the source to: `{RUN_TEMP}\.abap` 3. Confirm the file by reading back the first 5 lines. **If the user provided a file path:** @@ -217,7 +227,7 @@ The check VBScript template is at `./references/sap_se37_check.vbs`. ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se37_check_run.ps1`: +Write `{RUN_TEMP}\sap_se37_check_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_se37_check.vbs', [System.Text.Encoding]::UTF8) $content = $content -replace '%%FM_NAME%%','THE_FM_NAME' @@ -227,20 +237,20 @@ $content = $content -replace '%%SESSION_PATH%%', $sessionPath $content = $content -replace '%%ATTACH_LIB_VBS%%','\scripts\sap_attach_lib.vbs' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se37_check_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se37_check_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Replace `THE_FM_NAME` with the actual function module name (UPPERCASE) and `` with the absolute path to this skill directory. Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se37_check_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se37_check_run.ps1" ``` ### Execute ```bash -cscript //NoLogo {WORK_TEMP}\sap_se37_check_run.vbs +cscript //NoLogo {RUN_TEMP}\sap_se37_check_run.vbs ``` **Parse the last line of output:** @@ -331,7 +341,7 @@ ENDFUNCTION.` wrapper with the `*" Local Interface:` comment block listing all current parameters in the desired final state (used by the source upload and parsed by the PS1 generator below). -Write a single self-contained PS1 to `{WORK_TEMP}\sap_se37_update_run.ps1`: +Write a single self-contained PS1 to `{RUN_TEMP}\sap_se37_update_run.ps1`: ```powershell # ================================================================ @@ -495,8 +505,8 @@ $content = $content.Replace('%%SESSION_PATH%%', $sessionPath) $content = $content.Replace('%%ATTACH_LIB_VBS%%', '\scripts\sap_attach_lib.vbs') . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp $workTemp -[System.IO.File]::WriteAllText("$workTemp\sap_se37_update_run.vbs", $content, [System.Text.UnicodeEncoding]::new($false, $true)) -Write-Host "VBS written: $workTemp\sap_se37_update_run.vbs" +[System.IO.File]::WriteAllText("{RUN_TEMP}\sap_se37_update_run.vbs", $content, [System.Text.UnicodeEncoding]::new($false, $true)) +Write-Host "VBS written: {RUN_TEMP}\sap_se37_update_run.vbs" Write-Host 'Done' ``` @@ -516,7 +526,7 @@ Fill these placeholders before writing: Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se37_update_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se37_update_run.ps1" ``` ### Execute (with SAP GUI Security guard) @@ -549,7 +559,7 @@ if (-not $allowed) { } # 3. Run the upload + save + activate + syntax check (32-bit cscript). If the dialog # appears it blocks here until the watcher dismisses it; then the upload completes. -& 'C:/Windows/SysWOW64/cscript.exe' //NoLogo '{WORK_TEMP}\sap_se37_update_run.vbs' +& 'C:/Windows/SysWOW64/cscript.exe' //NoLogo '{RUN_TEMP}\sap_se37_update_run.vbs' # 4. Reap the watcher. if ($watcher) { $watcher | Wait-Process -Timeout 50 -ErrorAction SilentlyContinue } ``` @@ -568,7 +578,7 @@ The create VBScript template is at `./references/sap_se37_create.vbs`. ### Generate and run the complete PS1 -Write a single self-contained PS1 to `{WORK_TEMP}\sap_se37_create_run.ps1`. +Write a single self-contained PS1 to `{RUN_TEMP}\sap_se37_create_run.ps1`. Fill every `THE_*` placeholder with the actual value before writing. ```powershell @@ -779,7 +789,7 @@ $content = $content -replace '%%FM_NAME%%', $fmName $content = $content -replace '%%FUNC_GROUP%%', $funcGroup # Short text from a UTF-8 (no-BOM) file when present (never a PS literal -> avoids # cp932 mojibake of non-ASCII short text on PS 5.1); literal .Replace + VBS-quote escape. -if (Test-Path "$workTemp\se37_short_text.txt") { $shortText = ([System.IO.File]::ReadAllText("$workTemp\se37_short_text.txt", [System.Text.Encoding]::UTF8)).Trim() } +if (Test-Path "{RUN_TEMP}\se37_short_text.txt") { $shortText = ([System.IO.File]::ReadAllText("{RUN_TEMP}\se37_short_text.txt", [System.Text.Encoding]::UTF8)).Trim() } $content = $content.Replace('%%FM_SHORT_TEXT%%', $shortText.Replace('"','""')) $content = $content -replace '%%ABAP_SOURCE_FILE%%', $fmFilePath $content = $content -replace '%%PACKAGE%%', $package @@ -793,8 +803,8 @@ $content = $content -replace '%%SESSION_PATH%%', $sessionPath $content = $content -replace '%%ATTACH_LIB_VBS%%', '\scripts\sap_attach_lib.vbs' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp $workTemp -[System.IO.File]::WriteAllText("$workTemp\sap_se37_create_run.vbs", $content, [System.Text.UnicodeEncoding]::new($false, $true)) -Write-Host "VBS written: $workTemp\sap_se37_create_run.vbs" +[System.IO.File]::WriteAllText("{RUN_TEMP}\sap_se37_create_run.vbs", $content, [System.Text.UnicodeEncoding]::new($false, $true)) +Write-Host "VBS written: {RUN_TEMP}\sap_se37_create_run.vbs" Write-Host 'Done' ``` @@ -804,7 +814,7 @@ Fill these placeholders before writing: |---|---| | `THE_SOURCE_PATH` | Absolute path to FM source file (e.g. `C:\Temp\Z_HKFM_TEST006.txt`) | | `THE_FUNC_GROUP` | Function group (ask user if not in source) | -| `THE_SHORT_TEXT` | FM short description (ask user if not in source). For non-ASCII (ZH/JA), write it to `{WORK_TEMP}\se37_short_text.txt` (UTF-8, no BOM) via the Write tool — the generator reads that file instead of the literal to avoid cp932 mojibake. | +| `THE_SHORT_TEXT` | FM short description (ask user if not in source). For non-ASCII (ZH/JA), write it to `{RUN_TEMP}\se37_short_text.txt` (UTF-8, no BOM) via the Write tool — the generator reads that file instead of the literal to avoid cp932 mojibake. | | `THE_PACKAGE` | SAP package — blank for local $TMP | | `THE_TRANSPORT` | Transport request — blank for local | | `THE_SKILL_DIR` | Absolute path to this skill directory | @@ -812,7 +822,7 @@ Fill these placeholders before writing: Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se37_create_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se37_create_run.ps1" ``` Confirm the parse output matches the expected interface before proceeding. @@ -820,7 +830,7 @@ Confirm the parse output matches the expected interface before proceeding. ### Execute ```bash -cscript //NoLogo {WORK_TEMP}\sap_se37_create_run.vbs +cscript //NoLogo {RUN_TEMP}\sap_se37_create_run.vbs ``` Proceed to Step 6 to evaluate the result. @@ -862,12 +872,12 @@ run the VBS with no values (it will exit `DONE: NO_CHANGE`). ### Generate the filled-in VBScript **Short-text encoding (mandatory for non-ASCII).** Write the new short text to -`{WORK_TEMP}\se37_short_text.txt` as **UTF-8 (no BOM)** via the Write tool -- never +`{RUN_TEMP}\se37_short_text.txt` as **UTF-8 (no BOM)** via the Write tool -- never embed a ZH/JA/non-ASCII short text as a PowerShell literal (a BOM-less `.ps1` is read as the system ANSI codepage by PS 5.1 -> cp932 mojibake; cf. the se38 title fix). Leave the file absent/empty to leave the short text unchanged. -Write `{WORK_TEMP}\sap_se37_change_attrs_run.ps1`: +Write `{RUN_TEMP}\sap_se37_change_attrs_run.ps1`: ```powershell $skillDir = '' $tpl = "$skillDir\references\sap_se37_change_attrs.vbs" @@ -875,7 +885,7 @@ $content = [System.IO.File]::ReadAllText($tpl, [System.Text.Encoding]::UTF8) $content = $content.Replace('%%FM_NAME%%', 'THE_FM_NAME') # Short text: read from a UTF-8 (no-BOM) file so a non-ASCII short text is NEVER a # PS literal (BOM-less .ps1 -> system ANSI / cp932 mojibake on PS 5.1; cf. se38 title). -$stxt = if (Test-Path '{WORK_TEMP}\se37_short_text.txt') { ([System.IO.File]::ReadAllText('{WORK_TEMP}\se37_short_text.txt', [System.Text.Encoding]::UTF8)).Trim() } else { '' } +$stxt = if (Test-Path '{RUN_TEMP}\se37_short_text.txt') { ([System.IO.File]::ReadAllText('{RUN_TEMP}\se37_short_text.txt', [System.Text.Encoding]::UTF8)).Trim() } else { '' } $content = $content.Replace('%%SHORT_TEXT%%', $stxt.Replace('"','""')) $content = $content.Replace('%%PROCESSING_TYPE%%', 'THE_PROCESSING_TYPE') $content = $content.Replace('%%UPDATE_KIND%%', 'THE_UPDATE_KIND') @@ -887,7 +897,7 @@ $content = $content.Replace('%%SESSION_PATH%%', $sessionPath) $content = $content.Replace('%%ATTACH_LIB_VBS%%', '\scripts\sap_attach_lib.vbs') . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se37_change_attrs_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se37_change_attrs_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Use `.Replace()` (literal) — short text may contain regex metacharacters. @@ -895,13 +905,13 @@ Replace `` and the `THE_*` placeholders. Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se37_change_attrs_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se37_change_attrs_run.ps1" ``` ### Execute ```bash -cscript //NoLogo {WORK_TEMP}\sap_se37_change_attrs_run.vbs +cscript //NoLogo {RUN_TEMP}\sap_se37_change_attrs_run.vbs ``` ### Behaviour Notes @@ -986,7 +996,7 @@ VBS only aborts if SAP actually prompts. ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se37_reassign_fugr_run.ps1`: +Write `{RUN_TEMP}\sap_se37_reassign_fugr_run.ps1`: ```powershell $skillDir = '' $tpl = "$skillDir\references\sap_se37_reassign_fugr.vbs" @@ -1001,20 +1011,20 @@ $content = $content.Replace('%%SESSION_PATH%%', $sessionPath) $content = $content.Replace('%%ATTACH_LIB_VBS%%', '\scripts\sap_attach_lib.vbs') . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se37_reassign_fugr_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se37_reassign_fugr_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Replace `` and the `THE_*` placeholders. Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se37_reassign_fugr_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se37_reassign_fugr_run.ps1" ``` ### Execute ```bash -cscript //NoLogo {WORK_TEMP}\sap_se37_reassign_fugr_run.vbs +cscript //NoLogo {RUN_TEMP}\sap_se37_reassign_fugr_run.vbs ``` ### Behaviour Notes @@ -1082,7 +1092,7 @@ The delete VBScript template is at `./references/sap_se37_delete.vbs`. ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se37_delete_run.ps1`: +Write `{RUN_TEMP}\sap_se37_delete_run.ps1`: ```powershell $skillDir = '' $tpl = "$skillDir\references\sap_se37_delete.vbs" @@ -1095,20 +1105,20 @@ $content = $content.Replace('%%SESSION_PATH%%', $sessionPath) $content = $content.Replace('%%ATTACH_LIB_VBS%%', '\scripts\sap_attach_lib.vbs') . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se37_delete_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se37_delete_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Replace `` and the `THE_*` placeholders. Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se37_delete_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se37_delete_run.ps1" ``` ### Execute ```bash -cscript //NoLogo {WORK_TEMP}\sap_se37_delete_run.vbs +cscript //NoLogo {RUN_TEMP}\sap_se37_delete_run.vbs ``` ### Behaviour Notes @@ -1186,7 +1196,7 @@ Dim oShell Set oShell = oSession.findById("wnd[0]/usr/tabsFUNC_TAB_STRIP/tabpSOURCE/ssubSCREEN_HEADER:SAPLEDITOR_START:8430/cntlEDITOR/shellcont/shell") ' Read lines via GetLineText(n), 0-indexed, stop on error Dim oFSO : Set oFSO = CreateObject("Scripting.FileSystemObject") -Dim oFile : Set oFile = oFSO.CreateTextFile("{WORK_TEMP}\fm_src_from_sap.txt", True, True) +Dim oFile : Set oFile = oFSO.CreateTextFile("{RUN_TEMP}\fm_src_from_sap.txt", True, True) On Error Resume Next Dim i : For i = 0 To 500 Dim s : s = oShell.GetLineText(i) @@ -1202,15 +1212,15 @@ The resulting file is UTF-16 LE (because `CreateTextFile(..., True, True)` write ```powershell # Fix source file (UTF-16 LE), write fixed UTF-16 LE copy -$bytes = [System.IO.File]::ReadAllBytes('{WORK_TEMP}\fm_src_from_sap.txt') +$bytes = [System.IO.File]::ReadAllBytes('{RUN_TEMP}\fm_src_from_sap.txt') $enc = [System.Text.Encoding]::Unicode $text = $enc.GetString($bytes).TrimStart([char]0xFEFF) # Apply fixes — example: replace typo $text = $text -replace '(?i)bad_variable_name','correct_name' -[System.IO.File]::WriteAllText('{WORK_TEMP}\fm_src_fixed.txt', $text, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\fm_src_fixed.txt', $text, [System.Text.UnicodeEncoding]::new($false, $true)) ``` -Then run the **Step 5a update flow** with `%%ABAP_SOURCE_FILE%%` pointing to `{WORK_TEMP}\fm_src_fixed.txt`. +Then run the **Step 5a update flow** with `%%ABAP_SOURCE_FILE%%` pointing to `{RUN_TEMP}\fm_src_fixed.txt`. --- @@ -1222,7 +1232,7 @@ The check-and-download VBScript template is at `./references/sap_se37_check_and_ ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se37_check_and_download_run.ps1`: +Write `{RUN_TEMP}\sap_se37_check_and_download_run.ps1`: ```powershell $fmName = 'THE_FM_NAME' $outFile = 'THE_OUTPUT_FILE' @@ -1239,20 +1249,20 @@ $content = $content -replace '%%ATTACH_LIB_VBS%%', '\sc $content = $content -replace '%%SYNTAX_CHECK_LIB_VBS%%', '\scripts\sap_syntax_check_lib.vbs' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp $workTemp -[System.IO.File]::WriteAllText("$workTemp\sap_se37_check_and_download_run.vbs", $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText("{RUN_TEMP}\sap_se37_check_and_download_run.vbs", $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` | Placeholder | Value | |---|---| | `THE_FM_NAME` | Function module name (UPPERCASE) | -| `THE_OUTPUT_FILE` | `{WORK_TEMP}\_from_sap.txt` | +| `THE_OUTPUT_FILE` | `{RUN_TEMP}\_from_sap.txt` | | `THE_SKILL_DIR` | Absolute path to this skill directory | | `THE_WORK_TEMP` | `{WORK_TEMP}` resolved value | Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se37_check_and_download_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se37_check_and_download_run.ps1" ``` ### Execute (with SAP GUI Security guard) @@ -1268,7 +1278,7 @@ system / client: ```powershell $shared = '\scripts' -$out = '{WORK_TEMP}\THE_FM_NAME_from_sap.txt' # the path SAP GUI will write +$out = '{RUN_TEMP}\THE_FM_NAME_from_sap.txt' # the path SAP GUI will write # 1. Pre-check the allow-list (read-only; informational + lets us skip the watcher). & "$shared\sap_gui_security_precheck.ps1" -Path $out -Access w -System 'THE_SID' -Client 'THE_CLIENT' -Transaction 'SE37' | Out-Host $allowed = ($LASTEXITCODE -eq 0) @@ -1283,7 +1293,7 @@ if (-not $allowed) { } # 3. Run the check + download (32-bit cscript). If the dialog appears it blocks # here until the watcher dismisses it; then the download completes. -& 'C:/Windows/SysWOW64/cscript.exe' //NoLogo '{WORK_TEMP}\sap_se37_check_and_download_run.vbs' +& 'C:/Windows/SysWOW64/cscript.exe' //NoLogo '{RUN_TEMP}\sap_se37_check_and_download_run.vbs' # 4. Reap the watcher. if ($watcher) { $watcher | Wait-Process -Timeout 45 -ErrorAction SilentlyContinue } ``` @@ -1300,11 +1310,11 @@ if ($watcher) { $watcher | Wait-Process -Timeout 45 -ErrorAction SilentlyContinu ## Step B — Analyze and Fix Source -The source was downloaded to `{WORK_TEMP}\_from_sap.txt` (UTF-16 LE). +The source was downloaded to `{RUN_TEMP}\_from_sap.txt` (UTF-16 LE). **1. Read the file:** ```powershell -$bytes = [System.IO.File]::ReadAllBytes('{WORK_TEMP}\_from_sap.txt') +$bytes = [System.IO.File]::ReadAllBytes('{RUN_TEMP}\_from_sap.txt') $text = [System.Text.Encoding]::Unicode.GetString($bytes).TrimStart([char]0xFEFF) Write-Host $text ``` @@ -1319,7 +1329,7 @@ $text = $text -replace '(?i)bad_pattern', 'correct_replacement' **4. Write the fixed file:** ```powershell -[System.IO.File]::WriteAllText('{WORK_TEMP}\_fixed.txt', $text, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\_fixed.txt', $text, [System.Text.UnicodeEncoding]::new($false, $true)) ``` Repeat until all errors identified in Step A are addressed, then proceed to Step C. @@ -1328,7 +1338,7 @@ Repeat until all errors identified in Step A are addressed, then proceed to Step ## Step C — Re-upload Fixed Source -Run the **Step 5a (Update)** flow with `{WORK_TEMP}\_fixed.txt` as `THE_SOURCE_PATH`. +Run the **Step 5a (Update)** flow with `{RUN_TEMP}\_fixed.txt` as `THE_SOURCE_PATH`. The update VBS saves, activates, runs syntax check, and reports the result: @@ -1344,15 +1354,15 @@ The update VBS saves, activates, runs syntax check, and reports the result: Delete all temporary files: ```bash -cmd /c del {WORK_TEMP}\sap_se37_check_run.vbs & del {WORK_TEMP}\sap_se37_check_run.ps1 & del {WORK_TEMP}\sap_se37_create_run.vbs & del {WORK_TEMP}\sap_se37_create_run.ps1 & del {WORK_TEMP}\sap_se37_update_run.vbs & del {WORK_TEMP}\sap_se37_update_run.ps1 & del {WORK_TEMP}\sap_se37_check_and_download_run.vbs & del {WORK_TEMP}\sap_se37_check_and_download_run.ps1 & del {WORK_TEMP}\sap_se37_change_attrs_run.vbs & del {WORK_TEMP}\sap_se37_change_attrs_run.ps1 & del {WORK_TEMP}\sap_se37_reassign_fugr_run.vbs & del {WORK_TEMP}\sap_se37_reassign_fugr_run.ps1 & del {WORK_TEMP}\sap_se37_delete_run.vbs & del {WORK_TEMP}\sap_se37_delete_run.ps1 +cmd /c del {RUN_TEMP}\sap_se37_check_run.vbs & del {RUN_TEMP}\sap_se37_check_run.ps1 & del {RUN_TEMP}\sap_se37_create_run.vbs & del {RUN_TEMP}\sap_se37_create_run.ps1 & del {RUN_TEMP}\sap_se37_update_run.vbs & del {RUN_TEMP}\sap_se37_update_run.ps1 & del {RUN_TEMP}\sap_se37_check_and_download_run.vbs & del {RUN_TEMP}\sap_se37_check_and_download_run.ps1 & del {RUN_TEMP}\sap_se37_change_attrs_run.vbs & del {RUN_TEMP}\sap_se37_change_attrs_run.ps1 & del {RUN_TEMP}\sap_se37_reassign_fugr_run.vbs & del {RUN_TEMP}\sap_se37_reassign_fugr_run.ps1 & del {RUN_TEMP}\sap_se37_delete_run.vbs & del {RUN_TEMP}\sap_se37_delete_run.ps1 ``` For fix mode, also delete: ```bash -cmd /c del {WORK_TEMP}\_from_sap.txt & del {WORK_TEMP}\_fixed.txt +cmd /c del {RUN_TEMP}\_from_sap.txt & del {RUN_TEMP}\_fixed.txt ``` -Also delete `{WORK_TEMP}\.abap` if the user pasted code (not a user-supplied file). +Also delete `{RUN_TEMP}\.abap` if the user pasted code (not a user-supplied file). --- @@ -1363,7 +1373,7 @@ Log the run-end record. Best-effort. On success: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_se37_run.json" -Status SUCCESS -ExitCode 0 -MetricsJson '{"gate":"DEPLOY","verdict":"PASS","syntax_errors":0,"activated":true}' +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_se37_run.json" -Status SUCCESS -ExitCode 0 -MetricsJson '{"gate":"DEPLOY","verdict":"PASS","syntax_errors":0,"activated":true}' ``` **Build-KPI enrichment (best-effort).** Populate `-MetricsJson` from this deploy: @@ -1375,7 +1385,7 @@ omit if you cannot read the markers. On failure: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_se37_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_se37_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" ``` Suggested ``: `SE37_FAILED`, `SE37_INACTIVE`, `SE37_LOCKED`, `TR_RESOLUTION_FAILED`, `GUI_TIMEOUT`. @@ -1392,10 +1402,10 @@ file). Best-effort — never changes the deploy verdict. **Skip** when source was deployed. 1. Write the captured VBS stdout (the per-finding `... Line N: ` lines) - to `{WORK_TEMP}\se37_output.txt`. + to `{RUN_TEMP}\se37_output.txt`. 2. Run: ```bash - powershell -NoProfile -ExecutionPolicy Bypass -File "\scripts\sap_error_hints.ps1" -Action record -Source SE37 -CustomUrl "{custom_url}" -SourceFile "" -RawOutputFile "{WORK_TEMP}\se37_output.txt" -Program "" + powershell -NoProfile -ExecutionPolicy Bypass -File "\scripts\sap_error_hints.ps1" -Action record -Source SE37 -CustomUrl "{custom_url}" -SourceFile "" -RawOutputFile "{RUN_TEMP}\se37_output.txt" -Program "" ``` Report `STATUS: RECORDED ...` as INFO. Non-zero exit is non-fatal. diff --git a/plugins/sap-dev-core/skills/sap-se38/SKILL.md b/plugins/sap-dev-core/skills/sap-se38/SKILL.md index 71f1cbf..7937103 100644 --- a/plugins/sap-dev-core/skills/sap-se38/SKILL.md +++ b/plugins/sap-dev-core/skills/sap-se38/SKILL.md @@ -45,7 +45,7 @@ Task: $ARGUMENTS | `/rules/language_independence_rules.md` | GUI-scripting language independence — identify by component ID + DDIC field name, status-bar checks via `MessageType` codes (S/W/E/I/A), VKey instead of menu-text, no branching on `.Text`/`.Tooltip`/window titles | | `/rules/abap_code_quality_rules.md` | ABAP code-quality rules — deployed program source must follow modern syntax, OOP scaffolds, no literal MESSAGE strings, perf-band-appropriate SQL. Run `/sap-check-abap` before deploy when the source isn't generator-emitted. | | `/scripts/sap_log_lib.ps1` | Structured logger. Driven via the shared `sap_log_helper.ps1` wrapper that persists `run_id` between skill steps. | -| `/scripts/sap_log_helper.ps1` | Shared start/step/end wrapper around `sap_log_lib.ps1`. Persists run state to `{WORK_TEMP}\sap_se38_run.json` so this skill's discrete bash blocks share one logical run. Logging is best-effort and never breaks the skill. | +| `/scripts/sap_log_helper.ps1` | Shared start/step/end wrapper around `sap_log_lib.ps1`. Persists run state to `{RUN_TEMP}\sap_se38_run.json` so this skill's discrete bash blocks share one logical run. Logging is best-effort and never breaks the skill. | | `/scripts/sap_error_hints.ps1` | frequently_errors recorder. Step 6b feeds deploy syntax/activation errors (`-Action record -Source SE38 -RawOutputFile ...`) so FM/METHOD-related failures are captured to the team store. Best-effort; never changes the verdict. | | `/rules/sap_gui_security_handling.md` | SAP GUI Security dialog handling — the check-and-fix **source download** (Step A) is SAP-GUI-side file IO, so it can raise the modal "SAP GUI Security" dialog (which suspends the Scripting API and hangs cscript). Pre-check + OS-level watcher wrap that download. | | `/scripts/sap_gui_security_precheck.ps1` | Read-only allow-list pre-check (`saprules.xml`) — `ALLOWED` (exit 0) / `NOT_COVERED` (exit 1). Used by Step A before the source download. | @@ -58,7 +58,7 @@ Task: $ARGUMENTS **Resolve `work_dir` via the env-aware helper** — do NOT take `work_dir` from a direct `settings.json` read (that ignores the `SAPDEV_AI_WORK_DIR` env var and `userconfig.json`). Use the `WORK_DIR=` value printed by: ```bash -powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir))" +powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir)); Write-Output ('RUN_TEMP=' + (Get-SapRunTemp))" ``` The settings note below still applies to the OTHER keys. @@ -77,6 +77,16 @@ Ensure the temp directory exists: cmd /c if not exist "{WORK_TEMP}" mkdir "{WORK_TEMP}" ``` +Set `{RUN_TEMP}` = the `RUN_TEMP=` value printed above — a fresh per-run scratch +directory `{work_dir}\temp\run_`, already created by `Get-SapRunTemp`. +Resolve it **once here** and reuse the same value for the rest of this +invocation; it isolates this run's generated wrappers / state / scratch files so +concurrent runs (parallel sub-agents, multi-connection deploys) never collide. +**`{WORK_TEMP}` stays the base temp dir** and is used ONLY for +`Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'` (the session-attach plumbing +derives `{work_dir}\runtime` from its parent, so it must see the base path, not +the run dir). Everything the skill writes itself goes under `{RUN_TEMP}`. + --- ## Step 0.5 — Start Logging @@ -86,7 +96,7 @@ the `run_id` to a state file so subsequent steps and Step 7 can append to the same run. Logging is best-effort: if `userConfig.log_enabled` is `false` or the lib can't load, the helper silently no-ops. -State file: `{WORK_TEMP}\sap_se38_run.json` +State file: `{RUN_TEMP}\sap_se38_run.json` Build a JSON params object with the values gathered in Step 1 (omit any that are blank). Always include `program`. Include `mode` (`create` / @@ -95,7 +105,7 @@ that are blank). Always include `program`. Include `mode` (`create` / will mask known-sensitive keys, but don't add them in the first place. ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{WORK_TEMP}\sap_se38_run.json" -Skill sap-se38 -ParamsJson "{\"program\":\"\",\"mode\":\"\",\"package\":\"\",\"transport\":\"\"}" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{RUN_TEMP}\sap_se38_run.json" -Skill sap-se38 -ParamsJson "{\"program\":\"\",\"mode\":\"\",\"package\":\"\",\"transport\":\"\"}" ``` (Replace `` / `` with empty strings if not yet known — @@ -150,13 +160,13 @@ subsequent VBS templates. If `/sap-transport-request` reports `ERROR`, stop and surface it to the user. ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action step -StateFile "{WORK_TEMP}\sap_se38_run.json" -Step "transport" -Message "resolved TR=" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action step -StateFile "{RUN_TEMP}\sap_se38_run.json" -Step "transport" -Message "resolved TR=" ``` If the TR resolution failed, end the log run with FAILED before stopping: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_se38_run.json" -Status FAILED -ExitCode 2 -ErrorClass TR_RESOLUTION_FAILED -ErrorMsg "" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_se38_run.json" -Status FAILED -ExitCode 2 -ErrorClass TR_RESOLUTION_FAILED -ErrorMsg "" ``` See `/rules/tr_resolution.md` for the full policy. @@ -167,7 +177,7 @@ See `/rules/tr_resolution.md` for the full policy. **If the user pasted source code directly:** -1. Write the source to: `{WORK_TEMP}\.abap` +1. Write the source to: `{RUN_TEMP}\.abap` - Use the Write tool with the exact ABAP source as content. 2. Confirm the file by reading back the first 5 lines. @@ -204,7 +214,7 @@ The check VBScript template is at `./references/sap_se38_check.vbs`. ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se38_check_run.ps1`: +Write `{RUN_TEMP}\sap_se38_check_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_se38_check.vbs', [System.Text.Encoding]::UTF8) $content = $content -replace '%%PROGRAM_NAME%%','THE_PROGRAM_NAME' @@ -216,20 +226,20 @@ $content = $content -replace '%%SESSION_PATH%%', $sessionPath $content = $content -replace '%%ATTACH_LIB_VBS%%','\scripts\sap_attach_lib.vbs' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se38_check_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se38_check_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Replace `THE_PROGRAM_NAME` with the actual program name (UPPERCASE) and `` with the absolute path to this skill directory. Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se38_check_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se38_check_run.ps1" ``` ### Execute ```bash -cscript //NoLogo {WORK_TEMP}\sap_se38_check_run.vbs +cscript //NoLogo {RUN_TEMP}\sap_se38_check_run.vbs ``` **Parse the last line of output:** @@ -239,12 +249,12 @@ cscript //NoLogo {WORK_TEMP}\sap_se38_check_run.vbs Log the result: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action step -StateFile "{WORK_TEMP}\sap_se38_run.json" -Step "check" -Message "" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action step -StateFile "{RUN_TEMP}\sap_se38_run.json" -Step "check" -Message "" ``` If the check returned `ERROR:`, end the run with FAILED: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_se38_run.json" -Status FAILED -ExitCode 1 -ErrorClass SE38_CHECK_FAILED -ErrorMsg "" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_se38_run.json" -Status FAILED -ExitCode 1 -ErrorClass SE38_CHECK_FAILED -ErrorMsg "" ``` --- @@ -294,7 +304,7 @@ for object →TR linkage, `E070-TRSTATUS` for TR modifiable state. ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se38_update_run.ps1`: +Write `{RUN_TEMP}\sap_se38_update_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_se38_update.vbs', [System.Text.Encoding]::UTF8) $content = $content -replace '%%PROGRAM_NAME%%','THE_PROGRAM_NAME' @@ -317,7 +327,7 @@ $content = $content -replace '%%POST_ACTIVATE_VERIFY_VBS%%','\scripts\sap_se38_post_activate_verify.ps1' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se38_update_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se38_update_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Replace `THE_PROGRAM_NAME` (UPPERCASE), `THE_PROGRAM_TYPE` (`I` when updating an **Include** program — e.g. a function-exit `ZX…` include — so the verify skips the F8 run-test; otherwise empty or `1`), `THE_SOURCE_PATH` (absolute path with backslashes), `THE_PACKAGE` (SAP package or empty string), `THE_TRANSPORT` (transport number or empty string), ``, and `` (absolute path to `plugins/sap-dev-core/shared/`). @@ -330,13 +340,16 @@ Replace `THE_PROGRAM_NAME` (UPPERCASE), `THE_PROGRAM_TYPE` (`I` when updating an Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se38_update_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se38_update_run.ps1" ``` ### Execute ```bash -cscript //NoLogo {WORK_TEMP}\sap_se38_update_run.vbs +# SE38 stages ABAP source on the Windows clipboard + SendKeys ^v behind an OS +# foreground guard -- both machine-global singletons that a per-run folder cannot +# isolate. Serialize the paste across concurrent runs with a global named mutex. +powershell -NoProfile -ExecutionPolicy Bypass -File "\scripts\sap_run_with_lock.ps1" -MutexName SapDevGuiPaste_v1 -TimeoutMs 180000 -Command "cscript //NoLogo {RUN_TEMP}\sap_se38_update_run.vbs" ``` Proceed to Step 6 to evaluate the result. @@ -354,7 +367,7 @@ Program type codes: `1`=Executable, `I`=Include, `M`=Module Pool, `F`=Function G The create VBScript template is at `./references/sap_se38_create.vbs`. **Title encoding (mandatory).** Write the program title to -`{WORK_TEMP}\se38_title.txt` as **UTF-8 (no BOM)** using the Write tool — never +`{RUN_TEMP}\se38_title.txt` as **UTF-8 (no BOM)** using the Write tool — never embed a ZH/JA/non-ASCII title as a PowerShell literal in the generator below. PowerShell 5.1 reads a BOM-less `.ps1` as the system ANSI codepage (cp932 on a JP box) and mojibakes the title before it reaches the VBS (this corrupted the @@ -363,7 +376,7 @@ back via `[IO.File]::ReadAllText(...,UTF8)`, so the `.ps1` itself stays ASCII. ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se38_create_run.ps1`: +Write `{RUN_TEMP}\sap_se38_create_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_se38_create.vbs', [System.Text.Encoding]::UTF8) $content = $content -replace '%%PROGRAM_NAME%%','THE_PROGRAM_NAME' @@ -372,7 +385,7 @@ $content = $content -replace '%%PROGRAM_TYPE%%','THE_PROGRAM_TYPE' # (a BOM-less .ps1 is read as the system ANSI codepage by PS 5.1 -> cp932 mojibake; # the *57 ZMMRMAT057R01 title bug, 2026-06-07). Literal .Replace; double any " for # the VBS string literal. -$title = if (Test-Path '{WORK_TEMP}\se38_title.txt') { ([System.IO.File]::ReadAllText('{WORK_TEMP}\se38_title.txt', [System.Text.Encoding]::UTF8)).Trim() } else { '' } +$title = if (Test-Path '{RUN_TEMP}\se38_title.txt') { ([System.IO.File]::ReadAllText('{RUN_TEMP}\se38_title.txt', [System.Text.Encoding]::UTF8)).Trim() } else { '' } $content = $content.Replace('%%PROGRAM_TITLE%%', $title.Replace('"','""')) $content = $content -replace '%%ABAP_SOURCE_FILE%%','THE_SOURCE_PATH' $content = $content -replace '%%PACKAGE%%','THE_PACKAGE' @@ -390,20 +403,23 @@ $content = $content -replace '%%POST_ACTIVATE_VERIFY_VBS%%','\scripts\sap_se38_post_activate_verify.ps1' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se38_create_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se38_create_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Replace all `THE_*` placeholders and ``. `THE_PACKAGE` and `THE_TRANSPORT` follow the same rules as Step 5a. Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se38_create_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se38_create_run.ps1" ``` ### Execute ```bash -cscript //NoLogo {WORK_TEMP}\sap_se38_create_run.vbs +# SE38 stages ABAP source on the Windows clipboard + SendKeys ^v behind an OS +# foreground guard -- both machine-global singletons that a per-run folder cannot +# isolate. Serialize the paste across concurrent runs with a global named mutex. +powershell -NoProfile -ExecutionPolicy Bypass -File "\scripts\sap_run_with_lock.ps1" -MutexName SapDevGuiPaste_v1 -TimeoutMs 180000 -Command "cscript //NoLogo {RUN_TEMP}\sap_se38_create_run.vbs" ``` Proceed to Step 6 to evaluate the result. @@ -505,13 +521,13 @@ The text element VBScript template is at `./references/sap_se38_text_elements.vb Write the selection texts to a separate UTF-8 file first (avoids encoding issues when the PowerShell script itself contains multibyte characters like Japanese): -Write `{WORK_TEMP}\sap_se38_textelm_seltexts.txt` with just the pipe-delimited selection texts: +Write `{RUN_TEMP}\sap_se38_textelm_seltexts.txt` with just the pipe-delimited selection texts: ``` PARAM1=Text1|PARAM2=Text2 ``` If the source `.text_elements.txt` had a `[TEXT_SYMBOLS]` block, also write -`{WORK_TEMP}\sap_se38_textelm_symbols.txt` with the pipe-delimited symbols: +`{RUN_TEMP}\sap_se38_textelm_symbols.txt` with the pipe-delimited symbols: ``` 001=Selection|002=Result Output|T01=Seq ``` @@ -519,15 +535,15 @@ If the source `.text_elements.txt` had a `[TEXT_SYMBOLS]` block, also write The symbols file is OPTIONAL — when absent, the VBS still applies Selection Texts as before. Both files use UTF-8 (no BOM). -Write `{WORK_TEMP}\sap_se38_textelm_run.ps1`: +Write `{RUN_TEMP}\sap_se38_textelm_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_se38_text_elements.vbs', [System.Text.Encoding]::UTF8) $content = $content.Replace('%%PROGRAM_NAME%%','THE_PROGRAM_NAME') -$selTexts = [System.IO.File]::ReadAllText('{WORK_TEMP}\sap_se38_textelm_seltexts.txt', [System.Text.Encoding]::UTF8).Trim() +$selTexts = [System.IO.File]::ReadAllText('{RUN_TEMP}\sap_se38_textelm_seltexts.txt', [System.Text.Encoding]::UTF8).Trim() $content = $content.Replace('%%SELECTION_TEXTS%%', $selTexts) $txtSyms = '' -if (Test-Path '{WORK_TEMP}\sap_se38_textelm_symbols.txt') { - $txtSyms = [System.IO.File]::ReadAllText('{WORK_TEMP}\sap_se38_textelm_symbols.txt', [System.Text.Encoding]::UTF8).Trim() +if (Test-Path '{RUN_TEMP}\sap_se38_textelm_symbols.txt') { + $txtSyms = [System.IO.File]::ReadAllText('{RUN_TEMP}\sap_se38_textelm_symbols.txt', [System.Text.Encoding]::UTF8).Trim() } $content = $content.Replace('%%TEXT_SYMBOLS%%', $txtSyms) $content = $content.Replace('%%PACKAGE%%','THE_PACKAGE') @@ -538,7 +554,7 @@ $content = $content.Replace('%%SESSION_PATH%%', $sessionPath) $content = $content.Replace('%%ATTACH_LIB_VBS%%', '\scripts\sap_attach_lib.vbs') . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se38_textelm_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se38_textelm_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Replace `THE_PROGRAM_NAME` (UPPERCASE), `THE_PACKAGE`, `THE_TRANSPORT`, and ``. @@ -552,13 +568,13 @@ Replace `THE_PROGRAM_NAME` (UPPERCASE), `THE_PACKAGE`, `THE_TRANSPORT`, and `\scripts\sap_log_helper.ps1" -Action step -StateFile "{WORK_TEMP}\sap_se38_run.json" -Step "text_elements" -Message "" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action step -StateFile "{RUN_TEMP}\sap_se38_run.json" -Step "text_elements" -Message "" ``` **Known FAILED reasons (emit-side contract — sap_se38_text_elements.vbs):** @@ -734,13 +750,13 @@ VBS with no values (it will exit `DONE: NO_CHANGE`). ### Generate the filled-in VBScript **Title encoding (mandatory when changing the title).** Write the new title to -`{WORK_TEMP}\se38_title.txt` as **UTF-8 (no BOM)** via the Write tool — never +`{RUN_TEMP}\se38_title.txt` as **UTF-8 (no BOM)** via the Write tool — never embed a non-ASCII title as a PowerShell literal (a BOM-less `.ps1` is read as the system ANSI codepage by PS 5.1 → cp932 mojibake). The generator reads it back via `[IO.File]::ReadAllText(...,UTF8)`. Leave the file absent (or empty) to leave the title unchanged. -Write `{WORK_TEMP}\sap_se38_change_attrs_run.ps1`: +Write `{RUN_TEMP}\sap_se38_change_attrs_run.ps1`: ```powershell $skillDir = '' $tpl = "$skillDir\references\sap_se38_change_attrs.vbs" @@ -749,7 +765,7 @@ $content = $content.Replace('%%PROGRAM_NAME%%','THE_PROGRAM_NAME') # Title: read from a UTF-8 (no-BOM) file (never a PS literal -> BOM-less .ps1 is read as # the system ANSI codepage by PS 5.1 -> cp932 mojibake of ZH/JA titles). Absent/empty # file -> empty token (= leave the title unchanged). Double any " for the VBS literal. -$title = if (Test-Path '{WORK_TEMP}\se38_title.txt') { ([System.IO.File]::ReadAllText('{WORK_TEMP}\se38_title.txt', [System.Text.Encoding]::UTF8)).Trim() } else { '' } +$title = if (Test-Path '{RUN_TEMP}\se38_title.txt') { ([System.IO.File]::ReadAllText('{RUN_TEMP}\se38_title.txt', [System.Text.Encoding]::UTF8)).Trim() } else { '' } $content = $content.Replace('%%TITLE%%', $title.Replace('"','""')) $content = $content.Replace('%%STATUS%%', 'THE_STATUS') $content = $content.Replace('%%TYPE%%', 'THE_TYPE') @@ -761,7 +777,7 @@ $content = $content.Replace('%%SESSION_PATH%%', $sessionPath) $content = $content.Replace('%%ATTACH_LIB_VBS%%', '\scripts\sap_attach_lib.vbs') . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se38_change_attrs_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se38_change_attrs_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Use `.Replace()` (literal) — title/status texts may contain regex @@ -769,13 +785,13 @@ metacharacters. Replace `` and the `THE_*` placeholders. Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se38_change_attrs_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se38_change_attrs_run.ps1" ``` ### Execute ```bash -cscript //NoLogo {WORK_TEMP}\sap_se38_change_attrs_run.vbs +cscript //NoLogo {RUN_TEMP}\sap_se38_change_attrs_run.vbs ``` ### Behaviour Notes @@ -860,7 +876,7 @@ The delete VBScript template is at `./references/sap_se38_delete.vbs`. ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se38_delete_run.ps1`: +Write `{RUN_TEMP}\sap_se38_delete_run.ps1`: ```powershell $skillDir = '' $tpl = "$skillDir\references\sap_se38_delete.vbs" @@ -873,20 +889,20 @@ $content = $content.Replace('%%SESSION_PATH%%', $sessionPath) $content = $content.Replace('%%ATTACH_LIB_VBS%%', '\scripts\sap_attach_lib.vbs') . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se38_delete_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se38_delete_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Replace `` and the `THE_*` placeholders. Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se38_delete_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se38_delete_run.ps1" ``` ### Execute ```bash -cscript //NoLogo {WORK_TEMP}\sap_se38_delete_run.vbs +cscript //NoLogo {RUN_TEMP}\sap_se38_delete_run.vbs ``` ### Behaviour Notes @@ -965,7 +981,7 @@ SUCCESS line. - Show the full script output as a code block. - Log the SUCCESS end record: ```bash - powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_se38_run.json" -Status SUCCESS -ExitCode 0 -MetricsJson '{"gate":"DEPLOY","verdict":"PASS","syntax_errors":0,"activated":true,"text_elements":"APPLIED"}' + powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_se38_run.json" -Status SUCCESS -ExitCode 0 -MetricsJson '{"gate":"DEPLOY","verdict":"PASS","syntax_errors":0,"activated":true,"text_elements":"APPLIED"}' ``` **Build-KPI enrichment (best-effort).** Populate `-MetricsJson` from this deploy: `syntax_errors` from the `SYNTAX_ERRORS:` marker, `activated` from the @@ -1000,7 +1016,7 @@ Log the FAILED end record (pick `ErrorClass` from the matched row, e.g. `SE38_SYNTAX`, `SE38_INACTIVE`, `SE38_UPLOAD`, `SE38_LOCKED`, `SE38_AUTH`, `SE38_GENERIC`, `SE38_TEXTELM_*`): ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_se38_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_se38_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" ``` ### Step 6b — Record FM/METHOD errors to frequently_errors (best-effort) @@ -1019,11 +1035,11 @@ This is best-effort and MUST NOT change the deploy verdict. **Skip** when / attribute-change). 1. Write the captured VBS stdout (the `[ERROR] Line N: ` lines you - already see) verbatim to `{WORK_TEMP}\se38_output.txt`. + already see) verbatim to `{RUN_TEMP}\se38_output.txt`. 2. Run (CANDIDATE rows do not influence generation until `/sap-error-kb` promotes them): ```bash - powershell -NoProfile -ExecutionPolicy Bypass -File "\scripts\sap_error_hints.ps1" -Action record -Source SE38 -CustomUrl "{custom_url}" -SourceFile "" -RawOutputFile "{WORK_TEMP}\se38_output.txt" -Program "" + powershell -NoProfile -ExecutionPolicy Bypass -File "\scripts\sap_error_hints.ps1" -Action record -Source SE38 -CustomUrl "{custom_url}" -SourceFile "" -RawOutputFile "{RUN_TEMP}\se38_output.txt" -Program "" ``` Parse `STATUS: RECORDED added= updated= skipped=`; report it as an INFO note. A non-zero exit here is non-fatal — log and continue. @@ -1040,12 +1056,13 @@ The check-and-download VBScript template is at `./references/sap_se38_check_and_ ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se38_check_and_download_run.ps1`: +Write `{RUN_TEMP}\sap_se38_check_and_download_run.ps1`: ```powershell $pgmName = 'THE_PROGRAM_NAME' $outFile = 'THE_OUTPUT_FILE' $skillDir = 'THE_SKILL_DIR' -$workTemp = 'THE_WORK_TEMP' +$workTemp = 'THE_WORK_TEMP' # base {WORK_TEMP} -- feeds Get-SapCurrentSessionPath only +$runTemp = 'THE_RUN_TEMP' # per-run scratch dir -- where the generated wrapper lands $content = [System.IO.File]::ReadAllText("$skillDir\references\sap_se38_check_and_download.vbs", [System.Text.Encoding]::UTF8) $content = $content -replace '%%PROGRAM_NAME%%', $pgmName @@ -1057,20 +1074,21 @@ $content = $content -replace '%%ATTACH_LIB_VBS%%', '\sc $content = $content -replace '%%SYNTAX_CHECK_LIB_VBS%%', '\scripts\sap_syntax_check_lib.vbs' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp $workTemp -[System.IO.File]::WriteAllText("$workTemp\sap_se38_check_and_download_run.vbs", $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText("$runTemp\sap_se38_check_and_download_run.vbs", $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` | Placeholder | Value | |---|---| | `THE_PROGRAM_NAME` | Program name (UPPERCASE) | -| `THE_OUTPUT_FILE` | `{WORK_TEMP}\_from_sap.txt` | +| `THE_OUTPUT_FILE` | `{RUN_TEMP}\_from_sap.txt` | | `THE_SKILL_DIR` | Absolute path to this skill directory | -| `THE_WORK_TEMP` | `{WORK_TEMP}` resolved value | +| `THE_WORK_TEMP` | `{WORK_TEMP}` resolved value (base — session-attach only) | +| `THE_RUN_TEMP` | `{RUN_TEMP}` resolved value (per-run scratch dir) | Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se38_check_and_download_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se38_check_and_download_run.ps1" ``` ### Execute (with SAP GUI Security guard) @@ -1086,7 +1104,7 @@ system / client: ```powershell $shared = '\scripts' -$out = '{WORK_TEMP}\THE_PROGRAM_NAME_from_sap.txt' # the path SAP GUI will write +$out = '{RUN_TEMP}\THE_PROGRAM_NAME_from_sap.txt' # the path SAP GUI will write # 1. Pre-check the allow-list (read-only; informational + lets us skip the watcher). & "$shared\sap_gui_security_precheck.ps1" -Path $out -Access w -System 'THE_SID' -Client 'THE_CLIENT' -Transaction 'SE38' | Out-Host $allowed = ($LASTEXITCODE -eq 0) @@ -1101,7 +1119,7 @@ if (-not $allowed) { } # 3. Run the check + download (32-bit cscript). If the dialog appears it blocks # here until the watcher dismisses it; then the download completes. -& 'C:/Windows/SysWOW64/cscript.exe' //NoLogo '{WORK_TEMP}\sap_se38_check_and_download_run.vbs' +& 'C:/Windows/SysWOW64/cscript.exe' //NoLogo '{RUN_TEMP}\sap_se38_check_and_download_run.vbs' # 4. Reap the watcher. if ($watcher) { $watcher | Wait-Process -Timeout 45 -ErrorAction SilentlyContinue } ``` @@ -1118,7 +1136,7 @@ if ($watcher) { $watcher | Wait-Process -Timeout 45 -ErrorAction SilentlyContinu ## Step B — Analyze and Fix Source -The source was downloaded to `{WORK_TEMP}\_from_sap.txt` (UTF-16 LE). +The source was downloaded to `{RUN_TEMP}\_from_sap.txt` (UTF-16 LE). > **CAVEAT — `_from_sap.txt` may differ structurally from the disk source.** > The download uses `AbapEditor.GetLineText(i)` which reads what the editor @@ -1138,7 +1156,7 @@ The source was downloaded to `{WORK_TEMP}\_from_sap.txt` (UTF-16 L **1. Read the file:** ```powershell -$srcFile = '{WORK_TEMP}\_from_sap.txt' +$srcFile = '{RUN_TEMP}\_from_sap.txt' $bytes = [System.IO.File]::ReadAllBytes($srcFile) $text = [System.Text.Encoding]::Unicode.GetString($bytes).TrimStart([char]0xFEFF) Write-Host $text @@ -1149,8 +1167,8 @@ Write this to a `.ps1` file and run it — do not pass inline to `powershell -Co **3. Apply fixes and write fixed file:** ```powershell -$srcFile = '{WORK_TEMP}\_from_sap.txt' -$fixedFile = '{WORK_TEMP}\_fixed.txt' +$srcFile = '{RUN_TEMP}\_from_sap.txt' +$fixedFile = '{RUN_TEMP}\_fixed.txt' $bytes = [System.IO.File]::ReadAllBytes($srcFile) $text = [System.Text.Encoding]::Unicode.GetString($bytes).TrimStart([char]0xFEFF) # Apply fixes — example: @@ -1166,7 +1184,7 @@ After all fixes are applied, proceed to Step C. ## Step C — Re-upload Fixed Source -Run the **Step 5a (Update)** flow with `{WORK_TEMP}\_fixed.txt` as `THE_SOURCE_PATH`. +Run the **Step 5a (Update)** flow with `{RUN_TEMP}\_fixed.txt` as `THE_SOURCE_PATH`. The update VBS saves, activates, runs syntax check, and verifies activation via SA38 F8. @@ -1183,23 +1201,17 @@ The update VBS saves, activates, runs syntax check, and verifies activation via If the log run hasn't already been ended (Step 6 success/failure path or Step 1b TR-failure path), end it now as a safety net: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_se38_run.json" -Status SUCCESS -ExitCode 0 +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_se38_run.json" -Status SUCCESS -ExitCode 0 ``` (The helper deletes the state file on `end` and silently no-ops if the file is already gone, so this is safe to call unconditionally.) -Delete all temporary files: +Delete this run's scratch directory — one shot removes every generated wrapper, +the log-state file, the pasted source, and any downloaded / fixed files: ```bash -cmd /c del {WORK_TEMP}\sap_se38_check_run.vbs & del {WORK_TEMP}\sap_se38_check_run.ps1 & del {WORK_TEMP}\sap_se38_create_run.vbs & del {WORK_TEMP}\sap_se38_create_run.ps1 & del {WORK_TEMP}\sap_se38_update_run.vbs & del {WORK_TEMP}\sap_se38_update_run.ps1 & del {WORK_TEMP}\sap_se38_textelm_run.vbs & del {WORK_TEMP}\sap_se38_textelm_run.ps1 & del {WORK_TEMP}\sap_se38_textelm_seltexts.txt & del {WORK_TEMP}\sap_se38_check_and_download_run.vbs & del {WORK_TEMP}\sap_se38_check_and_download_run.ps1 & del {WORK_TEMP}\sap_se38_change_attrs_run.vbs & del {WORK_TEMP}\sap_se38_change_attrs_run.ps1 & del {WORK_TEMP}\sap_se38_delete_run.vbs & del {WORK_TEMP}\sap_se38_delete_run.ps1 +cmd /c rmdir /s /q {RUN_TEMP} ``` -For fix mode, also delete: -```bash -cmd /c del {WORK_TEMP}\_from_sap.txt & del {WORK_TEMP}\_fixed.txt -``` - -Also delete `{WORK_TEMP}\.abap` if the user pasted code (not a user-supplied file). - --- ## Security Note @@ -1362,7 +1374,7 @@ WRITE output displayed on the list screen (screen 120) produces 55+ `GuiLabel` c cannot be reliably captured through SAP GUI Scripting. **Workaround:** Always add `GUI_DOWNLOAD` in the ABAP code to export results to a local file -(e.g. `{WORK_TEMP}\_result.txt`). Read the file after execution instead of attempting +(e.g. `{RUN_TEMP}\_result.txt`). Read the file after execution instead of attempting to scrape the list screen. --- diff --git a/plugins/sap-dev-core/skills/sap-se91/SKILL.md b/plugins/sap-dev-core/skills/sap-se91/SKILL.md index d9bbad9..3d3c29a 100644 --- a/plugins/sap-dev-core/skills/sap-se91/SKILL.md +++ b/plugins/sap-dev-core/skills/sap-se91/SKILL.md @@ -39,7 +39,7 @@ Task: $ARGUMENTS **Resolve `work_dir` via the env-aware helper** — do NOT take `work_dir` from a direct `settings.json` read (that ignores the `SAPDEV_AI_WORK_DIR` env var and `userconfig.json`). Use the `WORK_DIR=` value printed by: ```bash -powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir))" +powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir)); Write-Output ('RUN_TEMP=' + (Get-SapRunTemp))" ``` The settings note below still applies to the OTHER keys. @@ -58,14 +58,24 @@ Ensure the temp directory exists: cmd /c if not exist "{WORK_TEMP}" mkdir "{WORK_TEMP}" ``` +Set `{RUN_TEMP}` = the `RUN_TEMP=` value printed above — a fresh per-run scratch +directory `{work_dir}\temp\run_`, already created by `Get-SapRunTemp`. +Resolve it **once here** and reuse the same value for the rest of this +invocation; it isolates this run's generated wrappers / state / scratch files so +concurrent runs (parallel sub-agents, multi-connection deploys) never collide. +**`{WORK_TEMP}` stays the base temp dir** and is used ONLY for +`Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'` (the session-attach plumbing +derives `{work_dir}\runtime` from its parent, so it must see the base path, not +the run dir). Everything the skill writes itself goes under `{RUN_TEMP}`. + --- ## Step 0.5 — Start Logging -Start a structured log run. State file: `{WORK_TEMP}\sap_se91_run.json`. Best-effort. +Start a structured log run. State file: `{RUN_TEMP}\sap_se91_run.json`. Best-effort. ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{WORK_TEMP}\sap_se91_run.json" -Skill sap-se91 -ParamsJson "{\"message_class\":\"\"}" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{RUN_TEMP}\sap_se91_run.json" -Skill sap-se91 -ParamsJson "{\"message_class\":\"\"}" ``` --- @@ -122,7 +132,7 @@ Query table T100 via SE16 to find the current maximum message number: **If the user provided messages inline** (in the conversation): -1. Write the messages to: `{WORK_TEMP}\_messages.txt` +1. Write the messages to: `{RUN_TEMP}\_messages.txt` - Tab-separated format: `<3-digit-number>\t` per line. - Pad message numbers to 3 digits (e.g. `001`, `050`). - Message placeholders use `&1`, `&2`, `&3`, `&4` (max 4 per message). @@ -150,7 +160,7 @@ The check VBScript template is at `./references/sap_se91_check.vbs`. ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se91_check_run.ps1`: +Write `{RUN_TEMP}\sap_se91_check_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_se91_check.vbs', [System.Text.Encoding]::UTF8) $content = $content -replace '%%MSG_CLASS%%','THE_MSG_CLASS' @@ -160,20 +170,20 @@ $content = $content -replace '%%SESSION_PATH%%', $sessionPath $content = $content -replace '%%ATTACH_LIB_VBS%%','\scripts\sap_attach_lib.vbs' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se91_check_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se91_check_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Replace `THE_MSG_CLASS` with the actual message class name (UPPERCASE) and `` with the absolute path to this skill directory. Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se91_check_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se91_check_run.ps1" ``` ### Execute ```bash -cscript //NoLogo {WORK_TEMP}\sap_se91_check_run.vbs +cscript //NoLogo {RUN_TEMP}\sap_se91_check_run.vbs ``` **Parse the last line of output:** @@ -215,7 +225,7 @@ they configure sap-dev-core settings.json for future use. ### Sub-step B: Run the duplicate check -Write `{WORK_TEMP}\sap_se91_checkmsg_run.ps1`: +Write `{RUN_TEMP}\sap_se91_checkmsg_run.ps1`: ```powershell $content = Get-Content '\references\sap_se91_check_messages.ps1' -Raw $content = $content -replace '%%MSG_CLASS%%','THE_MSG_CLASS' @@ -227,14 +237,14 @@ $content = $content -replace '%%SAP_USER%%','' $content = $content -replace '%%SAP_PASSWORD%%','' $content = $content -replace '%%SAP_LANGUAGE%%','' $content = $content -replace '%%RFC_LIB_PS1%%', '\scripts\sap_rfc_lib.ps1' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se91_checkmsg_run.ps1', $content, [System.Text.Encoding]::UTF8) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se91_checkmsg_run.ps1', $content, [System.Text.Encoding]::UTF8) Write-Host 'Done' ``` Replace all `THE_*` placeholders and ``. Execute (must use 32-bit PowerShell for SAP NCo 3.1): ```bash -C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se91_checkmsg_run.ps1" +C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se91_checkmsg_run.ps1" ``` ### Sub-step C: Parse results and update messages file @@ -273,7 +283,7 @@ The update VBScript template is at `./references/sap_se91_update.vbs`. ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se91_update_run.ps1`: +Write `{RUN_TEMP}\sap_se91_update_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_se91_update.vbs', [System.Text.Encoding]::UTF8) $content = $content -replace '%%MSG_CLASS%%','THE_MSG_CLASS' @@ -286,20 +296,20 @@ $content = $content -replace '%%SESSION_PATH%%', $sessionPath $content = $content -replace '%%ATTACH_LIB_VBS%%','\scripts\sap_attach_lib.vbs' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se91_update_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se91_update_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Replace `THE_MSG_CLASS` (UPPERCASE), `THE_MESSAGES_FILE` (absolute path with backslashes), `THE_PACKAGE`, `THE_TRANSPORT`, and ``. If package/transport not provided, replace with empty strings. Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se91_update_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se91_update_run.ps1" ``` ### Execute ```bash -cscript //NoLogo {WORK_TEMP}\sap_se91_update_run.vbs +cscript //NoLogo {RUN_TEMP}\sap_se91_update_run.vbs ``` Proceed to Step 6 to evaluate the result. @@ -315,7 +325,7 @@ The create VBScript template is at `./references/sap_se91_create.vbs`. ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se91_create_run.ps1`: +Write `{RUN_TEMP}\sap_se91_create_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_se91_create.vbs', [System.Text.Encoding]::UTF8) $content = $content -replace '%%MSG_CLASS%%','THE_MSG_CLASS' @@ -329,7 +339,7 @@ $content = $content -replace '%%SESSION_PATH%%', $sessionPath $content = $content -replace '%%ATTACH_LIB_VBS%%','\scripts\sap_attach_lib.vbs' . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se91_create_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se91_create_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` @@ -356,13 +366,13 @@ Replace all `THE_*` placeholders and ``. If package/transport not pro Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se91_create_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se91_create_run.ps1" ``` ### Execute ```bash -cscript //NoLogo {WORK_TEMP}\sap_se91_create_run.vbs +cscript //NoLogo {RUN_TEMP}\sap_se91_create_run.vbs ``` Proceed to Step 6 to evaluate the result. @@ -424,7 +434,7 @@ Do not run the VBS with no values (it will exit `DONE: NO_CHANGE`). ### Generate the filled-in VBScript -Write `{WORK_TEMP}\sap_se91_change_props_run.ps1`: +Write `{RUN_TEMP}\sap_se91_change_props_run.ps1`: ```powershell $skillDir = '' $tpl = "$skillDir\references\sap_se91_change_props.vbs" @@ -439,7 +449,7 @@ $content = $content.Replace('%%SESSION_PATH%%', $sessionPath) $content = $content.Replace('%%ATTACH_LIB_VBS%%', '\scripts\sap_attach_lib.vbs') . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_se91_change_props_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_se91_change_props_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Use `.Replace()` (literal) — short-text values may contain regex @@ -447,13 +457,13 @@ metacharacters. Replace `` and the `THE_*` placeholders. Run: ```bash -powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_se91_change_props_run.ps1" +powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_se91_change_props_run.ps1" ``` ### Execute ```bash -cscript //NoLogo {WORK_TEMP}\sap_se91_change_props_run.vbs +cscript //NoLogo {RUN_TEMP}\sap_se91_change_props_run.vbs ``` ### Behaviour Notes @@ -539,10 +549,10 @@ After the dump, decide: Delete all temporary files: ```bash -cmd /c del {WORK_TEMP}\sap_se91_check_run.vbs & del {WORK_TEMP}\sap_se91_check_run.ps1 & del {WORK_TEMP}\sap_se91_create_run.vbs & del {WORK_TEMP}\sap_se91_create_run.ps1 & del {WORK_TEMP}\sap_se91_update_run.vbs & del {WORK_TEMP}\sap_se91_update_run.ps1 & del {WORK_TEMP}\sap_se91_checkmsg_run.ps1 & del {WORK_TEMP}\sap_se91_change_props_run.vbs & del {WORK_TEMP}\sap_se91_change_props_run.ps1 +cmd /c del {RUN_TEMP}\sap_se91_check_run.vbs & del {RUN_TEMP}\sap_se91_check_run.ps1 & del {RUN_TEMP}\sap_se91_create_run.vbs & del {RUN_TEMP}\sap_se91_create_run.ps1 & del {RUN_TEMP}\sap_se91_update_run.vbs & del {RUN_TEMP}\sap_se91_update_run.ps1 & del {RUN_TEMP}\sap_se91_checkmsg_run.ps1 & del {RUN_TEMP}\sap_se91_change_props_run.vbs & del {RUN_TEMP}\sap_se91_change_props_run.ps1 ``` -Also delete `{WORK_TEMP}\_messages.txt` if messages were created from inline input. +Also delete `{RUN_TEMP}\_messages.txt` if messages were created from inline input. --- @@ -553,13 +563,13 @@ Log the run-end record. Best-effort. On success: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_se91_run.json" -Status SUCCESS -ExitCode 0 +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_se91_run.json" -Status SUCCESS -ExitCode 0 ``` On failure: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_se91_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_se91_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" ``` Suggested ``: `SE91_FAILED`, `TR_RESOLUTION_FAILED`, `GUI_TIMEOUT`. diff --git a/plugins/sap-dev-core/skills/sap-transport-request/SKILL.md b/plugins/sap-dev-core/skills/sap-transport-request/SKILL.md index 6be5ae0..553887b 100644 --- a/plugins/sap-dev-core/skills/sap-transport-request/SKILL.md +++ b/plugins/sap-dev-core/skills/sap-transport-request/SKILL.md @@ -41,7 +41,7 @@ Task: $ARGUMENTS **Resolve `work_dir` via the env-aware helper** — do NOT take `work_dir` from a direct `settings.json` read (that ignores the `SAPDEV_AI_WORK_DIR` env var and `userconfig.json`). Use the `WORK_DIR=` value printed by: ```bash -powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir))" +powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir)); Write-Output ('RUN_TEMP=' + (Get-SapRunTemp))" ``` The settings note below still applies to the OTHER keys. @@ -60,14 +60,25 @@ Ensure the temp directory exists: cmd /c if not exist "{WORK_TEMP}" mkdir "{WORK_TEMP}" ``` +Set `{RUN_TEMP}` = the `RUN_TEMP=` value printed above — a fresh per-run scratch +directory `{work_dir}\temp\run_`, already created by `Get-SapRunTemp`. +Resolve it **once here** and reuse it; write this skill's OWN scratch (the +generated `sap_tr_run.*` and the `sap_tr_run.json` log-state file) under +`{RUN_TEMP}` so concurrent TR resolutions never collide on fixed names. When this +skill calls `/sap-se16n` to read `E070`, it passes its own +`{RUN_TEMP}\se16n_E070.txt` as the **explicit output path** so the producer +(se16n) and this consumer agree on the same per-run location (se16n otherwise +writes to ITS own run dir, which this skill cannot read). `{WORK_TEMP}` (base) is +kept only for the Step-0 definition above. + --- ## Step 0.5 — Start Logging -Start a structured log run. State file: `{WORK_TEMP}\sap_tr_run.json`. Best-effort. Honours `SAPDEV_PARENT_RUN_ID` env var so parent skill calls can be linked. +Start a structured log run. State file: `{RUN_TEMP}\sap_tr_run.json`. Best-effort. Honours `SAPDEV_PARENT_RUN_ID` env var so parent skill calls can be linked. ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{WORK_TEMP}\sap_tr_run.json" -Skill sap-transport-request -ParamsJson "{}" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{RUN_TEMP}\sap_tr_run.json" -Skill sap-transport-request -ParamsJson "{}" ``` --- @@ -182,10 +193,10 @@ Invoke `/sap-se16n` to read the candidate TR's `TRSTATUS` directly from table `E070`: ``` -/sap-se16n TABLE=E070 WHERE: TRKORR= SELECT: TRKORR TRSTATUS TRFUNCTION AS4USER +/sap-se16n TABLE=E070 WHERE: TRKORR= SELECT: TRKORR TRSTATUS TRFUNCTION AS4USER Output file={RUN_TEMP}\se16n_E070.txt ``` -Parse the resulting `{WORK_TEMP}\se16n_E070.txt`: +Parse the resulting `{RUN_TEMP}\se16n_E070.txt`: | Observation | Outcome | |---|---| @@ -237,7 +248,7 @@ create. The PowerShell template is at `/references/sap_transport_request.ps1`. -Write `{WORK_TEMP}\sap_tr_run.ps1`: +Write `{RUN_TEMP}\sap_tr_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_transport_request.ps1', [System.Text.Encoding]::UTF8) $content = $content.Replace('%%SAP_APPLICATION_SERVER%%', '') @@ -249,7 +260,7 @@ $content = $content.Replace('%%SAP_LANGUAGE%%', '') $content = $content.Replace('%%RFC_LIB_PS1%%', '\scripts\sap_rfc_lib.ps1') $content = $content.Replace('%%TRANSPORT_REQUEST%%', 'THE_TR') $content = $content.Replace('%%SAP_DEV_MODE%%', 'THE_MODE') -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_tr_run.ps1', $content, [System.Text.Encoding]::UTF8) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_tr_run.ps1', $content, [System.Text.Encoding]::UTF8) Write-Host 'Done' ``` Replace all `THE_*` placeholders with actual values from Steps 1-2. @@ -270,7 +281,7 @@ dispatch in the caller, not the guardrail. Execute via **32-bit PowerShell** (SAP NCo 3.1 is registered in the 32-bit GAC): ```bash -C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_tr_run.ps1" +C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_tr_run.ps1" ``` --- @@ -300,7 +311,7 @@ Parse the script output for `RESULT_TR:` and `RESULT_STATUS:` lines. ## Step 5 — Clean Up ```bash -cmd /c del "{WORK_TEMP}\sap_tr_run.ps1" +cmd /c del "{RUN_TEMP}\sap_tr_run.ps1" ``` --- @@ -312,13 +323,13 @@ Log the run-end record. Best-effort. On success: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_tr_run.json" -Status SUCCESS -ExitCode 0 +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_tr_run.json" -Status SUCCESS -ExitCode 0 ``` On failure: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_tr_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_tr_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" ``` Suggested ``: `TR_RESOLUTION_FAILED`, `TR_NOT_MODIFIABLE`, `RFC_LOGON_FAILED`. diff --git a/plugins/sap-dev-core/skills/sap-update-addon/SKILL.md b/plugins/sap-dev-core/skills/sap-update-addon/SKILL.md index f68889b..b38f7c4 100644 --- a/plugins/sap-dev-core/skills/sap-update-addon/SKILL.md +++ b/plugins/sap-dev-core/skills/sap-update-addon/SKILL.md @@ -35,7 +35,7 @@ Task: $ARGUMENTS **Resolve `work_dir` via the env-aware helper** — do NOT take `work_dir` from a direct `settings.json` read (that ignores the `SAPDEV_AI_WORK_DIR` env var and `userconfig.json`). Use the `WORK_DIR=` value printed by: ```bash -powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir))" +powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir)); Write-Output ('RUN_TEMP=' + (Get-SapRunTemp))" ``` The settings note below still applies to the OTHER keys. @@ -54,14 +54,24 @@ Ensure the temp directory exists: cmd /c if not exist "{WORK_TEMP}" mkdir "{WORK_TEMP}" ``` +Set `{RUN_TEMP}` = the `RUN_TEMP=` value printed above — a fresh per-run scratch +directory `{work_dir}\temp\run_`, already created by `Get-SapRunTemp`. +Resolve it **once here** and reuse the same value for the rest of this +invocation; it isolates this run's generated wrappers / state / scratch files so +concurrent runs (parallel sub-agents, multi-connection deploys) never collide. +**`{WORK_TEMP}` stays the base temp dir** and is used ONLY for +`Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'` (the session-attach plumbing +derives `{work_dir}\runtime` from its parent, so it must see the base path, not +the run dir). Everything the skill writes itself goes under `{RUN_TEMP}`. + --- ## Step 0.5 — Start Logging -Start a structured log run. State file: `{WORK_TEMP}\sap_update_addon_run.json`. Best-effort. +Start a structured log run. State file: `{RUN_TEMP}\sap_update_addon_run.json`. Best-effort. ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{WORK_TEMP}\sap_update_addon_run.json" -Skill sap-update-addon -ParamsJson "{\"table\":\"
\"}" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action start -StateFile "{RUN_TEMP}\sap_update_addon_run.json" -Skill sap-update-addon -ParamsJson "{\"table\":\"
\"}" ``` --- @@ -77,7 +87,7 @@ Extract from `$ARGUMENTS`: For ZCMRUPDATE_ADDON_TABLE method, INSERT and UPDATE both result in MODIFY (upsert). - **SAP Logon description** — optional. Used for credential lookup. -If the user provides inline data instead of a file, write it to `{WORK_TEMP}\_data.txt` +If the user provides inline data instead of a file, write it to `{RUN_TEMP}\_data.txt` as a TAB-delimited file with header. Verify the data file exists: @@ -113,7 +123,7 @@ This skill requires an active SAP GUI session. If not already logged in, use the The detection PowerShell template is at `./references/sap_update_addon_detect.ps1`. -Write `{WORK_TEMP}\sap_update_addon_detect_run.ps1`: +Write `{RUN_TEMP}\sap_update_addon_detect_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_update_addon_detect.ps1', [System.Text.Encoding]::UTF8) $content = $content.Replace('%%SAP_SERVER%%', '') @@ -124,13 +134,13 @@ $content = $content.Replace('%%SAP_PASSWORD%%', '') $content = $content.Replace('%%SAP_LANGUAGE%%', '') $content = $content.Replace('%%RFC_LIB_PS1%%', '\scripts\sap_rfc_lib.ps1') $content = $content.Replace('%%TABLE_NAME%%', 'THE_TABLE_NAME') -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_update_addon_detect_run.ps1', $content, [System.Text.Encoding]::UTF8) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_update_addon_detect_run.ps1', $content, [System.Text.Encoding]::UTF8) Write-Host 'Done' ``` Execute via 32-bit PowerShell: ```bash -C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_update_addon_detect_run.ps1" +C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_update_addon_detect_run.ps1" ``` Parse the output lines: @@ -160,7 +170,7 @@ not affected by the same registration issue. Template: `./references/sap_update_addon_sm30.vbs` -Write `{WORK_TEMP}\sap_update_addon_sm30_run.ps1`: +Write `{RUN_TEMP}\sap_update_addon_sm30_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_update_addon_sm30.vbs', [System.Text.Encoding]::UTF8) $content = $content.Replace('%%TABLE_NAME%%', 'THE_TABLE_NAME') @@ -172,13 +182,13 @@ $content = $content.Replace('%%SESSION_PATH%%', $sessionPath) $content = $content.Replace('%%ATTACH_LIB_VBS%%', '\scripts\sap_attach_lib.vbs') . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_update_addon_sm30_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_update_addon_sm30_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Execute: ```bash -powershell -Command "& cscript //NoLogo '{WORK_TEMP}\sap_update_addon_sm30_run.vbs'" +powershell -Command "& cscript //NoLogo '{RUN_TEMP}\sap_update_addon_sm30_run.vbs'" ``` **SM30 Notes:** @@ -191,7 +201,7 @@ powershell -Command "& cscript //NoLogo '{WORK_TEMP}\sap_update_addon_sm30_run.v Template: `./references/sap_update_addon_se16.vbs` -Write `{WORK_TEMP}\sap_update_addon_se16_run.ps1`: +Write `{RUN_TEMP}\sap_update_addon_se16_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_update_addon_se16.vbs', [System.Text.Encoding]::UTF8) $content = $content.Replace('%%TABLE_NAME%%', 'THE_TABLE_NAME') @@ -203,13 +213,13 @@ $content = $content.Replace('%%SESSION_PATH%%', $sessionPath) $content = $content.Replace('%%ATTACH_LIB_VBS%%', '\scripts\sap_attach_lib.vbs') . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_update_addon_se16_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_update_addon_se16_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` Execute: ```bash -powershell -Command "& cscript //NoLogo '{WORK_TEMP}\sap_update_addon_se16_run.vbs'" +powershell -Command "& cscript //NoLogo '{RUN_TEMP}\sap_update_addon_se16_run.vbs'" ``` **SE16 Notes (INSERT / UPDATE):** @@ -240,19 +250,19 @@ The source code for ZCMRUPDATE_ADDON_TABLE is at `./references/ZCMRUPDATE_ADDON_ Template: `./references/sap_update_addon_prog.vbs` -Write `{WORK_TEMP}\sap_update_addon_prog_run.ps1`: +Write `{RUN_TEMP}\sap_update_addon_prog_run.ps1`: ```powershell $content = [System.IO.File]::ReadAllText('\references\sap_update_addon_prog.vbs', [System.Text.Encoding]::UTF8) $content = $content.Replace('%%TABLE_NAME%%', 'THE_TABLE_NAME') $content = $content.Replace('%%DATA_FILE%%', 'THE_DATA_FILE') -$content = $content.Replace('%%TEMP_DIR%%', '{WORK_TEMP}') +$content = $content.Replace('%%TEMP_DIR%%', '{RUN_TEMP}') # Phase 3.5 session-attach plumbing. $sessionPath = '' $content = $content.Replace('%%SESSION_PATH%%', $sessionPath) $content = $content.Replace('%%ATTACH_LIB_VBS%%', '\scripts\sap_attach_lib.vbs') . '\scripts\sap_connection_lib.ps1' $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}' -[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_update_addon_prog_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) +[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_update_addon_prog_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true)) Write-Host 'Done' ``` @@ -272,17 +282,17 @@ can appear well after the upload. ```powershell $shared = '\scripts' $watcher = Start-Process powershell -PassThru -WindowStyle Hidden ` - -RedirectStandardOutput "{WORK_TEMP}\sap_update_addon_sidecar.out" ` + -RedirectStandardOutput "{RUN_TEMP}\sap_update_addon_sidecar.out" ` -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File', "$shared\sap_gui_security_sidecar.ps1",'-TimeoutSeconds','90') Start-Sleep -Milliseconds 800 # cscript returns only after the program run + save-list complete (all dialogs handled). -& 'C:\Windows\SysWOW64\cscript.exe' //NoLogo "{WORK_TEMP}\sap_update_addon_prog_run.vbs" +& 'C:\Windows\SysWOW64\cscript.exe' //NoLogo "{RUN_TEMP}\sap_update_addon_prog_run.vbs" if (-not $watcher.HasExited) { Stop-Process -Id $watcher.Id -Force -ErrorAction SilentlyContinue } -Get-Content "{WORK_TEMP}\sap_update_addon_sidecar.out" -Tail 1 # DISMISSED:WIN32 / TIMEOUT +Get-Content "{RUN_TEMP}\sap_update_addon_sidecar.out" -Tail 1 # DISMISSED:WIN32 / TIMEOUT ``` -If output file was saved, read `{WORK_TEMP}\sap_update_addon_output.txt` for +If output file was saved, read `{RUN_TEMP}\sap_update_addon_output.txt` for results. NOTE: the program's WRITE list is Japanese; the save-list "unconverted" export can render the labels as `#` under a non-matching logon codepage (the `成功: N エラー: M` counts still parse by number). The authoritative result is @@ -302,7 +312,7 @@ Show the user: ## Step 6 — Clean Up ```bash -cmd /c del {WORK_TEMP}\sap_update_addon_detect_run.ps1 {WORK_TEMP}\sap_update_addon_sm30_run.vbs {WORK_TEMP}\sap_update_addon_sm30_run.ps1 {WORK_TEMP}\sap_update_addon_se16_run.vbs {WORK_TEMP}\sap_update_addon_se16_run.ps1 {WORK_TEMP}\sap_update_addon_prog_run.vbs {WORK_TEMP}\sap_update_addon_prog_run.ps1 {WORK_TEMP}\sap_update_addon_output.txt {WORK_TEMP}\sap_update_addon_sidecar.out +cmd /c del {RUN_TEMP}\sap_update_addon_detect_run.ps1 {RUN_TEMP}\sap_update_addon_sm30_run.vbs {RUN_TEMP}\sap_update_addon_sm30_run.ps1 {RUN_TEMP}\sap_update_addon_se16_run.vbs {RUN_TEMP}\sap_update_addon_se16_run.ps1 {RUN_TEMP}\sap_update_addon_prog_run.vbs {RUN_TEMP}\sap_update_addon_prog_run.ps1 {RUN_TEMP}\sap_update_addon_output.txt {RUN_TEMP}\sap_update_addon_sidecar.out ``` --- @@ -314,13 +324,13 @@ Log the run-end record. Best-effort. On success: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_update_addon_run.json" -Status SUCCESS -ExitCode 0 +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_update_addon_run.json" -Status SUCCESS -ExitCode 0 ``` On failure: ```bash -powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_update_addon_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" +powershell -ExecutionPolicy Bypass -File "\scripts\sap_log_helper.ps1" -Action end -StateFile "{RUN_TEMP}\sap_update_addon_run.json" -Status FAILED -ExitCode 1 -ErrorClass -ErrorMsg "" ``` Suggested ``: `UPDATE_ADDON_FAILED`, `RFC_LOGON_FAILED`. diff --git a/scripts/check-consistency.mjs b/scripts/check-consistency.mjs index ad347c9..45beff2 100644 --- a/scripts/check-consistency.mjs +++ b/scripts/check-consistency.mjs @@ -521,6 +521,7 @@ const LEDGER_GATE_SKILLS = new Set([ const ledgerWarnings = []; const step0Warnings = []; +const runTempWarnings = []; for (const plugin of mp.plugins) { const sourceRel = plugin.source.replace(/^\.\//, '').replace(/\/$/, ''); @@ -546,6 +547,24 @@ for (const plugin of mp.plugins) { && !/sap_workdir_setup\.ps1/.test(md)) { step0Warnings.push(`${plugin.name}: skills/${skillEntry}/SKILL.md has a Step 0 "Resolve Work Directory" but resolves work_dir via neither Get-SapWorkDir nor the env-aware onboarding helper sap_workdir_setup.ps1; resolve work_dir env-aware, not via a direct settings.json read (CLAUDE.md Rule 7)`); } + + // (c) Run-scoped temp isolation ({RUN_TEMP}). HARD ERROR: never point the + // session-attach plumbing at the per-run dir. Get-SapCurrentSessionPath + // -WorkTemp derives the durable runtime dir ({work_dir}\runtime, home of + // session_registry.json) from its PARENT, so passing {RUN_TEMP} there + // relocates the broker registry and breaks parallel-session coordination. + // Migrated skills keep the base '{WORK_TEMP}' on that call. + if (/Get-SapCurrentSessionPath\s+-WorkTemp\s+'?\{RUN_TEMP\}'?/.test(md)) { + errors.push(`${plugin.name}: skills/${skillEntry}/SKILL.md passes {RUN_TEMP} to Get-SapCurrentSessionPath -WorkTemp; that derives {work_dir}\\runtime from the parent and would relocate session_registry.json. Keep the base '{WORK_TEMP}' on that call; only the skill's own scratch goes under {RUN_TEMP}.`); + } + // (d) Ratcheting WARN: a skill still writing its generated *_run.vbs/.ps1 to + // the shared base {WORK_TEMP} is unmigrated -- two concurrent runs collide + // on that fixed name (generate-then-cscript TOCTOU -> wrong-object deploy). + // Move per-run scratch to {RUN_TEMP} (Get-SapRunTemp). Informational until + // full coverage, then promote to a hard error. + if (/\{WORK_TEMP\}\\[^\s'"]*_run\.(?:vbs|ps1)/.test(md)) { + runTempWarnings.push(`${plugin.name}: skills/${skillEntry}/SKILL.md writes a generated *_run.vbs/.ps1 under the shared base {WORK_TEMP}; move per-run scratch to {RUN_TEMP} (Get-SapRunTemp) so concurrent runs don't collide (CLAUDE.md "Work Directory Configuration")`); + } } } @@ -563,12 +582,16 @@ if (errors.length === 0) { if (step0Warnings.length > 0) { summary += `, ${step0Warnings.length} Step-0 warning(s)`; } + if (runTempWarnings.length > 0) { + summary += `, ${runTempWarnings.length} run-temp warning(s)`; + } summary += `, ${baselineCoverage}`; console.log(summary); for (const w of phase4Warnings) console.warn(' WARN: ' + w); for (const w of encodingWarnings) console.warn(' WARN: ' + w); for (const w of ledgerWarnings) console.warn(' WARN: ' + w); for (const w of step0Warnings) console.warn(' WARN: ' + w); + for (const w of runTempWarnings) console.warn(' WARN: ' + w); for (const w of baselineWarnings) console.warn(' WARN: ' + w); process.exit(0); } else { @@ -590,6 +613,10 @@ if (errors.length === 0) { console.error(`\nStep-0 work_dir warnings (informational):`); for (const w of step0Warnings) console.error(' WARN: ' + w); } + if (runTempWarnings.length > 0) { + console.error(`\nRun-scoped temp ({RUN_TEMP}) warnings (informational):`); + for (const w of runTempWarnings) console.error(' WARN: ' + w); + } console.error(`\n${baselineCoverage}`); if (baselineWarnings.length > 0) { console.error(`Golden-screen baseline warnings (informational):`); diff --git a/scripts/test-run-temp-helpers.ps1 b/scripts/test-run-temp-helpers.ps1 new file mode 100644 index 0000000..26ba0db --- /dev/null +++ b/scripts/test-run-temp-helpers.ps1 @@ -0,0 +1,115 @@ +# Offline tests for the run-scoped temp isolation helpers: +# Get-SapRunTemp / Remove-SapStaleRunTemp (sap_connection_lib.ps1) +# sap_run_with_lock.ps1 (global paste mutex) +# +# Pure-local, no SAP / RFC / GUI. Run: +# powershell -NoProfile -ExecutionPolicy Bypass -File scripts\test-run-temp-helpers.ps1 +# Exit 0 = all pass, 1 = a failure. + +$ErrorActionPreference = 'Stop' +$shared = Join-Path $PSScriptRoot '..\plugins\sap-dev-core\shared\scripts' +$connLib = (Resolve-Path (Join-Path $shared 'sap_connection_lib.ps1')).Path +$lockCli = (Resolve-Path (Join-Path $shared 'sap_run_with_lock.ps1')).Path +. $connLib + +$fails = 0 +function Check($name, $cond) { + if ($cond) { Write-Host " PASS $name" } + else { Write-Host " FAIL $name" -ForegroundColor Red; $script:fails++ } +} + +# Sandbox work dir (no spaces). +$wd = Join-Path $env:TEMP ('runtemp_test_' + [guid]::NewGuid().ToString('N').Substring(0,8)) +New-Item -ItemType Directory -Force -Path $wd | Out-Null +try { + + Write-Host "`n== Get-SapRunTemp ==" + $dirs = 1..200 | ForEach-Object { Get-SapRunTemp -WorkDir $wd } + Check 'mints 200 distinct dirs' (($dirs | Select-Object -Unique).Count -eq 200) + Check 'every dir exists' (@($dirs | Where-Object { -not (Test-Path $_) }).Count -eq 0) + Check 'all under {work_dir}\temp' (@($dirs | Where-Object { (Split-Path -Parent $_) -ne (Join-Path $wd 'temp') }).Count -eq 0) + Check 'name is run_' (@($dirs | Where-Object { (Split-Path -Leaf $_) -notmatch '^run_[0-9a-f]{8}$' }).Count -eq 0) + Check 'never equals {work_dir}\runtime' (@($dirs | Where-Object { $_ -eq (Join-Path $wd 'runtime') }).Count -eq 0) + # The runtime-derivation family does Split-Path -Parent on the temp path; a + # RUN_TEMP's parent must be {work_dir}\temp, NOT {work_dir}, so passing it to + # those helpers would be wrong -- assert the parent is the base temp. + Check "parent is base temp (not work_dir)" ((Split-Path -Parent $dirs[0]) -eq (Join-Path $wd 'temp')) + + Write-Host "`n== Remove-SapStaleRunTemp ==" + $base = Join-Path $wd 'temp' + $old = Join-Path $base 'run_oldoldld'; New-Item -ItemType Directory -Force -Path $old | Out-Null + $new = Join-Path $base 'run_newnewnw'; New-Item -ItemType Directory -Force -Path $new | Out-Null + $keep = Join-Path $base 'notarun_dir'; New-Item -ItemType Directory -Force -Path $keep | Out-Null + (Get-Item $old).LastWriteTime = (Get-Date).AddHours(-48) + $removed = Remove-SapStaleRunTemp -WorkDir $wd -MaxAgeHours 24 + Check 'stale run_ dir removed' (-not (Test-Path $old)) + Check 'fresh run_ dir kept' (Test-Path $new) + Check 'non-run_ dir untouched' (Test-Path $keep) + Check 'returns a count >= 1' ($removed -ge 1) + + # Native powershell.exe calls below may write to stderr (the lock CLI's + # timeout diagnostic); under -EA Stop that surfaces as NativeCommandError. + # Switch to Continue for the subprocess section -- assertions use exit codes. + $ErrorActionPreference = 'Continue' + + Write-Host "`n== sap_run_with_lock.ps1 : exit-code passthrough ==" + & powershell -NoProfile -ExecutionPolicy Bypass -File $lockCli -MutexName "SapDevTest_$PID" -TimeoutMs 5000 -Command "cmd /c exit 7" 2>$null | Out-Null + Check 'propagates wrapped exit code (7)' ($LASTEXITCODE -eq 7) + + Write-Host "`n== sap_run_with_lock.ps1 : serialization ==" + $probe = Join-Path $wd 'lock_probe.ps1' + $log = Join-Path $wd 'lock_log.txt' + @' +param([string]$Log) +Add-Content -LiteralPath $Log -Value ("START {0} {1}" -f $PID, [DateTime]::UtcNow.Ticks) +Start-Sleep -Milliseconds 700 +Add-Content -LiteralPath $Log -Value ("END {0} {1}" -f $PID, [DateTime]::UtcNow.Ticks) +'@ | Set-Content -LiteralPath $probe -Encoding UTF8 + $mtx = "SapDevTestSerial_$PID" + # Quote-free command (sandbox paths have no spaces), passed as a job arg so + # no command-line quoting is involved -- mirrors real skill usage where the + # wrapped command is `cscript //NoLogo \..vbs` (also space-free). + $cmd = "powershell -NoProfile -ExecutionPolicy Bypass -File $probe -Log $log" + $sb = { param($cli,$m,$c) & powershell -NoProfile -ExecutionPolicy Bypass -File $cli -MutexName $m -TimeoutMs 30000 -Command $c } + $j1 = Start-Job -ScriptBlock $sb -ArgumentList $lockCli, $mtx, $cmd + $j2 = Start-Job -ScriptBlock $sb -ArgumentList $lockCli, $mtx, $cmd + Wait-Job -Job $j1, $j2 -Timeout 60 | Out-Null + # Do NOT Receive-Job: the lock CLI writes a benign "acquired" diagnostic to + # stderr, which Receive-Job would re-raise as an error under -EA Stop. We read + # results from the log file instead. + Remove-Job -Job $j1, $j2 -Force + $lines = @(Get-Content -LiteralPath $log -ErrorAction SilentlyContinue) + # Parse two intervals; assert they do not overlap (mutex serialized them). + $starts = @{}; $intervals = @() + foreach ($ln in $lines) { + $t = $ln -split '\s+' + if ($t[0] -eq 'START') { $starts[$t[1]] = [int64]$t[2] } + elseif ($t[0] -eq 'END') { $intervals += ,@([int64]$starts[$t[1]], [int64]$t[2]) } + } + $overlap = $false + if ($intervals.Count -eq 2) { + $a = $intervals[0]; $b = $intervals[1] + # overlap iff a.start < b.end AND b.start < a.end + if (($a[0] -lt $b[1]) -and ($b[0] -lt $a[1])) { $overlap = $true } + } + Check 'two runs recorded' ($intervals.Count -eq 2) + Check 'critical sections DID NOT overlap' (-not $overlap) + + Write-Host "`n== sap_run_with_lock.ps1 : acquire timeout ==" + $heldName = "SapDevTestHeld_$PID" + $held = [System.Threading.Mutex]::new($false, $heldName) + [void]$held.WaitOne(2000) + try { + & powershell -NoProfile -ExecutionPolicy Bypass -File $lockCli -MutexName $heldName -TimeoutMs 1200 -Command "cmd /c exit 0" 2>$null | Out-Null + Check 'returns 2 when mutex unavailable' ($LASTEXITCODE -eq 2) + } finally { + $held.ReleaseMutex(); $held.Dispose() + } + +} finally { + Remove-Item -LiteralPath $wd -Recurse -Force -ErrorAction SilentlyContinue +} + +Write-Host "" +if ($fails -eq 0) { Write-Host "ALL TESTS PASSED" -ForegroundColor Green; exit 0 } +else { Write-Host "$fails TEST(S) FAILED" -ForegroundColor Red; exit 1 } From 0430567c9c5f53ece11adeff35124490a6ceee69 Mon Sep 17 00:00:00 2001 From: HKX BDDEV001 Date: Sat, 20 Jun 2026 11:44:20 +0900 Subject: [PATCH 4/9] feat: Implement two-bucket temp model for file management in SAP skills and agents --- CLAUDE.md | 40 +++++ plugins/sap-dev-core/agents/abap-developer.md | 18 +++ .../agents/cc-migration-engineer.md | 12 +- scripts/check-consistency.mjs | 34 +++- scripts/run-temp-hook.mjs | 147 ++++++++++++++++++ 5 files changed, 242 insertions(+), 9 deletions(-) create mode 100644 scripts/run-temp-hook.mjs diff --git a/CLAUDE.md b/CLAUDE.md index efa15c8..d71aab1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -449,6 +449,46 @@ fixed names. `{WORK_TEMP}` stays the **base** dir, used for the session broker a parent — passing them the run dir would relocate the registry). See `shared/scripts/sap_connection_lib.ps1` (`Get-SapRunTemp` / `Remove-SapStaleRunTemp`). +**Two-bucket temp model — decide by SCOPE, not by "is it transient".** The +mistake is "everything transient → `{RUN_TEMP}`": some scratch is genuinely +cross-session and MUST stay at a shared, stable path or coordination breaks. +Route every temp file to the bucket that matches *who needs to find it*: + +- **Bucket A — cross-session / cross-connection coordination → stable shared + path** (`{work_dir}\runtime\`): anything a *different* session must locate by + a *predictable* path — the broker registry (`session_registry.json`), + `connections.json`, the AI-session pins, the session-path anchor. These are + the **allowlisted** shared artifacts. `{WORK_TEMP}` (`{work_dir}\temp` root) + is itself only an *anchor* passed to `Get-SapCurrentSessionPath -WorkTemp` / + the broker (which derive `{work_dir}\runtime` from its parent) — it is **not** + a write target for generated scripts. +- **Bucket B — per-run private scratch → `{RUN_TEMP}`** (`{work_dir}\temp\run_`, + minted by `Get-SapRunTemp`): a skill's generated `*_run.vbs`/`.ps1`, the asXML + payload it pastes, `_run.json` state, the clipboard/title temp files, an input + file, AND any ad-hoc orchestrator/agent probe or verify script. Run-isolated so + concurrent runs — parallel sub-agents, multi-connection deploys, or **two + sessions of the same build** — never collide on a fixed name. + +**Decision rule:** *Will another session/connection ever read this exact file by +a predictable path? Yes → Bucket A (shared coordination state in +`{work_dir}\runtime\`, on the allowlist). No → Bucket B (`{RUN_TEMP}`).* Shipped +helper scripts (`sap_session_broker*.{ps1,vbs}`, `sap_attach_lib.vbs`, …) live in +`shared/scripts/`, never regenerated into temp. A cross-session helper can still +*run* from `{RUN_TEMP}` and just point at the shared **state** by absolute path + +the named mutex (`SapDevSessionBroker_v2`) + the global SAP GUI COM ROT — so even +coordination work does not need its *script* in `{WORK_TEMP}`. + +**This applies to agents and ad-hoc orchestration scratch too, not just skills.** +Writing a fixed-named file straight into `{WORK_TEMP}` root (or worse, the repo +root) is the smell that caused the 2026-06-20 cross-session +`sap_se38_update_run.vbs` collision (two concurrent v74 builds clobbered each +other's generated VBS). Enforced two ways: `scripts/check-consistency.mjs` (static, +catches the repo SKILL.md — note it does NOT see the running *cache* copy or +ad-hoc scratch) + the `run-temp` PreToolUse hook in `.claude/settings.local.json` +(runtime, catches the live tool call — cache-lagged skills and agent/orchestrator +scratch included). The shared/allowlisted Bucket-A basenames are codified in both +(`RUN_TEMP_SHARED_ALLOWLIST` in the checker; `SHARED_ALLOWLIST` in the hook). + Every skill includes a **Step 0 — Resolve Work Directory**. It MUST resolve `work_dir` via `Get-SapWorkDir` (which applies the env-var → settings.local → settings → default precedence) — **NOT** by reading `settings.json` directly, diff --git a/plugins/sap-dev-core/agents/abap-developer.md b/plugins/sap-dev-core/agents/abap-developer.md index a64b140..207013a 100644 --- a/plugins/sap-dev-core/agents/abap-developer.md +++ b/plugins/sap-dev-core/agents/abap-developer.md @@ -82,6 +82,24 @@ Then read the other keys per `shared/rules/settings_lookup.md` (merge env var writes go to `userconfig.json`): `custom_url` (default `{work_dir}\custom`), `design_docs_url`, `source_code_url`. Set `{WORK_TEMP}` = `{work_dir}\temp`. +Also mint a per-run scratch dir for the agent's OWN transient files — ad-hoc +probes, verify scripts, material/input files, any generated `.vbs`/`.ps1`. Write +them HERE, never into `{WORK_TEMP}` root, where a concurrent agent/run clobbers a +fixed name (the 2026-06-20 cross-session `sap_se38_update_run.vbs` collision). The +deploy skills the agent drives already mint their OWN `{RUN_TEMP}` internally — this +one is for the files the agent writes directly (via PowerShell/Bash, which the +Write-tool hook does not see): + +```bash +powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('RUN_TEMP=' + (Get-SapRunTemp))" +``` + +Take `{RUN_TEMP}` from the `RUN_TEMP=` line and reuse that ONE value for every +agent-authored scratch file. Keep `{WORK_TEMP}` only as the base anchor for +`Get-SapCurrentSessionPath -WorkTemp` and for the persistent transcript (Step 0.5), +which is a deliberate audit artifact (timestamped), not transient scratch. See +CLAUDE.md "Two-bucket temp model". + ### 0.2 Read the customer brief and set MODE flags Resolution chain (first hit wins): diff --git a/plugins/sap-migrate/agents/cc-migration-engineer.md b/plugins/sap-migrate/agents/cc-migration-engineer.md index d562337..5e64089 100644 --- a/plugins/sap-migrate/agents/cc-migration-engineer.md +++ b/plugins/sap-migrate/agents/cc-migration-engineer.md @@ -56,12 +56,20 @@ Task: $ARGUMENTS ## Step 0 — Pre-flight ### 0.1 Resolve work paths -Resolve `work_dir` via the env-aware helper (NOT a raw `settings.json` read): +Resolve `work_dir` via the env-aware helper (NOT a raw `settings.json` read), and +mint a per-run scratch dir for this agent's OWN transient files in the same call: ```bash -powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir))" +powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; Write-Output ('WORK_DIR=' + (Get-SapWorkDir)); Write-Output ('RUN_TEMP=' + (Get-SapRunTemp))" ``` +Take `{RUN_TEMP}` from the `RUN_TEMP=` line and write any agent-authored scratch +(ad-hoc probes, generated `.vbs`/`.ps1`, scratch files) under that ONE dir — never +into `{work_dir}\temp` root, where a concurrent run clobbers a fixed name (the +2026-06-20 cross-session collision). The `/sap-cc-*` skills the agent drives already +mint their own `{RUN_TEMP}`; `{work_dir}\temp` stays only for the persistent +transcript (Step 0.4, timestamped). See CLAUDE.md "Two-bucket temp model". + ### 0.2 Resolve (or create) the campaign Read the campaign id from `$ARGUMENTS` (`--campaign ` or "campaign "). If `{work_dir}\migrations\\campaign.json` does **not** exist, this is a new diff --git a/scripts/check-consistency.mjs b/scripts/check-consistency.mjs index 45beff2..0eb1e04 100644 --- a/scripts/check-consistency.mjs +++ b/scripts/check-consistency.mjs @@ -523,6 +523,19 @@ const ledgerWarnings = []; const step0Warnings = []; const runTempWarnings = []; +// Bucket A (cross-session / cross-connection coordination) allowlist: artifacts a +// DIFFERENT session must find by a predictable shared path, so they are NOT per-run +// scratch and must never be flagged by the {WORK_TEMP} gate (d) below. Keep small + +// explicit; mirror in the run-temp PreToolUse hook's SHARED_ALLOWLIST. Most live in +// {work_dir}\runtime or shared/scripts (so they don't match {WORK_TEMP}\*.vbs|ps1 +// anyway) — listed for intent + future-proofing. See CLAUDE.md "Two-bucket temp model". +const RUN_TEMP_SHARED_ALLOWLIST = new Set([ + 'session_registry.json', // broker state (home: {work_dir}\runtime) + 'sap_session_broker.ps1', // shipped broker (home: shared/scripts) + 'sap_session_broker_com.vbs', // shipped broker COM (home: shared/scripts) + 'sap_attach_lib.vbs', // shipped attach helper (home: shared/scripts) +].map(s => s.toLowerCase())); + for (const plugin of mp.plugins) { const sourceRel = plugin.source.replace(/^\.\//, '').replace(/\/$/, ''); const sourceAbs = join(repoRoot, sourceRel); @@ -557,13 +570,20 @@ for (const plugin of mp.plugins) { if (/Get-SapCurrentSessionPath\s+-WorkTemp\s+'?\{RUN_TEMP\}'?/.test(md)) { errors.push(`${plugin.name}: skills/${skillEntry}/SKILL.md passes {RUN_TEMP} to Get-SapCurrentSessionPath -WorkTemp; that derives {work_dir}\\runtime from the parent and would relocate session_registry.json. Keep the base '{WORK_TEMP}' on that call; only the skill's own scratch goes under {RUN_TEMP}.`); } - // (d) Ratcheting WARN: a skill still writing its generated *_run.vbs/.ps1 to - // the shared base {WORK_TEMP} is unmigrated -- two concurrent runs collide - // on that fixed name (generate-then-cscript TOCTOU -> wrong-object deploy). - // Move per-run scratch to {RUN_TEMP} (Get-SapRunTemp). Informational until - // full coverage, then promote to a hard error. - if (/\{WORK_TEMP\}\\[^\s'"]*_run\.(?:vbs|ps1)/.test(md)) { - runTempWarnings.push(`${plugin.name}: skills/${skillEntry}/SKILL.md writes a generated *_run.vbs/.ps1 under the shared base {WORK_TEMP}; move per-run scratch to {RUN_TEMP} (Get-SapRunTemp) so concurrent runs don't collide (CLAUDE.md "Work Directory Configuration")`); + // (d) Ratcheting WARN: a skill writing a GENERATED script (.vbs/.ps1) to the + // shared base {WORK_TEMP} root is unmigrated -- two concurrent runs collide + // on that fixed name (generate-then-cscript TOCTOU -> wrong-object deploy; + // the 2026-06-20 sap_se38_update_run.vbs cross-session clobber). Move per-run + // scratch to {RUN_TEMP} (Get-SapRunTemp). Widened from the narrow *_run.* + // pattern to ANY {WORK_TEMP}\.vbs|ps1, minus the cross-session + // RUN_TEMP_SHARED_ALLOWLIST (Bucket A). Informational until full coverage, + // then promote to a hard error. See CLAUDE.md "Two-bucket temp model". + const workTempScripts = [...md.matchAll(/\{WORK_TEMP\}\\([^\s'"\\]+\.(?:vbs|ps1))/gi)] + .map(m => m[1]) + .filter(name => !RUN_TEMP_SHARED_ALLOWLIST.has(name.toLowerCase())); + if (workTempScripts.length > 0) { + const uniq = [...new Set(workTempScripts)].sort(); + runTempWarnings.push(`${plugin.name}: skills/${skillEntry}/SKILL.md writes generated script(s) under the shared base {WORK_TEMP} (${uniq.join(', ')}); move per-run scratch to {RUN_TEMP} (Get-SapRunTemp) so concurrent runs don't collide (CLAUDE.md "Two-bucket temp model")`); } } } diff --git a/scripts/run-temp-hook.mjs b/scripts/run-temp-hook.mjs new file mode 100644 index 0000000..bcbdf75 --- /dev/null +++ b/scripts/run-temp-hook.mjs @@ -0,0 +1,147 @@ +#!/usr/bin/env node +// ============================================================================= +// run-temp-hook.mjs -- PreToolUse hook: keep generated scripts out of the +// {WORK_TEMP} root (the "two-bucket temp model" in CLAUDE.md). +// +// WHY a runtime hook on top of the static check-consistency.mjs gate: CI scans +// the *repo* SKILL.md, but agents run the *cache* copy (which can lag the +// migrated repo), and ad-hoc orchestrator/agent scratch is in no SKILL.md at +// all. Both bit us on 2026-06-20 (two concurrent v74 builds clobbered a shared +// {WORK_TEMP}\sap_se38_update_run.vbs, and the orchestrator wrote _probe*.ps1 / +// _verify074.ps1 straight into the temp root). A PreToolUse hook sees the live +// tool call, so it catches both. +// +// WHAT it flags: a GENERATED SCRIPT (.vbs / .ps1) written DIRECTLY into +// {work_dir}\temp\ (the {WORK_TEMP} root, NOT a {RUN_TEMP} subdir +// {work_dir}\temp\run_\..., and NOT a Bucket-A SHARED_ALLOWLIST name). +// +// PRECISION by tool: +// * Write / Edit -> file_path is unambiguously a write target -> ENFORCED +// (block by default; the model just re-issues under {RUN_TEMP}). +// * Bash / PowerShell -> a command only MENTIONS paths (a del/cscript is not +// a write), so command-scanning is ADVISORY ONLY here, never blocks. +// +// MODES (env SAPDEV_RUNTEMP_HOOK): block (default) | warn | off +// SAFETY: always exits 0 and fails OPEN on any error -- it can never wedge a +// session. Disable instantly with setx SAPDEV_RUNTEMP_HOOK off (or delete +// the hook block in .claude/settings.local.json). +// +// Contract: CLAUDE.md "Two-bucket temp model". Allowlist mirrors +// RUN_TEMP_SHARED_ALLOWLIST in check-consistency.mjs. +// ============================================================================= + +import { readFileSync, appendFileSync } from 'node:fs'; +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; + +// Bucket A: cross-session coordination artifacts that legitimately live at a +// shared path. (Most live in {work_dir}\runtime or shared/scripts and so never +// match a {WORK_TEMP}\*.vbs|ps1 write anyway -- listed for intent + symmetry.) +const SHARED_ALLOWLIST = new Set([ + 'session_registry.json', + 'sap_session_broker.ps1', + 'sap_session_broker_com.vbs', + 'sap_attach_lib.vbs', +].map((s) => s.toLowerCase())); + +const MODE = (process.env.SAPDEV_RUNTEMP_HOOK || 'block').toLowerCase(); + +function resolveWorkDir() { + const env = process.env.SAPDEV_AI_WORK_DIR; + if (env && env.trim()) return env.trim(); + try { + const ptr = join(process.env.APPDATA || '', 'sapdev-ai', 'work_dir.txt'); + if (existsSync(ptr)) { + const v = readFileSync(ptr, 'utf8').trim(); + if (v) return v; + } + } catch { /* ignore */ } + return 'C:\\sap_dev_work'; +} + +const norm = (s) => String(s == null ? '' : s).replace(/\//g, '\\').toLowerCase(); + +function findRootScripts(text, tempRoot) { + // Match \.vbs|ps1 where has no path + // separator (i.e. a DIRECT child of the temp root, not a run_ subdir), + // plus the literal {work_temp}\.vbs|ps1 token form (defensive). + const esc = tempRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const res = [ + new RegExp(esc + '\\\\([^\\\\/"\'\\s]+\\.(?:vbs|ps1))', 'gi'), + /\{work_temp\}\\([^\\/"'\s]+\.(?:vbs|ps1))/gi, + ]; + const hits = new Set(); + for (const re of res) { + re.lastIndex = 0; + let m; + while ((m = re.exec(text)) !== null) { + const base = m[1]; + if (!SHARED_ALLOWLIST.has(base)) hits.add(base); + } + } + return [...hits].sort(); +} + +function out(obj) { try { process.stdout.write(JSON.stringify(obj)); } catch { /* ignore */ } } + +try { + if (MODE === 'off') process.exit(0); + + const raw = readFileSync(0, 'utf8'); // stdin (PreToolUse payload) + if (!raw || !raw.trim()) process.exit(0); + const payload = JSON.parse(raw); + const tool = String(payload.tool_name || payload.toolName || ''); + const input = payload.tool_input || payload.toolInput || {}; + + const workDir = resolveWorkDir(); + const tempRoot = norm(workDir) + '\\temp'; + + const isWrite = /^(Write|Edit|MultiEdit|NotebookEdit)$/i.test(tool); + const candidate = isWrite ? norm(input.file_path) : norm(input.command); + if (!candidate) process.exit(0); + + const hits = findRootScripts(candidate, tempRoot); + if (hits.length === 0) process.exit(0); + + const list = hits.join(', '); + const reason = + `Generated script(s) [${list}] are headed for the {WORK_TEMP} root ` + + `(${workDir}\\temp). Per the two-bucket temp model (CLAUDE.md), per-run scratch ` + + `MUST go under {RUN_TEMP} = ${workDir}\\temp\\run_\\ (mint it with ` + + `Get-SapRunTemp, or use a run-scoped subdir) so concurrent sessions don't ` + + `clobber a fixed name. Only Bucket-A cross-session coordination state belongs ` + + `at a shared path. Re-issue the write under a {RUN_TEMP} subdirectory.`; + + // Guaranteed audit trail (best-effort; never throws past here). + try { + let ts = ''; + try { ts = new Date().toISOString(); } catch { ts = ''; } + appendFileSync(join(workDir, 'temp', 'run-temp-hook.log'), + `${ts}\t${tool}\t${MODE}\t${list}\n`); + } catch { /* ignore */ } + + // Bash/PowerShell: command text only MENTIONS a path -> advisory, never block. + const enforce = MODE === 'block' && isWrite; + + if (enforce) { + out({ + decision: 'block', // legacy shape + reason, + hookSpecificOutput: { // current shape + hookEventName: 'PreToolUse', + permissionDecision: 'deny', + permissionDecisionReason: reason, + }, + }); + } else { + out({ + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: `[run-temp advisory] ${reason}`, + }, + }); + } + process.exit(0); +} catch { + process.exit(0); // fail OPEN: never wedge a tool call +} From 11af3ec007693d936db91ee1ff54988cfd11e7e5 Mon Sep 17 00:00:00 2001 From: HKX BDDEV001 Date: Sat, 20 Jun 2026 12:36:17 +0900 Subject: [PATCH 5/9] feat: Implement session-scoped dev defaults for task transport requests to prevent clobbering in concurrent SAP connections --- CLAUDE.md | 19 ++ .../shared/rules/tr_resolution.md | 14 +- .../shared/scripts/sap_connection_lib.ps1 | 166 +++++++++++++++++- .../shared/scripts/sap_dev_default.ps1 | 57 ++++++ .../shared/scripts/sap_settings_lib.ps1 | 9 +- 5 files changed, 260 insertions(+), 5 deletions(-) create mode 100644 plugins/sap-dev-core/shared/scripts/sap_dev_default.ps1 diff --git a/CLAUDE.md b/CLAUDE.md index d71aab1..7df8a53 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -516,6 +516,25 @@ Use `{RUN_TEMP}` for the skill's OWN scratch (generated `*_run.vbs/.ps1`, the | `rule_of_tr_description` | `ASK`, `PATTERN`, `FIXED`, `RANDOM` | `ASK` | How `/sap-se01` builds the description for new TRs. | | `tr_description_template` | string | blank | Template for `PATTERN` (placeholders) or literal for `FIXED`. Final result truncated to 60 chars. | +**Two-layer dev defaults (per-connection + per-session).** The per-connection dev +keys (`sap_dev_transport_request`, `sap_dev_package`, `sap_dev_function_group`, +`sap_dev_mode`, `way_to_get_transport_request`, `rule_of_tr_description`, +`tr_description_template`) resolve through **two** layers, highest first: +1. **Session** — `{work_dir}\runtime\session_dev_defaults.json`, keyed per + `(AI-session × connection)`. A TASK's TR/package lives here, so concurrent + conversations on the **same** SAP connection never clobber each other (the + 2026-06-20 `069→074→075` thrash). Keyed on the connection too, so a + `/sap-login --switch` can't carry an `S4DK…` TR onto S4H. +2. **Connection** — `connections.json[].dev_defaults`, the developer's + **standing** default for that system (the existing Phase 4.4 layer). +Then the global settings file. **Writers:** a task TR/package → **Session** scope +(`Set-SapUserSetting … -Scope Session`, or the CLI `shared/scripts/sap_dev_default.ps1` +whose default is Session) — never a hand-edit of `connections.json`. A deliberate +standing default (onboarding: `/sap-dev-init`, `/sap-login`) → **Connection** scope +(the default of `Set-SapCurrentDevDefault`). Reads go through `Get-SapCurrentDevDefault` +(centralized), so all callers get the layered resolution for free. Session entries +are age-pruned (7 days) — task defaults are ephemeral. + ### FM Signature Cache Settings Read by `sap_rfc_lookup_fm.ps1` (used by `sap-gen-abap` Step 1.5 to pre-fetch FM signatures before generation). All keys are optional — defaults below. diff --git a/plugins/sap-dev-core/shared/rules/tr_resolution.md b/plugins/sap-dev-core/shared/rules/tr_resolution.md index 1baea88..8035a05 100644 --- a/plugins/sap-dev-core/shared/rules/tr_resolution.md +++ b/plugins/sap-dev-core/shared/rules/tr_resolution.md @@ -114,7 +114,19 @@ truncate to 60 chars by: - `ASK` mode → persist only if the user opts in. - `CREATE_NEW` mode → never persist. -Persistence is via `/update-config` writing to sap-dev-core `settings.json`. +**Scope — persist a task TR as a SESSION default, not a connection default.** A +TR a task creates/resolves is task-scoped: persist it to the per-`(AI-session × +connection)` layer so concurrent conversations on the **same** SAP connection +don't clobber each other's TR (the 2026-06-20 `069→074→075` thrash). Do this via +the canonical writer with Session scope — `Set-SapUserSetting -Key +sap_dev_transport_request -Value -Scope Session`, or the CLI +`shared/scripts/sap_dev_default.ps1 -Action set -Key sap_dev_transport_request +-Value ` (Session is its default) — **never** by hand-editing +`connections.json`. Reads resolve session → connection → global automatically, so +the task value wins for this conversation while the connection block stays the +standing fallback. Use `-Scope Connection` (or `/sap-dev-init` / `/sap-login`) +**only** to set a deliberate STANDING default that should persist across +conversations on that system. --- diff --git a/plugins/sap-dev-core/shared/scripts/sap_connection_lib.ps1 b/plugins/sap-dev-core/shared/scripts/sap_connection_lib.ps1 index 7cd67e2..d8e2837 100644 --- a/plugins/sap-dev-core/shared/scripts/sap_connection_lib.ps1 +++ b/plugins/sap-dev-core/shared/scripts/sap_connection_lib.ps1 @@ -1669,6 +1669,15 @@ function Get-SapCurrentDevDefault { param([Parameter(Mandatory)][string]$Key) $profile = $null try { $profile = Get-SapDevDefaultProfile } catch {} + # 1. Session layer (per AI-session x THIS connection) wins -- task-scoped, + # isolates concurrent conversations on the same connection (2026-06-20). + if ($profile -and (_NotEmpty "$($profile.id)")) { + try { + $sv = Get-SapSessionDevDefault -Key $Key -ConnId "$($profile.id)" + if ("$sv" -ne '') { return "$sv" } + } catch {} + } + # 2. Connection-level standing default. if ($profile -and $profile.ContainsKey('dev_defaults') -and $profile['dev_defaults']) { $dd = $profile['dev_defaults'] $v = $null @@ -1709,10 +1718,19 @@ function Set-SapCurrentDevDefault { #> param( [Parameter(Mandatory)][string]$Key, - [Parameter(Mandatory)][AllowEmptyString()][string]$Value + [Parameter(Mandatory)][AllowEmptyString()][string]$Value, + [ValidateSet('Connection','Session')][string]$Scope = 'Connection' ) $profile = $null try { $profile = Get-SapDevDefaultProfile } catch {} + # Session scope -> task-scoped (AI-session x connection); never touches + # connections.json, so concurrent conversations on the same connection don't + # clobber. Needs an unambiguous connection (for the system-qualifying + # ConnId); with none, fall through to the connection/global write (best effort). + if ($Scope -eq 'Session' -and $profile -and (_NotEmpty "$($profile.id)")) { + Set-SapSessionDevDefault -Key $Key -Value $Value -ConnId "$($profile.id)" + return + } if (-not $profile -or -not (_NotEmpty "$($profile.id)")) { # No unambiguous connection -> do NOT guess the default connection (that # writes one system's value into another's dev_defaults block). Warn when @@ -1755,6 +1773,152 @@ function Set-SapCurrentDevDefault { Write-SapConnectionStore -Store $store } +# ============================================================================= +# Session-scoped dev defaults (per AI-session x connection) [added 2026-06-20] +# ----------------------------------------------------------------------------- +# Phase 4.4's per-connection dev_defaults fixed cross-SYSTEM leakage but NOT +# cross-SESSION-same-system contention: N AI sessions pinned to ONE connection +# share one dev_defaults block and clobber each other (the 2026-06-20 +# 069->074->075 thrash on S4D). This layer scopes a TASK's TR/package to the +# (AI-session x connection) pair, so concurrent conversations on the same +# connection don't fight, while the connection block stays the standing +# fallback. Keyed on BOTH the AI session AND the connection so a /sap-login +# --switch mid-conversation can't carry an S4DK... TR onto S4H. Decoupled from +# the broker's hot session_registry.json (own file + own mutex) so the broker's +# reconcile/rewrite never stomps it and vice-versa. +# Resolution (in Get-SapCurrentDevDefault): session layer -> connection +# dev_defaults -> global file. GC: age-pruned on write (entries older than +# _SapSessDD_MaxAgeDays); task defaults are ephemeral, so dropping an old +# conversation's is correct. +# ============================================================================= +$script:_SapSessDD_MutexName = 'SapDevSessionDevDefaults_v1' +$script:_SapSessDD_MutexTimeoutMs = 10000 +$script:_SapSessDD_MaxAgeDays = 7 + +function Get-SapSessionDevDefaultPath { + param([string]$RuntimeDir = '') + if ([string]::IsNullOrWhiteSpace($RuntimeDir)) { $RuntimeDir = Get-SapWorkRuntimeDir } + return (Join-Path $RuntimeDir 'session_dev_defaults.json') +} + +function _With-SessionDevDefaultLock { + param([scriptblock]$Body) + $mutex = [System.Threading.Mutex]::new($false, $script:_SapSessDD_MutexName) + $acquired = $false + try { + try { $acquired = $mutex.WaitOne($script:_SapSessDD_MutexTimeoutMs) } + catch [System.Threading.AbandonedMutexException] { $acquired = $true } + if (-not $acquired) { throw "sap_connection_lib: could not acquire session-dev-default mutex within $($script:_SapSessDD_MutexTimeoutMs)ms" } + & $Body + } finally { + if ($acquired) { try { $mutex.ReleaseMutex() } catch {} } + try { $mutex.Dispose() } catch {} + } +} + +function _Read-SessionDevDefaultFile { + # -> hashtable { = @{ _ts=; by_conn = @{ = @{ key=val } } } } + param([string]$RuntimeDir = '') + $path = Get-SapSessionDevDefaultPath -RuntimeDir $RuntimeDir + if (-not (Test-Path $path)) { return @{} } + try { + $raw = Get-Content -Path $path -Raw -Encoding UTF8 + if ([string]::IsNullOrWhiteSpace($raw)) { return @{} } + $obj = $raw | ConvertFrom-Json + $h = @{} + foreach ($aidProp in $obj.PSObject.Properties) { + $blk = @{ _ts = ''; by_conn = @{} } + $v = $aidProp.Value + if ($v.PSObject.Properties['_ts']) { $blk._ts = "$($v._ts)" } + if ($v.PSObject.Properties['by_conn'] -and $v.by_conn) { + foreach ($cp in $v.by_conn.PSObject.Properties) { + $kv = @{} + foreach ($kp in $cp.Value.PSObject.Properties) { $kv[$kp.Name] = "$($kp.Value)" } + $blk.by_conn[$cp.Name] = $kv + } + } + $h[$aidProp.Name] = $blk + } + return $h + } catch { return @{} } +} + +function _Write-SessionDevDefaultFile { + param([hashtable]$Data, [string]$RuntimeDir = '') + $path = Get-SapSessionDevDefaultPath -RuntimeDir $RuntimeDir + $json = $Data | ConvertTo-Json -Depth 8 + [System.IO.File]::WriteAllText($path, $json, (New-Object System.Text.UTF8Encoding($false))) +} + +function _Prune-SessionDevDefaults { + param([hashtable]$Data) + try { + $cutoff = (Get-Date).ToUniversalTime().AddDays(-1 * [math]::Abs($script:_SapSessDD_MaxAgeDays)) + foreach ($aid in @($Data.Keys)) { + $ts = $null + try { + if ($Data[$aid].ContainsKey('_ts') -and $Data[$aid]['_ts']) { + $ts = [datetime]::Parse($Data[$aid]['_ts'], [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::RoundtripKind) + } + } catch {} + if ($ts -and $ts.ToUniversalTime() -lt $cutoff) { $Data.Remove($aid) | Out-Null } + } + } catch {} + return $Data +} + +function Get-SapSessionDevDefault { + <# + .SYNOPSIS + Read a TASK-scoped dev default for the (AI-session x connection) pair. + Returns '' on any miss. ConnId is REQUIRED (the system the default + belongs to) -- a session default is meaningless without it. + #> + param( + [Parameter(Mandatory)][string]$Key, + [string]$Aid = '', + [string]$ConnId = '', + [string]$RuntimeDir = '' + ) + if ([string]::IsNullOrWhiteSpace($ConnId)) { return '' } + if ([string]::IsNullOrWhiteSpace($RuntimeDir)) { $RuntimeDir = Get-SapWorkRuntimeDir } + if ([string]::IsNullOrWhiteSpace($Aid)) { try { $Aid = Get-SapAiSessionId -RuntimeDir $RuntimeDir } catch { return '' } } + $data = _Read-SessionDevDefaultFile -RuntimeDir $RuntimeDir + if ($data.ContainsKey($Aid) -and $data[$Aid].by_conn.ContainsKey($ConnId)) { + $kv = $data[$Aid].by_conn[$ConnId] + if ($kv.ContainsKey($Key) -and "$($kv[$Key])" -ne '') { return "$($kv[$Key])" } + } + return '' +} + +function Set-SapSessionDevDefault { + <# + .SYNOPSIS + Write a TASK-scoped dev default under (AI-session x connection). Does NOT + touch connections.json -- so concurrent conversations on the same + connection never clobber each other. Mutex-serialized; age-pruned. + #> + param( + [Parameter(Mandatory)][string]$Key, + [Parameter(Mandatory)][AllowEmptyString()][string]$Value, + [string]$Aid = '', + [string]$ConnId = '', + [string]$RuntimeDir = '' + ) + if ([string]::IsNullOrWhiteSpace($ConnId)) { throw "Set-SapSessionDevDefault: -ConnId is required (the system this default belongs to)." } + if ([string]::IsNullOrWhiteSpace($RuntimeDir)) { $RuntimeDir = Get-SapWorkRuntimeDir } + if ([string]::IsNullOrWhiteSpace($Aid)) { $Aid = Get-SapAiSessionId -RuntimeDir $RuntimeDir } + _With-SessionDevDefaultLock { + $data = _Read-SessionDevDefaultFile -RuntimeDir $RuntimeDir + if (-not $data.ContainsKey($Aid)) { $data[$Aid] = @{ _ts = ''; by_conn = @{} } } + if (-not $data[$Aid].by_conn.ContainsKey($ConnId)) { $data[$Aid].by_conn[$ConnId] = @{} } + $data[$Aid].by_conn[$ConnId][$Key] = "$Value" + $data[$Aid]['_ts'] = (Get-Date).ToUniversalTime().ToString('o') + $data = _Prune-SessionDevDefaults -Data $data + _Write-SessionDevDefaultFile -Data $data -RuntimeDir $RuntimeDir + } +} + # --- Legacy migration ------------------------------------------------------- function Import-LegacyConnectionFromSettings { diff --git a/plugins/sap-dev-core/shared/scripts/sap_dev_default.ps1 b/plugins/sap-dev-core/shared/scripts/sap_dev_default.ps1 new file mode 100644 index 0000000..5c7a40c --- /dev/null +++ b/plugins/sap-dev-core/shared/scripts/sap_dev_default.ps1 @@ -0,0 +1,57 @@ +# ============================================================================= +# sap_dev_default.ps1 -- CLI for task-scoped dev defaults (TR / package / ...) +# +# Default Scope=Session: a fresh TR/package a build creates becomes THIS +# conversation's default, keyed per (AI-session x pinned connection), WITHOUT +# touching connections.json -- so concurrent conversations on the same SAP +# connection never clobber each other (the 2026-06-20 069->074->075 thrash). +# Use -Scope Connection ONLY for a deliberate STANDING default (onboarding: +# /sap-dev-init, /sap-login). +# +# Reads always resolve session -> connection -> global (Get-SapCurrentDevDefault). +# Requires a pinned connection (run /sap-login first) OR a sole saved connection. +# +# Usage: +# ... -Action set -Key sap_dev_transport_request -Value S4DK941289 # Session (default) +# ... -Action set -Key sap_dev_package -Value ZMMA074 +# ... -Action set -Key sap_dev_package -Value ZMMA_DEV -Scope Connection # standing +# ... -Action get -Key sap_dev_transport_request +# +# Stdout last line: VALUE= (get) | SET: key= scope= effective_value= | ERROR: +# Exit: 0 ok, 2 error. +# ============================================================================= +[CmdletBinding()] +param( + [ValidateSet('get','set')][string] $Action = 'get', + [string] $Key = '', + [AllowEmptyString()][string] $Value = '', + [ValidateSet('Session','Connection')][string] $Scope = 'Session' +) + +$ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'sap_settings_lib.ps1') +. (Join-Path $PSScriptRoot 'sap_connection_lib.ps1') + +try { + if ([string]::IsNullOrWhiteSpace($Key)) { Write-Output 'ERROR: -Key is required'; exit 2 } + $perConn = Get-SapPerConnectionDevKeys + if ($perConn -notcontains $Key) { + Write-Output "ERROR: '$Key' is not a per-connection dev key. Allowed: $($perConn -join ', ')" + exit 2 + } + + if ($Action -eq 'get') { + $v = Get-SapCurrentDevDefault -Key $Key + Write-Output ("VALUE=" + $v) + exit 0 + } + + # set + Set-SapCurrentDevDefault -Key $Key -Value $Value -Scope $Scope + $eff = Get-SapCurrentDevDefault -Key $Key + Write-Output ("SET: key=$Key scope=$Scope effective_value=$eff") + exit 0 +} catch { + Write-Output ("ERROR: " + $_.Exception.Message) + exit 2 +} diff --git a/plugins/sap-dev-core/shared/scripts/sap_settings_lib.ps1 b/plugins/sap-dev-core/shared/scripts/sap_settings_lib.ps1 index 278c75c..f2ed57a 100644 --- a/plugins/sap-dev-core/shared/scripts/sap_settings_lib.ps1 +++ b/plugins/sap-dev-core/shared/scripts/sap_settings_lib.ps1 @@ -332,15 +332,18 @@ function Set-SapUserSetting { param( [Parameter(Mandatory)] [string] $Key, [Parameter(Mandatory)] [AllowEmptyString()] [string] $Value, - [switch] $SkipPerConnRouting + [switch] $SkipPerConnRouting, + [ValidateSet('Connection','Session')] [string] $Scope = 'Connection' ) - # Phase 4.4 write-path routing. + # Phase 4.4 write-path routing. -Scope Session targets the per-(AI-session x + # connection) layer (task-scoped, no cross-conversation clobber); default + # Connection keeps the standing per-connection default (zero regression). if (-not $SkipPerConnRouting -and (Get-Command Get-SapPerConnectionDevKeys -ErrorAction SilentlyContinue)) { $perConnKeys = Get-SapPerConnectionDevKeys if ($perConnKeys -contains $Key) { if (Get-Command Set-SapCurrentDevDefault -ErrorAction SilentlyContinue) { - Set-SapCurrentDevDefault -Key $Key -Value $Value + Set-SapCurrentDevDefault -Key $Key -Value $Value -Scope $Scope Reset-SapSettingsCache return } From 1ad91af232ee05d9924059a18bc3dc60fbc2f207 Mon Sep 17 00:00:00 2001 From: HKX BDDEV001 Date: Sat, 20 Jun 2026 12:56:50 +0900 Subject: [PATCH 6/9] feat: Update scope handling for SAP settings and transport requests to default to Session for task isolation --- CLAUDE.md | 5 ++-- .../shared/scripts/sap_connection_lib.ps1 | 12 +++++---- .../shared/scripts/sap_settings_lib.ps1 | 9 ++++--- .../skills/sap-dev-clean/SKILL.md | 14 ++++++++-- .../sap-dev-core/skills/sap-dev-init/SKILL.md | 27 ++++++++++++------- .../skills/sap-transport-request/SKILL.md | 19 +++++++++---- 6 files changed, 59 insertions(+), 27 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7df8a53..e1a8c96 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -530,8 +530,9 @@ keys (`sap_dev_transport_request`, `sap_dev_package`, `sap_dev_function_group`, Then the global settings file. **Writers:** a task TR/package → **Session** scope (`Set-SapUserSetting … -Scope Session`, or the CLI `shared/scripts/sap_dev_default.ps1` whose default is Session) — never a hand-edit of `connections.json`. A deliberate -standing default (onboarding: `/sap-dev-init`, `/sap-login`) → **Connection** scope -(the default of `Set-SapCurrentDevDefault`). Reads go through `Get-SapCurrentDevDefault` +STANDING default (onboarding: `/sap-dev-init`, `/sap-login`) must pass +**`-Scope Connection`** explicitly — **Session is now the writers' default**, so a +task TR/package is isolated without opting in. Reads go through `Get-SapCurrentDevDefault` (centralized), so all callers get the layered resolution for free. Session entries are age-pruned (7 days) — task defaults are ephemeral. diff --git a/plugins/sap-dev-core/shared/scripts/sap_connection_lib.ps1 b/plugins/sap-dev-core/shared/scripts/sap_connection_lib.ps1 index d8e2837..ffa0825 100644 --- a/plugins/sap-dev-core/shared/scripts/sap_connection_lib.ps1 +++ b/plugins/sap-dev-core/shared/scripts/sap_connection_lib.ps1 @@ -1719,14 +1719,16 @@ function Set-SapCurrentDevDefault { param( [Parameter(Mandatory)][string]$Key, [Parameter(Mandatory)][AllowEmptyString()][string]$Value, - [ValidateSet('Connection','Session')][string]$Scope = 'Connection' + [ValidateSet('Connection','Session')][string]$Scope = 'Session' ) $profile = $null try { $profile = Get-SapDevDefaultProfile } catch {} - # Session scope -> task-scoped (AI-session x connection); never touches - # connections.json, so concurrent conversations on the same connection don't - # clobber. Needs an unambiguous connection (for the system-qualifying - # ConnId); with none, fall through to the connection/global write (best effort). + # DEFAULT = Session: a write is task-scoped (per AI-session x connection) and + # never touches connections.json, so concurrent conversations on the same + # connection don't clobber. Pass -Scope Connection for a deliberate STANDING + # default (onboarding). Session needs an unambiguous connection (the + # system-qualifying ConnId); with none, fall through to the connection/global + # write below (best effort). if ($Scope -eq 'Session' -and $profile -and (_NotEmpty "$($profile.id)")) { Set-SapSessionDevDefault -Key $Key -Value $Value -ConnId "$($profile.id)" return diff --git a/plugins/sap-dev-core/shared/scripts/sap_settings_lib.ps1 b/plugins/sap-dev-core/shared/scripts/sap_settings_lib.ps1 index f2ed57a..917bea8 100644 --- a/plugins/sap-dev-core/shared/scripts/sap_settings_lib.ps1 +++ b/plugins/sap-dev-core/shared/scripts/sap_settings_lib.ps1 @@ -333,12 +333,13 @@ function Set-SapUserSetting { [Parameter(Mandatory)] [string] $Key, [Parameter(Mandatory)] [AllowEmptyString()] [string] $Value, [switch] $SkipPerConnRouting, - [ValidateSet('Connection','Session')] [string] $Scope = 'Connection' + [ValidateSet('Connection','Session')] [string] $Scope = 'Session' ) - # Phase 4.4 write-path routing. -Scope Session targets the per-(AI-session x - # connection) layer (task-scoped, no cross-conversation clobber); default - # Connection keeps the standing per-connection default (zero regression). + # Phase 4.4 write-path routing (per-conn keys only). DEFAULT = Session: a + # task TR/package is scoped per (AI-session x connection), so concurrent + # conversations on one connection don't clobber. Pass -Scope Connection for a + # deliberate STANDING per-connection default (onboarding). if (-not $SkipPerConnRouting -and (Get-Command Get-SapPerConnectionDevKeys -ErrorAction SilentlyContinue)) { $perConnKeys = Get-SapPerConnectionDevKeys if ($perConnKeys -contains $Key) { diff --git a/plugins/sap-dev-core/skills/sap-dev-clean/SKILL.md b/plugins/sap-dev-core/skills/sap-dev-clean/SKILL.md index b819366..2dc3f7b 100644 --- a/plugins/sap-dev-core/skills/sap-dev-clean/SKILL.md +++ b/plugins/sap-dev-core/skills/sap-dev-clean/SKILL.md @@ -203,7 +203,16 @@ With `--force`, additionally: If `--settings` was passed, after Steps 3a-3f finish, clear the `sap_dev_transport_request`, `sap_dev_package`, and -`sap_dev_function_group` keys via `/update-config`: +`sap_dev_function_group` standing defaults. Since `/sap-dev-init` now persists +them **Connection-scoped** (the pinned connection's `dev_defaults` block), clear +them THERE — an empty value reads as "unset", so reads fall through: + +```bash +powershell -NoProfile -ExecutionPolicy Bypass -Command ". '\scripts\sap_settings_lib.ps1'; . '\scripts\sap_connection_lib.ps1'; foreach ($k in 'sap_dev_transport_request','sap_dev_package','sap_dev_function_group') { Set-SapUserSetting -Key $k -Value '' -Scope Connection }" +``` + +Also clear the legacy global layer (for any pre-migration values that may still +linger there): ``` /update-config userConfig.sap_dev_transport_request = "" @@ -211,7 +220,8 @@ If `--settings` was passed, after Steps 3a-3f finish, clear the /update-config userConfig.sap_dev_function_group = "" ``` -The next `/sap-dev-init` will then ask the operator for fresh names +(The Session layer is per-conversation and age-pruned, so it needs no explicit +clear here.) The next `/sap-dev-init` will then ask the operator for fresh names (or pick defaults if defaults are configured). --- diff --git a/plugins/sap-dev-core/skills/sap-dev-init/SKILL.md b/plugins/sap-dev-core/skills/sap-dev-init/SKILL.md index 1348c68..f0acd02 100644 --- a/plugins/sap-dev-core/skills/sap-dev-init/SKILL.md +++ b/plugins/sap-dev-core/skills/sap-dev-init/SKILL.md @@ -50,7 +50,7 @@ The settings note below covers the OTHER keys. **Settings reads/writes follow `shared/rules/settings_lookup.md`** — merge per-key on the `.value` field (env var → `settings.local.json` → `userconfig.json` → `settings.json`); non-per-connection writes go to `userconfig.json`. Resolve sap-dev-core paths: 2 levels up from `` to the plugin root, then `settings.json` and (if present) `settings.local.json`. Read `custom_url`, `sap_dev_mode`. -**Per-connection keys (Phase 4.4)**: `sap_dev_mode` is SAP-system-specific (GUI/RFC/BDC capability varies per system). Per `settings_lookup.md` § Per-connection exception, read it from `connections.json[pinned-profile].dev_defaults` FIRST (resolve the pin via `{work_dir}\runtime\session_registry.json` `ai_sessions[]`); only fall back to the two-file merge when `dev_defaults` is empty. Sub-steps that delegate to `/sap-transport-request`, `/sap-se21`, `/sap-function-group` inherit the same per-connection routing for TR/PKG/FG. +**Per-connection keys (Phase 4.4)**: `sap_dev_mode` is SAP-system-specific (GUI/RFC/BDC capability varies per system). Per `settings_lookup.md` § Per-connection exception, read it from `connections.json[pinned-profile].dev_defaults` FIRST (resolve the pin via `{work_dir}\runtime\session_registry.json` `ai_sessions[]`); only fall back to the two-file merge when `dev_defaults` is empty. Sub-steps that delegate to `/sap-transport-request`, `/sap-se21`, `/sap-function-group` inherit the same per-connection routing for TR/PKG/FG. **WRITES — standing dev defaults go in the connection block, not the global layer:** when this skill persists a standing default (TR / package / FG / `way_to_get_transport_request` / `rule_of_tr_description` / `tr_description_template`), write it **Connection-scoped** — `\scripts\sap_dev_default.ps1 -Action set -Key -Value -Scope Connection` (or `Set-SapUserSetting … -Scope Connection`) — NOT `/update-config` (that writes the shared global layer, read only as a last-resort fallback, and not system-qualified). The delegated `/sap-transport-request` now persists the resolved TR **Session-scoped** (task default), so after it resolves the dev TR, ALSO persist that TR Connection-scoped here so the standing dev TR survives across conversations. | Setting | Default if blank | |---|---| @@ -407,8 +407,9 @@ Read `way_to_get_transport_request` from the merged sap-dev-core settings (per ` > 2. `ASK` — Ask each time and (optionally) save your choice as the default. > 3. `CREATE_NEW` — Always create a brand-new TR via `/sap-se01`; never reuse. -Persist the user's choice to `way_to_get_transport_request` via -`/update-config`. +Persist the user's choice to `way_to_get_transport_request` +**Connection-scoped** (standing per-system policy — see the Step 0 WRITES note; +`sap_dev_default.ps1 … -Scope Connection`, not `/update-config`). While asking the policy, also offer the description-rule settings if `rule_of_tr_description` is blank: @@ -419,7 +420,8 @@ While asking the policy, also offer the description-rule settings if > - `FIXED` — use a fixed text I provide > - `RANDOM` — auto-generate a random one -If `PATTERN` or `FIXED`, prompt for `tr_description_template` and persist. +If `PATTERN` or `FIXED`, prompt for `tr_description_template` and persist it +**Connection-scoped** (standing per-system formatting — see the Step 0 WRITES note). The template defaults to `{YYYYMMDD}_{OBJECT_TYPE}_{OBJECT_DESCRIPTION}` if the user just hits enter. @@ -441,7 +443,10 @@ Now act per the policy chosen above. The persistence behaviour matches - `new` → run the **transport-request skill chosen in the Step 0 plan** (`/sap-se01` for GUI, `/sap-transport-request` for RFC) with `OBJECT_TYPE=BASIC OBJECT_DESCRIPTION=SAP_DEV_INIT`. -3. Persist the resolved TR to `sap_dev_transport_request`. +3. Persist the resolved TR to `sap_dev_transport_request` **Connection-scoped** — + it's the STANDING dev TR, and the delegated `/sap-transport-request` only + persisted it Session-scoped, so write the connection block here: + `\scripts\sap_dev_default.ps1 -Action set -Key sap_dev_transport_request -Value -Scope Connection`. #### `ASK` 1. Keep `sap_dev_transport_request` blank. @@ -480,8 +485,10 @@ Only one implementation currently exists: > package name to use (e.g. `ZHKDEVAI`). Press Enter on its own to accept > the default `ZCMDEVAI`. - - If the user provides a name, persist it to `sap_dev_package` via - `/update-config` so subsequent runs reuse it without prompting. + - If the user provides a name, persist it to `sap_dev_package` + **Connection-scoped** (standing per-system default — see the Step 0 WRITES + note; `sap_dev_default.ps1 … -Scope Connection`, not `/update-config`) so + subsequent runs reuse it without prompting. - If the user accepts the default, persist `ZCMDEVAI`. - Validate the name against `/tables/sap_object_naming_rules.tsv` (`PACKAGE` row). If it fails the regex, show the rule and re-prompt. @@ -521,8 +528,10 @@ through and let the skill honour it. > function group name to use (must start with `ZFG`, e.g. `ZFGHKDEV`). > Press Enter on its own to accept the default `ZFGDEVAI`. - - If the user provides a name, persist it to `sap_dev_function_group` via - `/update-config` so subsequent runs reuse it without prompting. + - If the user provides a name, persist it to `sap_dev_function_group` + **Connection-scoped** (standing per-system default — see the Step 0 WRITES + note; `sap_dev_default.ps1 … -Scope Connection`, not `/update-config`) so + subsequent runs reuse it without prompting. - If the user accepts the default, persist `ZFGDEVAI`. - Validate the name against `/tables/sap_object_naming_rules.tsv` (`FUNCTION_GROUP` row, `^ZFG[A-Z0-9_]*$`). If it fails the regex, diff --git a/plugins/sap-dev-core/skills/sap-transport-request/SKILL.md b/plugins/sap-dev-core/skills/sap-transport-request/SKILL.md index 553887b..1eb4d71 100644 --- a/plugins/sap-dev-core/skills/sap-transport-request/SKILL.md +++ b/plugins/sap-dev-core/skills/sap-transport-request/SKILL.md @@ -121,8 +121,14 @@ TR). - User supplies TR → candidate = that TR. - User types `new` → skip to **Create Path**. 3. Verify candidate (Step 1b — mode-aware). If not modifiable, repeat the prompt above. -4. On success, **persist** the resolved TR to `sap_dev_transport_request` via - `/update-config` (so future `DEFAULT` calls reuse it). +4. On success, **persist** the resolved TR to `sap_dev_transport_request` as a + SESSION-scoped task default — so future `DEFAULT` calls in THIS conversation + reuse it WITHOUT clobbering other conversations on the same connection. Run + `\scripts\sap_dev_default.ps1 -Action set -Key + sap_dev_transport_request -Value ` (Session is its default), or + `Set-SapUserSetting -Key sap_dev_transport_request -Value -Scope Session`. + Do NOT use `/update-config` (that writes the global layer, shared across + conversations and systems). See `shared/rules/tr_resolution.md` §4. ### `ASK` @@ -175,8 +181,11 @@ caller. Do NOT fall through to Steps 2-4 from the GUI branch. If during a session the user explicitly says e.g. "switch to ask mode", "always create new from now on", or "use the default TR every time", update -`way_to_get_transport_request` immediately via `/update-config` and follow -the new policy for the rest of the session. +`way_to_get_transport_request` immediately and follow the new policy for the +rest of the session. Because it applies to THIS conversation only, persist it +SESSION-scoped (`Set-SapUserSetting -Key way_to_get_transport_request -Value +<...> -Scope Session`), not `/update-config`. Use `-Scope Connection` only if the +user says it should be the standing policy for that system from now on. --- @@ -293,7 +302,7 @@ Parse the script output for `RESULT_TR:` and `RESULT_STATUS:` lines. | RESULT_STATUS | Meaning | Action | |---|---|---| | `EXISTING_MODIFIABLE` | Provided TR is still modifiable | Report: "Transport request `` is modifiable and ready to use." Persistence per Step 1a (policy-driven). | -| `NEWLY_CREATED` | New TR was created | Report: "Created new transport request ``." Persistence per Step 1a: `DEFAULT` → save automatically; `ASK` → ask the user once; `CREATE_NEW` → do NOT save. Use `/update-config` to write `sap_dev_transport_request` when persisting. | +| `NEWLY_CREATED` | New TR was created | Report: "Created new transport request ``." Persistence per Step 1a: `DEFAULT` → save automatically; `ASK` → ask the user once; `CREATE_NEW` → do NOT save. Persist via the SESSION writer (`sap_dev_default.ps1` / `Set-SapUserSetting -Scope Session`), NOT `/update-config`. | | `ERROR` | Something went wrong | Show full output and diagnose (see error table below). | ### Error Diagnosis From 4fa1548f6615b70f092545a4538fa939202528f4 Mon Sep 17 00:00:00 2001 From: HKX BDDEV001 Date: Sat, 20 Jun 2026 14:57:15 +0900 Subject: [PATCH 7/9] feat: Implement session isolation for parallel conversations in SAP connections --- CLAUDE.md | 2 +- .../shared/rules/sap_session_broker.md | 33 +++ .../shared/scripts/sap_connection_lib.ps1 | 15 ++ .../shared/scripts/sap_log_lib.ps1 | 67 +++++- .../shared/scripts/sap_log_lib.vbs | 20 ++ .../shared/scripts/sap_session_broker.ps1 | 192 +++++++++++++++++- .../sap-dev-core/skills/sap-login/SKILL.md | 35 ++++ 7 files changed, 353 insertions(+), 11 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e1a8c96..36ce948 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -580,7 +580,7 @@ Read by `sap_log_lib.ps1` / `sap_log_lib.vbs`. All keys are optional — default | `log_enabled` | `true` / `false` | `true` | Master switch. When `false`, `Start-SapLog` / `LogStart` still return a run object/id (so wrappers don't crash) but write nothing. | | `log_level` | `DEBUG`, `INFO`, `WARN`, `ERROR`, `OFF` | `INFO` | Minimum level recorded. | | `log_dir` | path | `{work_dir}\logs` | Output directory (auto-created). | -| `log_file_pattern` | template | `sap-dev-{YYYYMMDD}.log` | Filename. Placeholders: `{YYYYMMDD}`, `{YYYYMM}`, `{HHMMSS}`, `{HHMM}`, `{RUN_ID}`, `{SKILL}`, `{USER}`, `{SYSTEM}`. Default groups all runs of a day into one file (cheap log analysis); use `sap-dev-{YYYYMMDD}-{HHMMSS}-{SKILL}.log` for one-file-per-invocation (forensic mode — each run gets its own file, easy to diff), or `sap-dev-{YYYYMMDD}-{RUN_ID}.log` for guaranteed uniqueness even when two skills fire in the same second. | +| `log_file_pattern` | template | `sap-dev-{YYYYMMDD}.log` | Filename. Placeholders: `{YYYYMMDD}`, `{YYYYMM}`, `{HHMMSS}`, `{HHMM}`, `{RUN_ID}`, `{SKILL}`, `{USER}`, `{SYSTEM}`, `{AI_SESSION}`, `{SID}`, `{CLIENT}`. Default groups all runs of a day into one file (cheap log analysis); use `sap-dev-{YYYYMMDD}-{HHMMSS}-{SKILL}.log` for one-file-per-invocation (forensic mode — each run gets its own file, easy to diff), or `sap-dev-{YYYYMMDD}-{RUN_ID}.log` for guaranteed uniqueness even when two skills fire in the same second. **For parallel multi-session builds, use `sap-dev-{YYYYMMDD}-{SID}-{CLIENT}-{AI_SESSION}.log`** — one coherent file per *(AI session × SAP connection)* so concurrent builds never interleave, while each JSONL record still carries `run_id`/`skill` for per-skill drill-down within the file. **Placeholder gotchas:** `{SYSTEM}` = the Windows `%COMPUTERNAME%` (workstation), **NOT** the SAP SID — it does not separate parallel builds on one machine; use `{SID}`/`{CLIENT}` (pinned SAP connection) + `{AI_SESSION}` for that. `{AI_SESSION}` = first 8 chars of the conversation id, resolved from `CLAUDE_CODE_SESSION_ID` (stable across a Claude host restart) → `SAPDEV_AI_SESSION_ID` (override) → `Get-SapAiSessionId` (parent-PID fallback, **drifts** on host restart). `{SID}`/`{CLIENT}` come from the pinned connection profile (best-effort: fall back to empty / the default connection if the AI-session pin is orphaned — e.g. after a host-restart id drift); `{AI_SESSION}` alone already guarantees one-file-per-build uniqueness. PowerShell logger resolves all three directly; the (currently unused) VBS logger reads `{AI_SESSION}` from `CLAUDE_CODE_SESSION_ID`/`SAPDEV_AI_SESSION_ID` and `{SID}`/`{CLIENT}` from `SAPDEV_SID`/`SAPDEV_CLIENT` env vars. | | `log_retention_days` | integer | `30` | Delete `*.log` older than N days (PS1 only, sweep runs ~1-in-50 invocations). `0` = keep forever. | | `log_format` | `JSONL`, `TSV`, `TEXT` | `JSONL` | On-disk record format. `JSONL` is required for `/sap-log-analyze`. `TSV` writes a header row on first write. `TEXT` is human-readable. | | `log_console_echo` | `true` / `false` | `false` | Mirror each record to stdout (or stderr for `WARN` / `ERROR`). | diff --git a/plugins/sap-dev-core/shared/rules/sap_session_broker.md b/plugins/sap-dev-core/shared/rules/sap_session_broker.md index 2b81a79..c009492 100644 --- a/plugins/sap-dev-core/shared/rules/sap_session_broker.md +++ b/plugins/sap-dev-core/shared/rules/sap_session_broker.md @@ -231,6 +231,39 @@ identity (and `set-connection-id` re-binds the profile). --- +### `ensure-own-session` + +``` +pwsh -File sap_session_broker.ps1 -Action ensure-own-session -WorkTemp "" [-TtlSeconds ] [-OwnerSkill ] +``` + +Guarantees the calling **AI session** owns a dedicated session on its +pinned connection, so two conversations logged into the **same** SAP +connection never drive the same `/app/con[N]/ses[M]` (the +`Get-SapCurrentSessionPath` resolver otherwise hands both the connection's +first session). Auto-resolves the AI-session id (parent-PID walk) and: + +1. already claims a session here → refresh + return it; +2. else the session it currently *resolves to* (mirror of + `Get-SapCurrentSessionPath`: first `free`, else first entry) is + **formalized** — claimed in place, no GUI navigation — UNLESS that + session is claimed by a *different live* AI session; +3. else (resolved target taken by a live other) → **spawn** a fresh + session, reset only the newcomer to Easy Access, and claim it. + +The claim's `owner_pid` is the AI session's conversation PID (reverse- +mapped from `runtime\ai_session_by_pid\`), so the PID-death sweep releases +it when that conversation ends. Idempotent and non-disruptive (never +navigates another conversation's session). Wired into `/sap-login` +**Step 6.7** so isolation is automatic for every parallel login. Output: +``` +OWN_SESSION: path=

connection= reused= spawned= [formalized=true] +NO_PIN: # this AI session is not pinned to a live connection yet +DENIED: # contended connection + spawn failed (exit 1) +``` + +--- + ## Registry schema `{WORK_TEMP}\session_registry.json`, UTF-8 no BOM. Schema v2 (multi- diff --git a/plugins/sap-dev-core/shared/scripts/sap_connection_lib.ps1 b/plugins/sap-dev-core/shared/scripts/sap_connection_lib.ps1 index ffa0825..98faccc 100644 --- a/plugins/sap-dev-core/shared/scripts/sap_connection_lib.ps1 +++ b/plugins/sap-dev-core/shared/scripts/sap_connection_lib.ps1 @@ -1225,6 +1225,21 @@ function Get-SapAiSessionId { return $env:SAPDEV_AI_SESSION_ID } + # Prefer the Claude-provided conversation id. It is STABLE for the whole + # conversation and survives a Claude host-process restart -- unlike the + # parent-PID owner heuristic below, which mints a NEW id when the host PID + # changes, silently orphaning this AI session's broker pin / session + # dev-defaults and falling resolution back to the DEFAULT connection (a + # latent wrong-system risk for long-running or parallel builds across a + # host restart). Every consumer routes through this function (broker + # `acquire`, /sap-login `Resolve-AiSessionId`, Get-SapCurrentConnectionProfile, + # the log filename), so anchoring here fixes them all at once. Falls through + # to the parent-PID walk when unset (non-Claude-Code invocations, older + # hosts) -- behaviour outside Claude Code is unchanged. + if (-not [string]::IsNullOrWhiteSpace($env:CLAUDE_CODE_SESSION_ID)) { + return $env:CLAUDE_CODE_SESSION_ID + } + if ([string]::IsNullOrWhiteSpace($RuntimeDir)) { $RuntimeDir = Get-SapWorkRuntimeDir } diff --git a/plugins/sap-dev-core/shared/scripts/sap_log_lib.ps1 b/plugins/sap-dev-core/shared/scripts/sap_log_lib.ps1 index 5457b51..2cb0105 100644 --- a/plugins/sap-dev-core/shared/scripts/sap_log_lib.ps1 +++ b/plugins/sap-dev-core/shared/scripts/sap_log_lib.ps1 @@ -122,19 +122,68 @@ function Resolve-SapLogPath { } $now = Get-Date $name = $cfg.Pattern + + # AI-session + pinned SAP-connection identity, for per-(AI session x SAP + # connection) log files: one coherent file per build, so parallel builds + # (each a distinct conversation, usually on a distinct connection) never + # interleave -- while every JSONL record still carries run_id/skill for + # per-skill drill-down WITHIN the file. Resolved best-effort: env-var + # override first (SAPDEV_AI_SESSION_ID / SAPDEV_SID / SAPDEV_CLIENT, also + # the path VBS callers use), then the connection-lib functions IF they are + # loaded in this scope -- they are when invoked via sap_log_helper.ps1 + # 'start', which dot-sources sap_connection_lib.ps1 before calling + # Start-SapLog -- else empty. Empty is safe: the segment just drops out. + # The path is pinned at run START and reused for step/end (persisted in the + # helper state file), so this resolves exactly once per run. + $aiSession = '' + try { + # Prefer the Claude-provided conversation id: it is stable for the whole + # conversation and -- unlike the parent-PID heuristic in + # Get-SapAiSessionId -- survives a Claude host-process restart, so a long + # build keeps writing to ONE file instead of splitting at the restart. + # SAPDEV_AI_SESSION_ID stays the explicit test / override hook. + if (-not [string]::IsNullOrWhiteSpace($env:SAPDEV_AI_SESSION_ID)) { + $aiSession = "$($env:SAPDEV_AI_SESSION_ID)" + } elseif (-not [string]::IsNullOrWhiteSpace($env:CLAUDE_CODE_SESSION_ID)) { + $aiSession = "$($env:CLAUDE_CODE_SESSION_ID)" + } elseif (Get-Command Get-SapAiSessionId -ErrorAction SilentlyContinue) { + $aiSession = "$(Get-SapAiSessionId)" + } + } catch {} + # Shorten the GUID-like id to 8 chars -- enough to disambiguate parallel + # conversations, short enough for a readable filename. + if ($aiSession.Length -gt 8) { $aiSession = $aiSession.Substring(0, 8) } + + $sid = ''; $client = '' + try { + if (-not [string]::IsNullOrWhiteSpace($env:SAPDEV_SID)) { $sid = "$($env:SAPDEV_SID)" } + if (-not [string]::IsNullOrWhiteSpace($env:SAPDEV_CLIENT)) { $client = "$($env:SAPDEV_CLIENT)" } + if ([string]::IsNullOrWhiteSpace($sid) -and (Get-Command Get-SapCurrentConnectionProfile -ErrorAction SilentlyContinue)) { + $prof = Get-SapCurrentConnectionProfile + if ($prof) { + if ([string]::IsNullOrWhiteSpace($sid)) { $sid = "$($prof.system_name)" } + if ([string]::IsNullOrWhiteSpace($client)) { $client = "$($prof.client)" } + } + } + } catch {} + # Date / time placeholders -- {HHMMSS} gives per-second uniqueness so a # pattern like 'sap-dev-{YYYYMMDD}-{HHMMSS}-{SKILL}.log' produces one # file per skill invocation. {RUN_ID} is the unique GUID assigned at # Start-SapLog; use it for absolute uniqueness even if two skills fire - # in the same second. - $name = $name.Replace('{YYYYMMDD}', $now.ToString('yyyyMMdd')) - $name = $name.Replace('{YYYYMM}', $now.ToString('yyyyMM')) - $name = $name.Replace('{HHMMSS}', $now.ToString('HHmmss')) - $name = $name.Replace('{HHMM}', $now.ToString('HHmm')) - $name = $name.Replace('{RUN_ID}', ($RunId -replace '[^A-Za-z0-9_\-]', '_')) - $name = $name.Replace('{SKILL}', ($Skill -replace '[^A-Za-z0-9_\-]', '_')) - $name = $name.Replace('{USER}', ($env:USERNAME -replace '[^A-Za-z0-9_\-]', '_')) - $name = $name.Replace('{SYSTEM}', ($env:COMPUTERNAME -replace '[^A-Za-z0-9_\-]', '_')) + # in the same second. {AI_SESSION}/{SID}/{CLIENT} group a whole build + # (one conversation on one SAP connection) into a single file. + $name = $name.Replace('{YYYYMMDD}', $now.ToString('yyyyMMdd')) + $name = $name.Replace('{YYYYMM}', $now.ToString('yyyyMM')) + $name = $name.Replace('{HHMMSS}', $now.ToString('HHmmss')) + $name = $name.Replace('{HHMM}', $now.ToString('HHmm')) + $name = $name.Replace('{RUN_ID}', ($RunId -replace '[^A-Za-z0-9_\-]', '_')) + $name = $name.Replace('{SKILL}', ($Skill -replace '[^A-Za-z0-9_\-]', '_')) + $name = $name.Replace('{USER}', ($env:USERNAME -replace '[^A-Za-z0-9_\-]', '_')) + $name = $name.Replace('{SYSTEM}', ($env:COMPUTERNAME -replace '[^A-Za-z0-9_\-]', '_')) + $name = $name.Replace('{AI_SESSION}', ($aiSession -replace '[^A-Za-z0-9_\-]', '_')) + $name = $name.Replace('{SID}', ($sid -replace '[^A-Za-z0-9_\-]', '_')) + $name = $name.Replace('{CLIENT}', ($client -replace '[^A-Za-z0-9_\-]', '_')) return Join-Path $cfg.Dir $name } diff --git a/plugins/sap-dev-core/shared/scripts/sap_log_lib.vbs b/plugins/sap-dev-core/shared/scripts/sap_log_lib.vbs index 6fbb0b7..1fabcce 100644 --- a/plugins/sap-dev-core/shared/scripts/sap_log_lib.vbs +++ b/plugins/sap-dev-core/shared/scripts/sap_log_lib.vbs @@ -175,6 +175,26 @@ Function LogResolvePath(sSkill, sRunId) Dim oWsh : Set oWsh = CreateObject("WScript.Shell") sName = Replace(sName, "{USER}", LogSanitize(oWsh.ExpandEnvironmentStrings("%USERNAME%"))) sName = Replace(sName, "{SYSTEM}", LogSanitize(oWsh.ExpandEnvironmentStrings("%COMPUTERNAME%"))) + ' {AI_SESSION}/{SID}/{CLIENT}: per-(AI session x SAP connection) grouping so + ' parallel builds never interleave. VBS cannot call the PowerShell resolvers + ' (Get-SapAiSessionId / Get-SapCurrentConnectionProfile), so it reads the + ' values from env vars the launching PowerShell wrapper exports. Empty when + ' unset -- the segment just drops out. (No operational VBS logs to file + ' today; this keeps parity with the PowerShell logger for any future caller.) + ' Prefer SAPDEV_AI_SESSION_ID (explicit override), then CLAUDE_CODE_SESSION_ID + ' (Claude-provided, stable across host restarts). ExpandEnvironmentStrings + ' returns the literal "%VAR%" when a var is unset, so an InStr "%" check = unset. + Dim sAi : sAi = oWsh.ExpandEnvironmentStrings("%SAPDEV_AI_SESSION_ID%") + If InStr(sAi, "%") > 0 Then sAi = oWsh.ExpandEnvironmentStrings("%CLAUDE_CODE_SESSION_ID%") + If InStr(sAi, "%") > 0 Then sAi = "" ' both unset + If Len(sAi) > 8 Then sAi = Left(sAi, 8) + sName = Replace(sName, "{AI_SESSION}", LogSanitize(sAi)) + Dim sSid : sSid = oWsh.ExpandEnvironmentStrings("%SAPDEV_SID%") + If InStr(sSid, "%") > 0 Then sSid = "" + sName = Replace(sName, "{SID}", LogSanitize(sSid)) + Dim sCli : sCli = oWsh.ExpandEnvironmentStrings("%SAPDEV_CLIENT%") + If InStr(sCli, "%") > 0 Then sCli = "" + sName = Replace(sName, "{CLIENT}", LogSanitize(sCli)) LogResolvePath = cfg("Dir") & "\" & sName End Function diff --git a/plugins/sap-dev-core/shared/scripts/sap_session_broker.ps1 b/plugins/sap-dev-core/shared/scripts/sap_session_broker.ps1 index 78a1071..d5db3eb 100644 --- a/plugins/sap-dev-core/shared/scripts/sap_session_broker.ps1 +++ b/plugins/sap-dev-core/shared/scripts/sap_session_broker.ps1 @@ -49,7 +49,7 @@ param( [Parameter(Mandatory = $true)] [ValidateSet('acquire', 'release', 'gc', 'list', 'discover', - 'stuck', 'pin', 'unpin', 'set-connection-id')] + 'stuck', 'pin', 'unpin', 'set-connection-id', 'ensure-own-session')] [string] $Action, [Parameter(Mandatory = $true)] @@ -491,6 +491,57 @@ function Is-ProcessAlive { try { $p = Get-Process -Id $ProcessId -ErrorAction Stop; return ($null -ne $p) } catch { return $false } } +# =========================================================================== +# AI-session liveness map (Phase 4.5: per-AI-session session ownership) +# --------------------------------------------------------------------------- +# {work_dir}\runtime\ai_session_by_pid\.txt holds the ai_session_id owned +# by conversation process . These helpers read that directory to (a) list +# the ai-session ids whose owning process is still alive (the contention check +# in ensure-own-session) and (b) reverse-map a given ai-session id back to its +# live owner PID, so an ownership claim is tied to the RIGHT conversation's +# lifetime even when the claim is written on another conversation's behalf +# (PID-death sweep then cleans it up automatically). +# =========================================================================== + +function Get-AiSessionPidDir { + return (Join-Path $script:WorkRuntimeDir 'ai_session_by_pid') +} + +function Get-LiveAiSessionIds { + $dir = Get-AiSessionPidDir + $live = @{} + if (Test-Path $dir) { + foreach ($f in (Get-ChildItem -Path $dir -Filter '*.txt' -File -ErrorAction SilentlyContinue)) { + $pidName = [System.IO.Path]::GetFileNameWithoutExtension($f.Name) + $n = 0 + if ([int]::TryParse($pidName, [ref]$n) -and (Is-ProcessAlive -ProcessId $n)) { + $aid = '' + try { $aid = (Get-Content -Path $f.FullName -Raw -ErrorAction Stop) } catch {} + $aid = ("" + $aid).Trim() + if ($aid -ne '') { $live[$aid] = $true } + } + } + } + return $live +} + +function Get-PidForAiSession { + param([string] $AiId) + if ([string]::IsNullOrWhiteSpace($AiId)) { return 0 } + $dir = Get-AiSessionPidDir + if (-not (Test-Path $dir)) { return 0 } + foreach ($f in (Get-ChildItem -Path $dir -Filter '*.txt' -File -ErrorAction SilentlyContinue)) { + $aid = '' + try { $aid = (Get-Content -Path $f.FullName -Raw -ErrorAction Stop) } catch {} + if (("" + $aid).Trim() -eq $AiId) { + $pidName = [System.IO.Path]::GetFileNameWithoutExtension($f.Name) + $n = 0 + if ([int]::TryParse($pidName, [ref]$n)) { return $n } + } + } + return 0 +} + # =========================================================================== # Identity reconciliation helpers # --------------------------------------------------------------------------- @@ -1368,6 +1419,144 @@ function Invoke-Unpin { Write-Host $script:Result } +# =========================================================================== +# Action: ensure-own-session (Phase 4.5 -- parallel-conversation isolation) +# --------------------------------------------------------------------------- +# Guarantee that THIS ai-session owns a dedicated SAP session on its pinned +# connection, so two conversations sharing one SAP connection never drive the +# same /app/con[N]/ses[M]. Idempotent and non-disruptive: it never navigates a +# session another conversation may be mid-task in -- it only writes an ownership +# claim, and when this ai-session's resolved target is already claimed by a +# DIFFERENT LIVE ai-session it spawns a NEW session and resets only that +# newcomer to Easy Access. +# +# Resolution (mirrors Get-SapCurrentSessionPath within the pinned connection): +# 1. Already claim an entry here -> refresh + done. +# 2. Compute the entry this ai-session currently resolves to (first 'free', +# else first entry). If it is NOT explicitly claimed by a different live +# ai-session -> formalize: claim it in place (no GUI navigation). This is +# the "first conversation keeps ses[0]" / "formalize an existing owner" +# path. +# 3. Otherwise (target taken by a live other) -> spawn a fresh session, +# verify Easy Access, and claim it. This is the "second conversation gets +# its own ses[1]" path. +# +# The claim's owner_pid is the AI-session's conversation PID (reverse-looked-up +# from ai_session_by_pid), so the existing PID-death sweep auto-releases it when +# that conversation ends. Wired into /sap-login finalize; also callable +# standalone to formalize an existing session's owner. +# =========================================================================== + +function Invoke-EnsureOwnSession { + if ([string]::IsNullOrWhiteSpace($AiSessionId)) { + Write-Host 'NO_PIN: ai_session_id could not be resolved; nothing to isolate' + return + } + $stableTask = if ($TaskId -ne '') { $TaskId } else { "ownsession-$AiSessionId" } + $ttl = if ($TtlSeconds -gt 0) { $TtlSeconds } else { 600 } + $claimPid = Get-PidForAiSession -AiId $AiSessionId + if ($claimPid -le 0 -and $OwnerPid -gt 0) { $claimPid = $OwnerPid } + + $script:Result = '' + With-RegistryLock { + $reg = Read-Registry + $swept = Sweep-StaleEntries -Registry $reg -VerboseDrops $false + + # Resolve this ai-session's pinned connection block. + $pinConnId = '' + if ($reg.ai_sessions.ContainsKey($AiSessionId)) { $pinConnId = "$($reg.ai_sessions[$AiSessionId].connection_id)" } + if (-not $pinConnId) { + Persist-IfSwept -Registry $reg -Swept $swept + $script:Result = "NO_PIN: ai_session $AiSessionId is not pinned to a connection; run /sap-login first" + return + } + $cb = $reg.connections | Where-Object { "$($_.connection_id)" -eq $pinConnId } | Select-Object -First 1 + if (-not $cb) { + Persist-IfSwept -Registry $reg -Swept $swept + $script:Result = "NO_PIN: pinned connection_id=$pinConnId is not currently live; nothing to isolate" + return + } + $targetCon = "$($cb.connection_path)" + $now = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ss') + + # 1. Already own a session here -> refresh + done (idempotent). + $mine = $cb.entries | Where-Object { "$($_.ai_session_id)" -eq $AiSessionId } | Select-Object -First 1 + if ($mine) { + $mine.status = 'claimed' + $mine.task_id = $stableTask + $mine.owner_skill = $OwnerSkill + if ($claimPid -gt 0) { $mine.owner_pid = $claimPid } + $mine.claim_time = $now + $mine.ttl_seconds = $ttl + $reg.ai_sessions[$AiSessionId].last_seen_at = $now + Write-Registry -Registry $reg + $script:Result = "OWN_SESSION: path=$($mine.path) connection=$targetCon reused=true spawned=false" + return + } + + # 2. Compute the entry this ai-session currently resolves to (mirror of + # Get-SapCurrentSessionPath within a connection: first 'free', else + # first entry). Formalize it unless a different LIVE ai-session owns it. + $liveAi = Get-LiveAiSessionIds + $target = $cb.entries | Where-Object { "$($_.status)" -eq 'free' } | Select-Object -First 1 + if (-not $target) { $target = $cb.entries | Select-Object -First 1 } + + if ($target) { + $owner = "$($target.ai_session_id)" + $takenByLiveOther = ($owner -ne '' -and $owner -ne $AiSessionId -and $liveAi.ContainsKey($owner)) + if (-not $takenByLiveOther) { + $target.status = 'claimed' + $target.task_id = $stableTask + $target.ai_session_id = $AiSessionId + $target.owner_skill = $OwnerSkill + if ($claimPid -gt 0) { $target.owner_pid = $claimPid } + $target.claim_time = $now + $target.ttl_seconds = $ttl + $reg.ai_sessions[$AiSessionId].last_seen_at = $now + Write-Registry -Registry $reg + $script:Result = "OWN_SESSION: path=$($target.path) connection=$targetCon reused=false spawned=false formalized=true" + return + } + } + + # 3. Our resolved target is taken by a different live ai-session (or no + # entry exists) -> spawn a fresh session and claim it. + $newSes = Spawn-NewSession -TargetConnectionPath $targetCon + if (-not $newSes) { + Persist-IfSwept -Registry $reg -Swept $swept + $script:Result = "DENIED: contended connection $targetCon and spawn failed (session cap reached or SAP GUI unreachable)" + return + } + # Reset only the NEWCOMER to Easy Access (never touches the other + # conversation's session). + $snap = Resolve-SapSessionSnap -Path $newSes.path + if (-not (Is-SessionAtEasyAccess -Snap $snap)) { + [void](Reset-SessionToEasyAccess -Path $newSes.path) + } + $cb.entries += @{ + path = "$($newSes.path)" + session_number = [int]$newSes.session_number + task_id = $stableTask + ai_session_id = $AiSessionId + owner_pid = $claimPid + owner_skill = $OwnerSkill + status = 'claimed' + claim_time = $now + ttl_seconds = $ttl + discovered = $false + stuck_program = '' + stuck_screen = '' + was_created = $true + } + $reg.ai_sessions[$AiSessionId].last_seen_at = $now + Write-Registry -Registry $reg + $script:Result = "OWN_SESSION: path=$($newSes.path) connection=$targetCon reused=false spawned=true" + } + + Write-Host $script:Result + if ($script:Result.StartsWith('DENIED')) { exit 1 } +} + # =========================================================================== # Dispatch # =========================================================================== @@ -1382,5 +1571,6 @@ switch ($Action) { 'pin' { Invoke-Pin } 'unpin' { Invoke-Unpin } 'set-connection-id' { Invoke-SetConnectionId } + 'ensure-own-session' { Invoke-EnsureOwnSession } } exit 0 diff --git a/plugins/sap-dev-core/skills/sap-login/SKILL.md b/plugins/sap-dev-core/skills/sap-login/SKILL.md index 9681b1c..b52420b 100644 --- a/plugins/sap-dev-core/skills/sap-login/SKILL.md +++ b/plugins/sap-dev-core/skills/sap-login/SKILL.md @@ -709,6 +709,41 @@ Access and free for the user or another AI session. --- +### Step 6.7 — Ensure a dedicated session (parallel-conversation isolation) + +After the pin is set, claim a **dedicated SAP session** for this +conversation so two conversations logged into the **same** SAP connection +never drive the same `/app/con[N]/ses[M]` (without this, +`Get-SapCurrentSessionPath` hands both the connection's first session and +they trample each other). Best-effort — do **not** fail the login if this +step errors: + +```bash +powershell -ExecutionPolicy Bypass -File "\scripts\sap_session_broker.ps1" -Action ensure-own-session -WorkTemp "{WORK_TEMP}" -TtlSeconds 2592000 -OwnerSkill sap-login +``` + +The broker auto-resolves this conversation's AI-session id (parent-PID +walk) and prints one of: + +- `OWN_SESSION: … formalized=true` — **first / sole** conversation on the + connection: it claims the session it already resolves to (usually + `ses[0]`); no new window opens. +- `OWN_SESSION: … spawned=true` — **a second live conversation** is already + on this connection: it opens a fresh `ses[1]` (resetting only that + newcomer to Easy Access — the other conversation's session is never + touched) and claims it. Tell the user a second SAP session window opened + for this conversation. +- `NO_PIN: …` — nothing pinned yet (e.g. an RFC-only flow); nothing to + isolate, continue. + +The claim's `owner_pid` is this conversation's process, so the broker's +PID-death sweep releases it automatically when the conversation ends. From +here on `Get-SapCurrentSessionPath` returns this conversation's own session +and every downstream skill wrapper picks it up via +`$env:SAPDEV_SESSION_PATH` (see the resolution contract below). + +--- + ## Consumer-skill resolution contract (unchanged interface, Phase-4 enriched) Every downstream skill resolves its target session like this: From aa1c21530855cd9ad38802b7f73aace9a2cc377e Mon Sep 17 00:00:00 2001 From: HKX BDDEV001 Date: Sat, 20 Jun 2026 16:19:01 +0900 Subject: [PATCH 8/9] feat: Enhance parallel-safety model with additional isolation layers for SAP-driving VBS --- CLAUDE.md | 2 +- contributing/parallel_safe_session_attach.md | 126 ++++++++++++++++++- 2 files changed, 125 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 36ce948..5d99e83 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -278,7 +278,7 @@ Cross-plugin shared files live at `plugins/sap-dev-core/shared/`. All other plug | `shared/rules/tr_resolution.md` | **All deploy skills (mandatory)** + `/sap-transport-request` + `/sap-se01` | Transport request resolution flow — `way_to_get_transport_request` (DEFAULT/ASK/CREATE_NEW), `rule_of_tr_description` (ASK/PATTERN/FIXED/RANDOM), 60-char compression | | `shared/rules/language_independence_rules.md` | **All GUI-scripting skills (mandatory)** | Make VBS work under any logon language — identify by component ID + DDIC field name, status-bar checks via `MessageType` codes (S/W/E/I/A), VKey instead of menu-text, no branching on `.Text`/`.Tooltip`/window titles | | `shared/rules/sap_session_broker.md` | **All GUI-scripting skills that may run in parallel** — will become broadly mandatory after the Tier 3 attach-helper migration. Today: `sap-gui-skill-scaffold` parallel path + the 4 Phase-3.1 migrated read-only skills. | SAP GUI Session Broker contract (v2, Phase 3.5 — multi-connection aware). Coordinates which AI task drives which SAP GUI session via `acquire` / `release` / `discover` / `gc` / `list` CLI on `sap_session_broker.ps1`. **Connection isolation guaranteed**: a claim resolved against `/app/con[1]` never returns a `/app/con[0]` path; ambiguous acquires (multiple connections, no resolver) are refused loud. Acquire's targeting args (`-SessionPath` / `-ConnectionPath` / `-SystemName -Client -User` / `-PinFile`) determine which connection the session comes from; resolution falls back to sole-connection auto-default for the 99% single-connection case. Cross-process named-mutex (`SapDevSessionBroker_v2`) serialises concurrent callers. Reactive cleanup + identity-reconciliation sweep mirrors live identity onto each block then handles these failure modes — user closes a window, owner process crashes, TTL expires, relogin (incl. a slot reused by a DIFFERENT system, detected by the `(system,client,user)` tuple), entire connection closed — surfaced as `DROP: reason=<...>` lines. Identity caveat: SAP exposes no stable per-session ID on the kernels tested — `SystemSessionId` is **per-workstation, not per-logon** (it does NOT change across a logout+relogin or an A→B system swap on one slot), and `(path, SessionNumber)` recycles together — so the broker keys connection identity on the `(system,client,user)` tuple and uses operational hygiene (Easy-Access verification on every acquire + `findById` re-resolution on every use) instead of a session id. Pairs with `shared/scripts/sap_session_broker.ps1` (PowerShell broker) + `shared/scripts/sap_session_broker_com.vbs` (32-bit cscript COM helper — required because PowerShell 7+/.NET 5+ cannot bind to the SAP GUI Scripting Engine directly). | -| `contributing/parallel_safe_session_attach.md` (repo-level, NOT shipped with the plugin) | **All authors writing new SAP-driving VBS templates (mandatory; enforced by CI gate in `sap-dev/scripts/check-consistency.mjs`)**. Read this BEFORE writing a new operational `.vbs` under `plugins//skills//references/`. Lives outside the plugin because it has zero runtime callers — end users installing the plugin from marketplace don't need it. | Architectural contract for the **per-VBS attach pattern** that's complementary to the broker contract above. Every SAP-driving VBS template must: (1) declare `Const SESSION_PATH = "%%SESSION_PATH%%"`, (2) `ExecuteGlobal`-include `%%ATTACH_LIB_VBS%%`, (3) call `Set oSession = AttachSapSession(SESSION_PATH)` — and nothing else for attach. Every wrapping SKILL.md PS block must substitute the two new tokens and set `$env:SAPDEV_SESSION_PATH` via `Get-SapCurrentSessionPath` (Phase 4.2). The rule doc spells out the canonical VBS pattern + the canonical SKILL.md wrapper convention as copy-paste blocks, lists the 7 exempt files (`sap_login.vbs`, `sap_check_gui_login_status.vbs`, `sap_gui_security_warmup.vbs`, `sap_login_capture_active_session.vbs`, `sap_gui_object_details.vbs`, `sap_gui_probe_action.vbs`, `sap_attach_lib.vbs`) and why each is exempt, documents the four common gotchas (`Chr(37)` sentinel idiom, include order with session-lock, helper handles all error paths, `SAPDEV_SESSION_PATH` is read via env var), and ends with the CI verification command. **The CI gate fires on five conditions**: legacy `For Each` idiom present, `SESSION_PATH` declared without `%%ATTACH_LIB_VBS%%` include, operational VBS with neither token (unmigrated), include without `AttachSapSession(...)` call (dead code), or wrapping SKILL.md missing the substitution when the template needs it. **History tail** at the bottom of the rule doc traces Tier 3 phase chronology (3.0 helper → 3.1 read-only → 3.5 multi-conn → 3.2/3/4 bulk → 3.6 CI gate). Read this when joining the project or adding the first new SAP-driving skill. | +| `contributing/parallel_safe_session_attach.md` (repo-level, NOT shipped with the plugin) | **All authors writing new SAP-driving VBS templates (mandatory; enforced by CI gate in `sap-dev/scripts/check-consistency.mjs`)**. Read this BEFORE writing a new operational `.vbs` under `plugins//skills//references/`. Lives outside the plugin because it has zero runtime callers — end users installing the plugin from marketplace don't need it. | Architectural contract for the **per-VBS attach pattern** (layer 1) that's complementary to the broker contract above; the doc now also summarizes the other three parallel-safety layers (layer 2 temp buckets, layer 3 per-session dev defaults, layer 4 per-session logs). Every SAP-driving VBS template must: (1) declare `Const SESSION_PATH = "%%SESSION_PATH%%"`, (2) `ExecuteGlobal`-include `%%ATTACH_LIB_VBS%%`, (3) call `Set oSession = AttachSapSession(SESSION_PATH)` — and nothing else for attach. Every wrapping SKILL.md PS block must substitute the two new tokens and set `$env:SAPDEV_SESSION_PATH` via `Get-SapCurrentSessionPath` (Phase 4.2). The rule doc spells out the canonical VBS pattern + the canonical SKILL.md wrapper convention as copy-paste blocks, lists the 7 exempt files (`sap_login.vbs`, `sap_check_gui_login_status.vbs`, `sap_gui_security_warmup.vbs`, `sap_login_capture_active_session.vbs`, `sap_gui_object_details.vbs`, `sap_gui_probe_action.vbs`, `sap_attach_lib.vbs`) and why each is exempt, documents the four common gotchas (`Chr(37)` sentinel idiom, include order with session-lock, helper handles all error paths, `SAPDEV_SESSION_PATH` is read via env var), and ends with the CI verification command. **The CI gate fires on seven conditions** — five for session attach (legacy `For Each` idiom present, `SESSION_PATH` declared without `%%ATTACH_LIB_VBS%%` include, operational VBS with neither token (unmigrated), include without `AttachSapSession(...)` call (dead code), or wrapping SKILL.md missing the substitution when the template needs it) plus two for the run-temp model (a SKILL.md passing `{RUN_TEMP}` to `Get-SapCurrentSessionPath -WorkTemp` — hard error; a SKILL.md writing fixed-named generated scratch under the `{WORK_TEMP}` root instead of `{RUN_TEMP}` — warning). **History tail** at the bottom of the rule doc traces the chronology (3.0 helper → 3.1 read-only → 3.5 multi-conn → 3.2/3/4 bulk → 3.6 CI gate → 2026-06-20 layers 2–4: temp buckets / dev defaults / logs). Read this when joining the project or adding the first new SAP-driving skill. | | `contributing/golden_screen_baselines.md` (repo-level, NOT shipped with the plugin) | **All authors adding/maintaining a SAP-driving VBS** — defines the screen-fingerprint baseline contract enforced by the coverage gate in `sap-dev/scripts/check-consistency.mjs`. Half 1 (static) of the GUI-robustness harness; half 2 is the `/sap-gui-screen-check` live skill (PowerShell orchestrator `sap_screen_check.ps1` + self-resolving probe `sap_screen_check_probe.vbs`). | Per-VBS golden-screen baseline `references/.screens.json` (schema `sapdev.screenbaseline/1`): `{ schema, vbs, captured_on{release,kernel,date,method}, checkpoints[]{id, reach, identity{program,dynpro}, required_ids[], status} }`. Records the control IDs + screen identity each VBS depends on per checkpoint so drift (a release/locale that moved a control) is caught BEFORE a user hits a silent false-success. **CI gate** (driving VBS = declares `Const SESSION_PATH` / includes `%%ATTACH_LIB_VBS%%` / calls `AttachSapSession` / binds the Scripting engine, minus the Tier-3 exempt set): **WARN** on a missing baseline (ratcheting `screen-baseline coverage N/M`, does not break the build), **HARD error** on a malformed one. `status` ∈ `captured` (identity verified live) \| `pending_live` (static dependency-set seed, identity captured on first `/sap-gui-screen-check` run). Seeded (coverage 7/116, all `pending_live`): se38/se37/se24 create + se11 {domain,dataelement,structure,table} create initial screens. Promote missing→hard-error once coverage hits 100%. | | `shared/rules/abap_code_quality_rules.md` | sap-gen-abap, sap-check-abap, sap-fix-abap | ABAP code-quality rules — modern syntax, OOP scaffolds, exception classes, performance gates, authz hooks, ABAP Unit, dependency + traceability emission. Driven by the customer brief. | | `shared/rules/frequently_errors.md` | sap-gen-abap, sap-se38/se37/se24, sap-atc, sap-error-kb | Contract for the **frequently_errors feedback loop** — 3 tiers + precedence, schema, statuses (CONFIRMED/CANDIDATE/MUTE), read path (gen Step 1.5f), write path (deploy/ATC auto-record), curation. The data layer of the trap knowledge in `abap_code_quality_rules.md`. | diff --git a/contributing/parallel_safe_session_attach.md b/contributing/parallel_safe_session_attach.md index 02c65a8..e22378f 100644 --- a/contributing/parallel_safe_session_attach.md +++ b/contributing/parallel_safe_session_attach.md @@ -7,6 +7,19 @@ via `GetObject("SAPGUI") + GetScriptingEngine`. **Enforced by**: `sap-dev/scripts/check-consistency.mjs` — runs in CI; fails the build on legacy patterns. +> **Scope note (2026-06-20).** Session attach is **layer 1** of the +> parallel-safety model. After a batch of concurrent same-spec builds (one +> `MaterialUpload` spec built simultaneously across several SAP connections, plus +> several conversations on one connection) surfaced cross-session clobbering that +> attach alone doesn't cover, three more layers were added — **layer 2** per-run +> temp isolation, **layer 3** per-session dev defaults, **layer 4** per-session +> log files. They're summarized in *"Beyond session attach — the other three +> isolation layers"* near the end of this doc; CLAUDE.md holds the authoritative +> per-key detail. The temp model also gained a **runtime** enforcer, +> `sap-dev/scripts/run-temp-hook.mjs` (a `PreToolUse` hook), because the static +> `check-consistency.mjs` can't see the running *cache* copy of a skill or ad-hoc +> agent/orchestrator scratch. + --- ## TL;DR for new skill authors @@ -165,9 +178,9 @@ cd sap-dev node scripts/check-consistency.mjs ``` -Should print: +Should print (counts grow as the repo grows — the shape is what matters): ``` -OK: 3 plugins, 49 skills, all manifests aligned at version 0.3.0, Tier 3 attach contract clean +OK: 4 plugins, 78 skills, all manifests aligned at version 0.6.5, Tier 3 attach contract clean, 10 non-ASCII warning(s), 30 run-temp warning(s), screen-baseline coverage 7/119 (112 unbaselined) ``` On failure, the script lists each non-conforming file with a specific reason. The check covers: @@ -177,6 +190,8 @@ On failure, the script lists each non-conforming file with a specific reason. Th 3. VBS that drives SAP GUI (matches `GetObject("SAPGUI")` or `GetScriptingEngine`) but uses neither token → unmigrated. 4. VBS with `%%ATTACH_LIB_VBS%%` include but no `AttachSapSession(...)` call → dead include. 5. SKILL.md that wraps a template requiring `%%ATTACH_LIB_VBS%%` but doesn't substitute it → the runtime VBS will fail to attach. +6. SKILL.md that passes `{RUN_TEMP}` to `Get-SapCurrentSessionPath -WorkTemp` → **hard error** (that call derives `{work_dir}\runtime` from the parent, so a run-scoped path relocates `session_registry.json`; keep the base `{WORK_TEMP}` there). Layer 2. +7. SKILL.md that writes fixed-named generated scratch under the shared `{WORK_TEMP}` base instead of `{RUN_TEMP}` → **run-temp warning** (the Bucket-A coordination files are exempt via `RUN_TEMP_SHARED_ALLOWLIST`). Layer 2. --- @@ -185,6 +200,9 @@ On failure, the script lists each non-conforming file with a specific reason. Th - **`shared/rules/sap_session_broker.md`** — the broker's CLI contract (`acquire` / `release` / `discover` / `gc` / `list`), v2 multi-connection registry schema, cleanup architecture. Read this when you need to coordinate session ownership across parallel agents. - **`shared/scripts/sap_attach_lib.vbs`** — the helper itself; well-commented. - **`shared/scripts/sap_session_broker.ps1`** + **`shared/scripts/sap_session_broker_com.vbs`** — broker implementation. +- **`shared/scripts/sap_dev_default.ps1`** + `Get-SapCurrentDevDefault` (`sap_connection_lib.ps1`) + `Set-SapUserSetting -Scope Session` (`sap_settings_lib.ps1`) — layer 3, per-session dev defaults. +- **`scripts/run-temp-hook.mjs`** — runtime `PreToolUse` enforcer for layer 2 (mirrors `RUN_TEMP_SHARED_ALLOWLIST` from `check-consistency.mjs` as `SHARED_ALLOWLIST`). +- **CLAUDE.md** — "Work Directory Configuration" (two-bucket temp), "Transport Request Settings" (two-layer dev defaults), "Logging Settings" (per-session log patterns): authoritative per-key detail for layers 2–4. --- @@ -290,6 +308,107 @@ Every PS skill wrapper that calls the broker MUST: --- +## Beyond session attach — the other three isolation layers + +Session attach (layer 1, everything above) decides *which GUI session a VBS +drives*. Three more layers, added **2026-06-20** after concurrent same-spec +builds clobbered each other, isolate the *build artifacts* around it. Full +per-key detail lives in CLAUDE.md; this is the parallel-safety summary. + +### Layer 2 — Two-bucket temp model + +Route every temp file by **scope, not transience** ("it's transient → `{RUN_TEMP}`" +is the trap — some scratch is genuinely cross-session and must stay shared): + +- **Bucket A — cross-session coordination → stable shared path** in + `{work_dir}\runtime\`: files a *different* session must find by a *predictable* + path — `session_registry.json` (broker), `connections.json`, + `session_dev_defaults.json`, the AI-session pins. **Allowlisted.** +- **Bucket B — per-run private scratch → `{RUN_TEMP}`** (`{work_dir}\temp\run_`, + minted by `Get-SapRunTemp` in Step 0): a skill's generated `*_run.vbs`/`.ps1`, + asXML payloads, `_run.json`, clipboard/title temp files, input files — **and any + ad-hoc orchestrator/agent probe or verify script.** + +Decision rule: *will another session read this exact file by a predictable path?* +Yes → Bucket A; no → Bucket B. Writing a **fixed-named** file into the +`{WORK_TEMP}` root (or the repo root) is the smell that caused the 2026-06-20 +cross-session `sap_se38_update_run.vbs` collision — two concurrent v74 builds +clobbered each other's generated VBS between write and `cscript` exec. + +Enforced **twice**, because the static checker can't see the running *cache* copy +of a skill or ad-hoc scratch: +- `scripts/check-consistency.mjs` — static (repo SKILL.md): hard error on + `{RUN_TEMP}` passed to `Get-SapCurrentSessionPath -WorkTemp`; warning on + fixed-named scratch outside `{RUN_TEMP}`. Shared basenames in + `RUN_TEMP_SHARED_ALLOWLIST`. +- `scripts/run-temp-hook.mjs` — runtime `PreToolUse` hook (registered per-developer + in the gitignored `.claude/settings.local.json`, so it's absent from a fresh + checkout; modes `block` (default) / `warn` / `off` via `SAPDEV_RUNTEMP_HOOK`, + and it fails **open**): catches the live tool call, including cache-lagged + skills and **agent/orchestrator scratch**. Blocks a `Write`/`Edit` of a + generated `.vbs`/`.ps1` into the `{WORK_TEMP}` root; mirrors the Bucket-A + basenames in `SHARED_ALLOWLIST`. + +> This applies to **agents and ad-hoc orchestration**, not just skills. A probe / +> verify / generator script you write on the fly goes to `{RUN_TEMP}`, never a +> fixed name in `{WORK_TEMP}` or the repo root. + +### Layer 3 — Two-layer dev defaults (per-(AI-session × connection)) + +The per-connection dev keys (`sap_dev_transport_request`, `sap_dev_package`, +`sap_dev_function_group`, `sap_dev_mode`, `way_to_get_transport_request`, +`rule_of_tr_description`, `tr_description_template`) now resolve through **two** +layers, highest first: + +1. **Session** — `{work_dir}\runtime\session_dev_defaults.json`, keyed per + `(AI-session × connection)`. A *task's* TR/package lives here, so two + conversations on the **same** connection stop clobbering each other (the + 2026-06-20 `069 → 074 → 075` thrash). Keyed on the connection too, so a + `/sap-login --switch` can't carry an `S4DK…` TR onto S4H. +2. **Connection** — `connections.json[].dev_defaults`: the developer's + **standing** default for that system (the Phase 4.4 layer). + +Then the global settings file. + +- **Writers:** a *task* TR/package → **Session** scope, which is now the + **default** — `Set-SapUserSetting … -Scope Session` or the CLI + `shared/scripts/sap_dev_default.ps1`. A deliberate **standing** default + (onboarding via `/sap-dev-init` / `/sap-login`) must pass `-Scope Connection` + explicitly. **Never hand-edit `connections.json` for a task default** — that is + the cross-conversation clobber this layer exists to prevent. +- **Readers:** go through `Get-SapCurrentDevDefault` (`sap_connection_lib.ps1`) + for the layered resolution. Session entries are age-pruned (7 days). + +### Layer 4 — Per-session log files + +Set `log_file_pattern = sap-dev-{YYYYMMDD}-{SID}-{CLIENT}-{AI_SESSION}.log` so +parallel builds get one coherent file per *(AI session × SAP connection)* instead +of interleaving into the daily file; each JSONL record still carries `run_id` / +`skill` for in-file drill-down. Gotchas: `{SYSTEM}` is the Windows +`%COMPUTERNAME%` (workstation), **not** the SAP SID — it does NOT separate +parallel builds on one machine; use `{SID}` / `{CLIENT}` (pinned connection) + +`{AI_SESSION}`. `{AI_SESSION}` resolves `CLAUDE_CODE_SESSION_ID` (stable across a +Claude host restart) → `SAPDEV_AI_SESSION_ID` (override) → `Get-SapAiSessionId` +(parent-PID fallback, **drifts** on host restart). + +### The layers at a glance + +| Layer | Isolates | Mechanism | Keyed by | +|---|---|---|---| +| 1 — session attach | which GUI session a VBS drives | `AttachSapSession` + broker pin | AI-session → connection_id | +| 2 — temp buckets | generated scratch files | `{RUN_TEMP}` + Bucket-A allowlist | run_id | +| 3 — dev defaults | task TR / package | `session_dev_defaults.json` | AI-session × connection | +| 4 — logs | forensic log streams | `{AI_SESSION}`/`{SID}`/`{CLIENT}` pattern | AI-session × connection | + +**Known gap — the customer brief is still a single shared file.** +`{custom_url}\customer_brief.md` is NOT auto-isolated. Keep it +**connection-agnostic** so parallel builds in different logon languages don't +contend: leave *Comments language* **blank** (each build then inherits its own +connection's logon language) and let per-build specifics (program / package / +message class) come from the spec, not the brief. + +--- + ## History - **Pre-Tier-3** (legacy): every operational VBS had its own attach loop. Parallel runs and multi-connection users both broken. @@ -301,3 +420,6 @@ Every PS skill wrapper that calls the broker MUST: - **Phase 4** (2026-05-16): multi-profile connection store (`connections.json`), AI-session pin, broker pin enforcement, RFC load-balanced login, stuck-screen tracking, `was_created` close-semantics. - **Phase 4.1** (2026-05-16): AI-session id derived from parent-process tree (Get-SapAiSessionId in sap_connection_lib.ps1) so parallel Claude Code conversations get distinct ids and subagents inherit. Broker auto-resolves `-AiSessionId` — wrappers no longer need to bootstrap it. - **Phase 4.2** (2026-05-16): `sap_active_session.json` pin file eliminated. Version fields moved into the connection profile in `connections.json`. Session path resolved live via `Get-SapCurrentSessionPath` (reads broker registry). 25+ deploy SKILL.md wrappers migrated from `SAPDEV_PIN_FILE` to `SAPDEV_SESSION_PATH`. `sap_attach_lib.vbs` Strategy 3 removed. +- **Layer 2 — two-bucket temp model** (2026-06-20): per-run scratch isolation via `{RUN_TEMP}` + a Bucket-A shared allowlist for `{work_dir}\runtime\` coordination files; dual enforcement — `check-consistency.mjs` (static) + `run-temp-hook.mjs` (runtime `PreToolUse`). Motivated by the cross-session `sap_se38_update_run.vbs` collision between two concurrent v74 builds. +- **Layer 3 — two-layer dev defaults** (2026-06-20): `{work_dir}\runtime\session_dev_defaults.json` keyed per (AI-session × connection); `Set-SapUserSetting -Scope Session` (now the writers' default) + `sap_dev_default.ps1`; reads centralized through `Get-SapCurrentDevDefault`. Fixes the same-connection `069 → 074 → 075` default thrash and stops a `--switch` carrying a TR across systems. +- **Layer 4 — per-session log files** (2026-06-20): `{AI_SESSION}` / `{SID}` / `{CLIENT}` `log_file_pattern` placeholders, so parallel builds get one log per (AI session × connection) instead of interleaving the daily file. From ed5113e359e6a36769796d7e3ab61f9ea81e7668 Mon Sep 17 00:00:00 2001 From: HKX BDDEV001 Date: Sat, 20 Jun 2026 16:27:24 +0900 Subject: [PATCH 9/9] chore: bump version to 0.6.6 Bump all manifests (marketplace top-level + metadata + 4 plugin entries, 4 plugin.json, package.json) to 0.6.6, aligned per check-consistency.mjs. Add CHANGELOG [0.6.6] documenting the 2026-06-20 parallel-safety layers 2-4 (two-bucket temp model, two-layer per-session dev defaults, per-session log files) and the parallel_safe_session_attach.md four-layer consolidation. Update CLAUDE.md pointer (CI gate seven conditions, four-layer scope) and README badge. Co-Authored-By: Claude Opus 4.8 --- .claude-plugin/marketplace.json | 14 +++---- CHANGELOG.md | 40 +++++++++++++++++++ CLAUDE.md | 2 +- README.md | 2 +- contributing/parallel_safe_session_attach.md | 2 +- package.json | 2 +- .../sap-dev-core/.claude-plugin/plugin.json | 2 +- .../sap-gen-code/.claude-plugin/plugin.json | 2 +- .../sap-migrate/.claude-plugin/plugin.json | 2 +- plugins/sap-tcd/.claude-plugin/plugin.json | 2 +- 10 files changed, 55 insertions(+), 15 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 9543268..3e77d2b 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,6 +1,6 @@ { "name": "sap-dev", - "version": "0.6.5", + "version": "0.6.6", "description": "SAP GUI automation plugins for AI coding assistants", "repository": "https://github.com/sapdev-ai/sap-dev", "owner": { @@ -9,8 +9,8 @@ "url": "https://sapdev.ai" }, "metadata": { - "version": "0.6.5", - "last_updated": "2026-06-17", + "version": "0.6.6", + "last_updated": "2026-06-20", "total_plugins": 4, "total_skills": 78, "categories": [ @@ -83,7 +83,7 @@ "agents": [ "./agents/abap-developer.md" ], - "version": "0.6.5", + "version": "0.6.6", "category": "tooling", "keywords": [ "abap-developer-agent", @@ -149,7 +149,7 @@ "./skills/sap-gen-abap-unit", "./skills/sap-review-abap" ], - "version": "0.6.5", + "version": "0.6.6", "category": "tooling", "keywords": [ "abap", @@ -181,7 +181,7 @@ "./skills/sap-mm01", "./skills/sap-va01" ], - "version": "0.6.5", + "version": "0.6.6", "category": "tooling", "keywords": [ "bp", @@ -212,7 +212,7 @@ "agents": [ "./agents/cc-migration-engineer.md" ], - "version": "0.6.5", + "version": "0.6.6", "category": "tooling", "keywords": [ "abap", diff --git a/CHANGELOG.md b/CHANGELOG.md index 8688118..fbb0707 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,46 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [0.6.6] — 2026-06-20 + +### Added + +- **Parallel multi-session build safety — three isolation layers on top of the + session-attach contract (layer 1),** after concurrent same-spec builds across + several SAP connections surfaced cross-session clobbering: + - **Layer 2 — two-bucket temp model.** Per-run scratch is isolated to + `{RUN_TEMP}` (`{work_dir}\temp\run_`); only allowlisted coordination + files (`session_registry.json`, `connections.json`, + `session_dev_defaults.json`, AI-session pins) live at the shared + `{work_dir}\runtime\` path. Enforced statically by `scripts/check-consistency.mjs` + (hard error on `{RUN_TEMP}` passed to `Get-SapCurrentSessionPath -WorkTemp`; + warning on fixed-named scratch under the `{WORK_TEMP}` root) and at runtime by + `scripts/run-temp-hook.mjs` (a `PreToolUse` hook; modes `block`/`warn`/`off` + via `SAPDEV_RUNTEMP_HOOK`, fails open). Motivated by a cross-session + `sap_se38_update_run.vbs` collision between two concurrent builds. + - **Layer 3 — two-layer dev defaults.** The per-connection dev keys + (`sap_dev_transport_request`, `sap_dev_package`, `sap_dev_function_group`, + `sap_dev_mode`, `way_to_get_transport_request`, `rule_of_tr_description`, + `tr_description_template`) now resolve Session → Connection → global, with a + per-`(AI-session × connection)` Session layer at + `{work_dir}\runtime\session_dev_defaults.json` (`Set-SapUserSetting -Scope + Session`, now the writers' default, + the `shared/scripts/sap_dev_default.ps1` + CLI; reads centralized through `Get-SapCurrentDevDefault`). Fixes + same-connection default thrash and stops `/sap-login --switch` carrying a TR + across systems. Session entries are age-pruned (7 days). + - **Layer 4 — per-session log files.** New `log_file_pattern` placeholders + `{AI_SESSION}` / `{SID}` / `{CLIENT}` give one coherent log per + `(AI session × connection)` instead of interleaving the daily file. + +### Changed + +- **`contributing/parallel_safe_session_attach.md` extended from a + session-attach-only contract into the full four-layer parallel-safety + reference** (layers 2–4 section, an at-a-glance table, and a "known gap" note + that the customer brief is still a single shared file — keep it + connection-agnostic, comment-language blank). CLAUDE.md's pointer row updated to + match (CI gate now "seven conditions", four-layer scope, extended history tail). + ## [0.6.5] — 2026-06-17 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index 5d99e83..7709c32 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ Repository: sap-dev Purpose: SAP GUI automation plugins for AI coding assistants -Version: 0.6.5 | Plugins: 4 | Last Updated: 2026-06-17 +Version: 0.6.6 | Plugins: 4 | Last Updated: 2026-06-20 ## What This Repository Is diff --git a/README.md b/README.md index 69bfa3d..4e6ea0e 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ SAP development automation skills for AI coding assistants. Project home: -## Available Plugins (4 plugins · 78 skills · 2 agents · v0.6.5) +## Available Plugins (4 plugins · 78 skills · 2 agents · v0.6.6) | Plugin | Skills | Description | |--------|--------|-------------| diff --git a/contributing/parallel_safe_session_attach.md b/contributing/parallel_safe_session_attach.md index e22378f..50bdd58 100644 --- a/contributing/parallel_safe_session_attach.md +++ b/contributing/parallel_safe_session_attach.md @@ -180,7 +180,7 @@ node scripts/check-consistency.mjs Should print (counts grow as the repo grows — the shape is what matters): ``` -OK: 4 plugins, 78 skills, all manifests aligned at version 0.6.5, Tier 3 attach contract clean, 10 non-ASCII warning(s), 30 run-temp warning(s), screen-baseline coverage 7/119 (112 unbaselined) +OK: 4 plugins, 78 skills, all manifests aligned at version 0.6.6, Tier 3 attach contract clean, 10 non-ASCII warning(s), 30 run-temp warning(s), screen-baseline coverage 7/119 (112 unbaselined) ``` On failure, the script lists each non-conforming file with a specific reason. The check covers: diff --git a/package.json b/package.json index cb70e03..cd362ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sap-dev-validation", - "version": "0.6.5", + "version": "0.6.6", "private": true, "scripts": { "validate": "./scripts/validate-json-schemas.sh", diff --git a/plugins/sap-dev-core/.claude-plugin/plugin.json b/plugins/sap-dev-core/.claude-plugin/plugin.json index d3a18cd..75a91e2 100644 --- a/plugins/sap-dev-core/.claude-plugin/plugin.json +++ b/plugins/sap-dev-core/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "sap-dev-core", - "version": "0.6.5", + "version": "0.6.6", "description": "Core SAP development utilities and ABAP Workbench automation: login, connection management, credential storage, BDC execution, add-on table maintenance, and SAP GUI Scripting for SE38, SE37, SE24, SE11, SE19, SE41, SE51, SE54, SE91, CMOD, and GUI recording. Also provides shared resources (shared/tables/, shared/scripts/) used by all other sap-dev plugins. Install this plugin first.", "author": { "name": "KexiHe", diff --git a/plugins/sap-gen-code/.claude-plugin/plugin.json b/plugins/sap-gen-code/.claude-plugin/plugin.json index 3e5fbda..6b1570a 100644 --- a/plugins/sap-gen-code/.claude-plugin/plugin.json +++ b/plugins/sap-gen-code/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "sap-gen-code", - "version": "0.6.5", + "version": "0.6.6", "description": "ABAP code generation pipeline: convert design docs, extract structured data, validate process/DDIC, generate ABAP, check quality, and auto-fix issues", "author": { "name": "KexiHe", diff --git a/plugins/sap-migrate/.claude-plugin/plugin.json b/plugins/sap-migrate/.claude-plugin/plugin.json index 4844f0b..9c758d7 100644 --- a/plugins/sap-migrate/.claude-plugin/plugin.json +++ b/plugins/sap-migrate/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "sap-migrate", - "version": "0.6.5", + "version": "0.6.6", "description": "S/4HANA custom-code migration engine: run a brownfield conversion as a tracked campaign — inventory custom (Z/Y) objects, flag unused code for decommission, run the S/4HANA-readiness ATC, triage findings, and remediate mechanical changes at scale on a sandbox. Companion to sap-dev-core (install that first).", "author": { "name": "KexiHe", diff --git a/plugins/sap-tcd/.claude-plugin/plugin.json b/plugins/sap-tcd/.claude-plugin/plugin.json index b6ede8f..1c5bfbe 100644 --- a/plugins/sap-tcd/.claude-plugin/plugin.json +++ b/plugins/sap-tcd/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "sap-tcd", - "version": "0.6.5", + "version": "0.6.6", "description": "SAP transaction automation for business processes: Business Partner (BP), Material Master (MM01), and Sales Order (VA01) management", "author": { "name": "KexiHe",