From dbfc27dfad84c0de697e32d74f54e69397062c57 Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 19 May 2026 10:47:09 -0400 Subject: [PATCH] gsd snapshot: uncommitted changes after 177m inactivity --- .gitignore | 31 + .gsd/STATE.md | 18 - .gsd/event-log.jsonl | 3 - .gsd/graphs/graph.json | 65 +- .gsd/milestones/M001/M001-ROADMAP.md | 6 +- .gsd/milestones/M001/slices/S01/S01-PLAN.md | 36 +- .gsd/milestones/M001/slices/S02/S02-PLAN.md | 60 +- .gsd/state-manifest.json | 702 ------------------ .../src/Utils/GDevelopServices/Generation.js | 20 +- newIDE/electron-app/app/main.js | 4 + newIDE/electron-app/app/package-lock.json | 4 +- 11 files changed, 170 insertions(+), 779 deletions(-) delete mode 100644 .gsd/STATE.md delete mode 100644 .gsd/event-log.jsonl delete mode 100644 .gsd/state-manifest.json diff --git a/.gitignore b/.gitignore index 5ac5441044..255d4bd33d 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,34 @@ .Trashes Thumbs.db .claude + +# ── GSD baseline (auto-generated) ── +.gsd +.gsd-id +.mcp.json +.bg-shell/ +*.swp +*.swo +*~ +.idea/ +.vscode/ +*.code-workspace +.env +.env.* +!.env.example +node_modules/ +.next/ +dist/ +build/ +__pycache__/ +*.pyc +.venv/ +venv/ +target/ +vendor/ +*.log +coverage/ +.cache/ +tmp/ +.gsd.migrating +.gsds \ No newline at end of file diff --git a/.gsd/STATE.md b/.gsd/STATE.md deleted file mode 100644 index c8e6cc5655..0000000000 --- a/.gsd/STATE.md +++ /dev/null @@ -1,18 +0,0 @@ -# GSD State - -**Active Milestone:** M001: BYOK Fork -**Active Slice:** S02: S02 -**Phase:** executing -**Requirements Status:** 7 active · 3 validated · 0 deferred · 0 out of scope - -## Milestone Registry -- 🔄 **M001:** BYOK Fork - -## Recent Decisions -- None recorded - -## Blockers -- None - -## Next Action -Execute T04: Create preload.js, byokMain.js, and edit main.js for IPC wiring in slice S02. diff --git a/.gsd/event-log.jsonl b/.gsd/event-log.jsonl deleted file mode 100644 index 553bb80251..0000000000 --- a/.gsd/event-log.jsonl +++ /dev/null @@ -1,3 +0,0 @@ -{"v":2,"cmd":"complete-task","params":{"milestoneId":"M001","sliceId":"S02","taskId":"T01"},"ts":"2026-05-19T01:37:41.480Z","actor":"agent","hash":"cdfb261a3cee2da1","session_id":"01112a50-1cb7-4b0d-9860-d6b7f62c3c6c"} -{"v":2,"cmd":"complete-task","params":{"milestoneId":"M001","sliceId":"S02","taskId":"T02"},"ts":"2026-05-19T01:45:38.385Z","actor":"agent","hash":"373b19e76678397e","session_id":"01112a50-1cb7-4b0d-9860-d6b7f62c3c6c"} -{"v":2,"cmd":"complete-task","params":{"milestoneId":"M001","sliceId":"S02","taskId":"T03"},"ts":"2026-05-19T01:58:02.784Z","actor":"agent","actor_name":"executor-01","trigger_reason":"auto-mode task execution","hash":"00412cfd0b09e3c4","session_id":"01112a50-1cb7-4b0d-9860-d6b7f62c3c6c"} diff --git a/.gsd/graphs/graph.json b/.gsd/graphs/graph.json index b2dbb59edc..93a512d6f9 100644 --- a/.gsd/graphs/graph.json +++ b/.gsd/graphs/graph.json @@ -42,8 +42,39 @@ }, { "id": "slice:M001:S02", - "label": "M001/S02", + "label": "S02: IPC Infrastructure", "type": "slice", + "confidence": "EXTRACTED", + "sourceFile": "milestones/M001/slices/S02/S02-PLAN.md" + }, + { + "id": "task:M001:S02:T01", + "label": "T01: Create errors.js and buildSystemPrompt.js modules with tests", + "type": "task", + "confidence": "EXTRACTED" + }, + { + "id": "task:M001:S02:T02", + "label": "T02: Create byokConfig.js and requestStore.js modules with tests", + "type": "task", + "confidence": "EXTRACTED" + }, + { + "id": "task:M001:S02:T03", + "label": "T03: Create callLLM.js — core OpenAI-compatible LLM caller", + "type": "task", + "confidence": "EXTRACTED" + }, + { + "id": "task:M001:S02:T04", + "label": "T04: Create preload.js, byokMain.js, and edit main.js for IPC wiring", + "type": "task", + "confidence": "EXTRACTED" + }, + { + "id": "task:M001:S02:T05", + "label": "T05: Verify IPC round-trip works end-to-end", + "type": "task", "confidence": "EXTRACTED" } ], @@ -77,7 +108,37 @@ "to": "slice:M001:S02", "type": "contains", "confidence": "EXTRACTED" + }, + { + "from": "slice:M001:S02", + "to": "task:M001:S02:T01", + "type": "contains", + "confidence": "EXTRACTED" + }, + { + "from": "slice:M001:S02", + "to": "task:M001:S02:T02", + "type": "contains", + "confidence": "EXTRACTED" + }, + { + "from": "slice:M001:S02", + "to": "task:M001:S02:T03", + "type": "contains", + "confidence": "EXTRACTED" + }, + { + "from": "slice:M001:S02", + "to": "task:M001:S02:T04", + "type": "contains", + "confidence": "EXTRACTED" + }, + { + "from": "slice:M001:S02", + "to": "task:M001:S02:T05", + "type": "contains", + "confidence": "EXTRACTED" } ], - "builtAt": "2026-05-19T01:02:00.643Z" + "builtAt": "2026-05-19T12:00:05.446Z" } \ No newline at end of file diff --git a/.gsd/milestones/M001/M001-ROADMAP.md b/.gsd/milestones/M001/M001-ROADMAP.md index 607de511cc..74f3de3aba 100644 --- a/.gsd/milestones/M001/M001-ROADMAP.md +++ b/.gsd/milestones/M001/M001-ROADMAP.md @@ -15,13 +15,13 @@ ## Slices -- [ ] **S01: Core Premium Unlock** `risk:low` `depends:[]` +- [x] **S01: S01** `risk:low` `depends:[]` > After this: IDE launches with subscription valid, max limits, no watermark -- [ ] **S02: IPC Infrastructure** `risk:medium` `depends:[]` +- [x] **S02: S02** `risk:medium` `depends:[]` > After this: window.byokAi available in renderer, IPC handlers registered in main process -- [ ] **S03: AI Routing** `risk:medium` `depends:[S02]` +- [ ] **S03: S03** `risk:medium` `depends:[]` > After this: BYOK preset appears in dropdown, selecting it routes AI requests through local IPC - [ ] **S04: Config UI** `risk:low` `depends:[S02,S03]` diff --git a/.gsd/milestones/M001/slices/S01/S01-PLAN.md b/.gsd/milestones/M001/slices/S01/S01-PLAN.md index 0ab92e8967..58f7ed945c 100644 --- a/.gsd/milestones/M001/slices/S01/S01-PLAN.md +++ b/.gsd/milestones/M001/slices/S01/S01-PLAN.md @@ -1,11 +1,11 @@ # S01: Core Premium Unlock -**Goal:** IDE launches with subscription valid, max limits, no watermark +**Goal:** The IDE launches with subscription valid, max limits, no watermark — all gating functions bypassed at source. **Demo:** IDE launches with subscription valid, max limits, no watermark ## Must-Haves -- R001: hasValidSubscriptionPlan() always returns true. R002: getUserLimits() returns hardcoded max-limits without API call. R003: Watermark constructor and deserialization default to showWatermark: false. +- [ ] GDevelop launches without errors\n- [ ] No subscription gate or upsell visible\n- [ ] No Made with GDevelop watermark on preview\n- [ ] `hasValidSubscriptionPlan()` returns `true` regardless of subscription\n- [ ] `getUserLimits()` returns premium capabilities without API call\n- [ ] Watermark.cpp defaults to `showWatermark=false` ## Proof Level @@ -13,30 +13,30 @@ ## Integration Closure -Three surgical edits at definition sites. All downstream callers pick up changes automatically. T01 touches C++ constructor+serialization defaults. T02+T03 touch the same Usage.js file. No new wiring needed — these are pure bypass edits at the source of truth. +This slice is self-contained — it modifies the subscription/limits/watermark source functions. Downstream callers pick up the changes transparently. ## Verification -- None — these are compile-time/default changes. No runtime signals added. Verification is via grep on the edited files and existing test suite pass. +- none — simple function-level bypasses with no async state or runtime signals ## Tasks -- [ ] **T01: Disable watermark in Watermark.cpp defaults** `est:10m` - Why: R003 requires the Made with GDevelop watermark to be disabled by default. The Watermark.cpp constructor initializes showWatermark to true, and UnserializeFrom falls back to true when the attribute is missing. +- [x] **T01: Bypass hasValidSubscriptionPlan to always return true** `est:15m` + Modify `hasValidSubscriptionPlan()` in `Usage.js` to always return `true` regardless of the subscription argument. This is the single choke point used by all premium gating callers (SubscriptionChecker, AskAiEditorContainer, etc.). + - Files: `newIDE/app/src/Utils/GDevelopServices/Usage.js` + - Verify: grep -q 'return true' newIDE/app/src/Utils/GDevelopServices/Usage.js && node -e "require('fs').readFileSync('newIDE/app/src/Utils/GDevelopServices/Usage.js','utf8').includes('hasValidSubscriptionPlan') && console.log('T01: hasValidSubscriptionPlan modified') || process.exit(1)" + +- [x] **T02: Bypass getUserLimits to return max capabilities without API call** `est:30m` + Modify `getUserLimits()` in `Usage.js` to skip the API call and return a static max-limits object. The function should ignore its parameters and return: a `Limits` object with `quotas` having maxed-out values (limitReached: false, current: 0, max: 999999), `capabilities` set to premium defaults (all booleans true, max counts at 999999, themeCustomization FULL, etc.), and a `credits` object with typical premium values. + - Files: `newIDE/app/src/Utils/GDevelopServices/Usage.js` + - Verify: grep -q '999999' newIDE/app/src/Utils/GDevelopServices/Usage.js && node -e "const u=require('fs').readFileSync('newIDE/app/src/Utils/GDevelopServices/Usage.js','utf8'); u.includes('getUserLimits') && !u.includes('apiClient.get') && console.log('T02: getUserLimits bypassed') || process.exit(1)" + +- [x] **T03: Disable watermark default in Watermark.cpp** `est:10m` + Change the default value of `showWatermark` from `true` to `false` in `Watermark.cpp`'s constructor initialization list. This prevents the GDevelop watermark from appearing on preview exports. - Files: `Core/GDCore/Project/Watermark.cpp` - - Verify: grep -q "showWatermark(false)" Core/GDCore/Project/Watermark.cpp - -- [ ] **T02: Bypass hasValidSubscriptionPlan to always return true** `est:10m` - Why: R001 requires hasValidSubscriptionPlan() to return true unconditionally, removing all subscription gating. Called by ~15 consumers across the IDE. - - Files: `newIDE/app/src/Utils/GDevelopServices/Usage.js` - - Verify: grep -q "return true" newIDE/app/src/Utils/GDevelopServices/Usage.js - -- [ ] **T03: Bypass getUserLimits with hardcoded max-limits object** `est:15m` - Why: R002 requires getUserLimits() to return maximum values for all limits without making an API call. Removes all caps on builds, projects, AI tokens, etc. Must return a valid Limits Flow type. - - Files: `newIDE/app/src/Utils/GDevelopServices/Usage.js` - - Verify: grep -q "maximumCount: 999" newIDE/app/src/Utils/GDevelopServices/Usage.js + - Verify: grep -q 'showWatermark(false)' Core/GDCore/Project/Watermark.cpp && echo 'T03: watermark disabled' ## Files Likely Touched -- Core/GDCore/Project/Watermark.cpp - newIDE/app/src/Utils/GDevelopServices/Usage.js +- Core/GDCore/Project/Watermark.cpp diff --git a/.gsd/milestones/M001/slices/S02/S02-PLAN.md b/.gsd/milestones/M001/slices/S02/S02-PLAN.md index 758c1932d0..8ae996651d 100644 --- a/.gsd/milestones/M001/slices/S02/S02-PLAN.md +++ b/.gsd/milestones/M001/slices/S02/S02-PLAN.md @@ -1,62 +1,62 @@ # S02: IPC Infrastructure -**Goal:** Establish the Electron IPC bridge and main-process BYOK proxy modules that enable AI routing through user-owned LLM keys. After this slice, window.byokAi is available in the renderer via contextBridge and all IPC handlers are registered in the main process. +**Goal:** window.byokAi available in renderer, IPC handlers registered in main process **Demo:** window.byokAi available in renderer, IPC handlers registered in main process ## Must-Haves -- All 5 proxy modules (errors.js, byokConfig.js, requestStore.js, buildSystemPrompt.js, callLLM.js) pass node --check. byokMain.js and preload.js pass node --check. main.js has preload path in webPreferences and registerByokHandlers(ipcMain) called inside app.on('ready'). Unit tests for pure JS modules pass with node --test. +- All proxy modules load without syntax errors. node --test passes for all test-t0X.cjs files. main.js has preload path, byok require, and registerByokHandlers call. ## Proof Level -- This slice proves: contract +- This slice proves: Integration complete: preload bridge → IPC handler → LLM call works end-to-end in a running Electron instance ## Integration Closure -This slice wires 5 new modules into the Electron main process via byokMain.js and preload.js. Upstream surfaces consumed: main.js (preload path in webPreferences, IPC handler registration inside app.on('ready')). Downstream: S03 (AI Routing) will consume window.byokAi from the renderer side. S04 (Config UI) and S05 (Integration) depend on the IPC channels established here. +main.js has all three edits applied, preload.js is syntactically valid, byokMain.js loads and exports registerByokHandlers, all proxy modules load without errors ## Verification -- byokMain.js registers structured IPC channels with error forwarding to the renderer. requestStore.js exposes getRequestStatus() for inspecting in-flight requests. All errors are typed ByokError instances serializable over IPC via toJSON(). +- Structured ByokError with typed codes, requestStore provides active request tracking via IPC ## Tasks -- [ ] **T01: Create errors.js and buildSystemPrompt.js modules** `est:20m` - Why: errors.js provides structured error handling for all BYOK modules (typed error codes: INVALID_KEY, ENDPOINT_UNREACHABLE, MODEL_NOT_FOUND, RATE_LIMITED, UNKNOWN). buildSystemPrompt.js assembles the system prompt for AI requests — both are foundational pure-JS modules with no Electron dependency. - - Files: `newIDE/electron-app/app/byok/errors.js`, `newIDE/electron-app/app/byok/buildSystemPrompt.js` - - Verify: node --check newIDE/electron-app/app/byok/errors.js && node --check newIDE/electron-app/app/byok/buildSystemPrompt.js +- [x] **T01: Create errors.js and buildSystemPrompt.js modules with tests** `est:15m` + Create the byok/ directory at newIDE/electron-app/app/byok/. Create errors.js exporting a ByokError class extending Error with a static codes object. Create buildSystemPrompt.js exporting a buildSystemPrompt function accepting {context, language} options and returning an assembled prompt string. Create test-t01.cjs using node:assert and node:test to verify: (1) ByokError is an instance of Error, (2) ByokError stores code and message correctly, (3) All error codes exist with expected values, (4) buildSystemPrompt returns a string, (5) buildSystemPrompt includes context when provided. + - Files: `newIDE/electron-app/app/byok/errors.js`, `newIDE/electron-app/app/byok/buildSystemPrompt.js`, `newIDE/electron-app/app/byok/test-t01.cjs` + - Verify: node --test newIDE/electron-app/app/byok/test-t01.cjs -- [ ] **T02: Create byokConfig.js and requestStore.js modules** `est:25m` - Why: byokConfig.js persists BYOK configuration (provider, endpoint, key, model) to a JSON file at app.getPath('userData')/byok-config.json using fs-extra (already a dependency). requestStore.js manages in-flight AI requests with AbortController tracking for cancellation support. - - Files: `newIDE/electron-app/app/byok/byokConfig.js`, `newIDE/electron-app/app/byok/requestStore.js` - - Verify: node --check newIDE/electron-app/app/byok/requestStore.js && node --check newIDE/electron-app/app/byok/byokConfig.js +- [x] **T02: Create byokConfig.js and requestStore.js modules with tests** `est:20m` + Create byokConfig.js exporting readConfig(), writeConfig(config), getConfigPath(). readConfig returns parsed JSON or empty object on ENOENT. writeConfig writes JSON with 2-space indent. Uses fs-extra (already in package.json deps) and requires('electron') for app.getPath. byokConfig.js cannot be unit-tested outside Electron, so only syntax-verify it. + - Files: `newIDE/electron-app/app/byok/byokConfig.js`, `newIDE/electron-app/app/byok/requestStore.js`, `newIDE/electron-app/app/byok/test-t02.cjs` + - Verify: node --test newIDE/electron-app/app/byok/test-t02.cjs -- [ ] **T03: Create callLLM.js core module** `est:30m` - Why: callLLM.js is the core LLM API caller that translates IPC requests into OpenAI-compatible HTTP API calls. Uses native fetch (available in Electron 32 / Node 22) rather than axios to avoid dependency hoisting issues. Supports both streaming and non-streaming modes with configurable timeouts. - - Files: `newIDE/electron-app/app/byok/callLLM.js` - - Verify: node --check newIDE/electron-app/app/byok/callLLM.js +- [x] **T03: Create callLLM.js — core OpenAI-compatible LLM caller** `est:30m` + Create callLLM.js exporting callLLM({messages, requestId, signal}) and callLLMStream({messages, onChunk, requestId, signal}). + - Files: `newIDE/electron-app/app/byok/callLLM.js`, `newIDE/electron-app/app/byok/test-t03.cjs` + - Verify: node --test newIDE/electron-app/app/byok/test-t03.cjs -- [ ] **T04: Create byokMain.js and preload.js, wire into main.js** `est:30m` - Why: byokMain.js is the centralized IPC handler registration module that imports all 5 BYOK modules and registers IPC handlers. preload.js exposes window.byokAi via contextBridge. main.js needs 3 surgical edits to wire everything together. - - Files: `newIDE/electron-app/app/byok/byokMain.js`, `newIDE/electron-app/app/preload.js`, `newIDE/electron-app/app/main.js` - - Verify: node --check newIDE/electron-app/app/byok/byokMain.js && node --check newIDE/electron-app/app/preload.js +- [x] **T04: Create preload.js, byokMain.js, and edit main.js for IPC wiring** `est:30m` + Create preload.js exposing window.byokAi via contextBridge with {callLLM, getConfig, saveConfig, getActiveRequests, abortRequest}. + - Files: `newIDE/electron-app/app/preload.js`, `newIDE/electron-app/app/byok/byokMain.js`, `newIDE/electron-app/app/main.js`, `newIDE/electron-app/app/byok/test-t04.cjs` + - Verify: node --test newIDE/electron-app/app/byok/test-t04.cjs -- [ ] **T05: Write and run unit tests for pure JS modules** `est:25m` - Why: errors.js, buildSystemPrompt.js, requestStore.js, and callLLM.js are pure JavaScript with no Electron dependency and can be unit tested using Node 22's built-in node:test runner. Tests provide regression protection for downstream slices. - - Files: `newIDE/electron-app/app/byok/tests/errors.test.js`, `newIDE/electron-app/app/byok/tests/buildSystemPrompt.test.js`, `newIDE/electron-app/app/byok/tests/requestStore.test.js`, `newIDE/electron-app/app/byok/tests/callLLM.test.js` - - Verify: node --test newIDE/electron-app/app/byok/tests/ +- [x] **T05: Verify IPC round-trip works end-to-end** `est:15m` + Launch the Electron app and verify that window.byokAi is available in the renderer DevTools console. Test IPC round-trip by calling window.byokAi.getConfig() and verifying it returns an empty config (no crash). Verify that window.byokAi has all 5 expected methods: callLLM, getConfig, saveConfig, getActiveRequests, abortRequest. + - Files: `newIDE/electron-app/app/main.js`, `newIDE/electron-app/app/preload.js`, `newIDE/electron-app/app/byok/byokMain.js` + - Verify: Launch Electron app, check window.byokAi in DevTools ## Files Likely Touched - newIDE/electron-app/app/byok/errors.js - newIDE/electron-app/app/byok/buildSystemPrompt.js +- newIDE/electron-app/app/byok/test-t01.cjs - newIDE/electron-app/app/byok/byokConfig.js - newIDE/electron-app/app/byok/requestStore.js +- newIDE/electron-app/app/byok/test-t02.cjs - newIDE/electron-app/app/byok/callLLM.js -- newIDE/electron-app/app/byok/byokMain.js +- newIDE/electron-app/app/byok/test-t03.cjs - newIDE/electron-app/app/preload.js +- newIDE/electron-app/app/byok/byokMain.js - newIDE/electron-app/app/main.js -- newIDE/electron-app/app/byok/tests/errors.test.js -- newIDE/electron-app/app/byok/tests/buildSystemPrompt.test.js -- newIDE/electron-app/app/byok/tests/requestStore.test.js -- newIDE/electron-app/app/byok/tests/callLLM.test.js +- newIDE/electron-app/app/byok/test-t04.cjs diff --git a/.gsd/state-manifest.json b/.gsd/state-manifest.json deleted file mode 100644 index 0360356009..0000000000 --- a/.gsd/state-manifest.json +++ /dev/null @@ -1,702 +0,0 @@ -{ - "version": 1, - "exported_at": "2026-05-19T01:58:02.782Z", - "milestones": [ - { - "id": "M001", - "title": "BYOK Fork", - "status": "active", - "depends_on": [], - "created_at": "2026-05-18T23:23:03.706Z", - "completed_at": null, - "vision": "GDevelop IDE fully unlocked with BYOK AI routing through user-owned LLM keys, built directly into the source.", - "success_criteria": [ - "GDevelop launches without errors with all BYOK changes applied", - "No subscription gate or upsell visible", - "No Made with GDevelop watermark on preview", - "BYOK preset visible in AI configuration dropdown", - "Config panel renders with provider/endpoint/key/model fields when BYOK selected", - "Saving config persists across app restart", - "Chat request with BYOK preset calls configured LLM and returns response", - "Chat request with non-BYOK preset still works normally" - ], - "key_risks": [ - { - "risk": "addMessageToAiRequest has a complex call signature — the BYOK interceptor must return a valid AiRequest shape", - "whyItMatters": "Getting the return type wrong could break compilation or cause runtime errors in AI chat" - }, - { - "risk": "BrowserWindow creation in main.js is dense — finding the right spot for preload registration requires care", - "whyItMatters": "Wrong placement could break the app startup or leave window.byokAi undefined" - } - ], - "proof_strategy": [ - { - "riskOrUnknown": "addMessageToAiRequest return type compatibility", - "retireIn": "S03", - "whatWillBeProven": "Sending a chat message with BYOK preset returns a valid response without errors" - }, - { - "riskOrUnknown": "Preload registration in main.js", - "retireIn": "S02", - "whatWillBeProven": "window.byokAi is defined and callable from renderer devtools" - } - ], - "verification_contract": "Files exist with correct changes; exports match expected interfaces; new modules export as documented", - "verification_integration": "window.byokAi bridge works; IPC handler receives requests; LLM API returns real response", - "verification_operational": "Config persists across app restart; non-BYOK presets still functional", - "verification_uat": "Full manual walkthrough: launch, check watermark absent, select BYOK, configure provider, send chat, switch to default preset, verify it still works", - "definition_of_done": [ - "All source files created/modified as specified in slice plans", - "App builds and launches without errors", - "Each slice demo outcome verified at least once", - "Non-BYOK AI presets still functional (no regression)", - "Config persists across restart" - ], - "requirement_coverage": "Covers: R001, R002, R003, R004, R005, R006, R007, R008, R009, R010. All 10 active requirements mapped to slices.", - "boundary_map_markdown": "Not provided.", - "sequence": 0 - } - ], - "slices": [ - { - "milestone_id": "M001", - "id": "S01", - "title": "S01", - "status": "complete", - "risk": "low", - "depends": [], - "demo": "IDE launches with subscription valid, max limits, no watermark", - "created_at": "2026-05-18T23:25:51.561Z", - "completed_at": "2026-05-19T01:02:00.590Z", - "full_summary_md": "---\nid: S01\nparent: M001\nmilestone: M001\nprovides:\n - Fully unlocked IDE: subscription always valid, all limits maxed, no watermark — downstream slices (IPC, AI Routing, Config UI, Integration) build on this foundation\nrequires:\n []\naffects:\n - S02\n - S03\n - S04\n - S05\nkey_files:\n - newIDE/app/src/Utils/GDevelopServices/Usage.js\n - Core/GDCore/Project/Watermark.cpp\nkey_decisions:\n - Bypass hasValidSubscriptionPlan at source — single choke point for all premium gates (SubscriptionChecker, AskAiEditorContainer, etc.)\n - Bypass getUserLimits at source — static max Limits object eliminates the API call entirely, unlocking all premium features without network dependency\n - Default showWatermark to false at both constructor and deserialization fallback — double safeguard ensures watermark never appears unless explicitly enabled\npatterns_established:\n - Source-level choke-point bypass: modify one function used by all downstream callers rather than patching each call site\n - Double-default pattern: set the safe default in both constructor and deserialization fallback to prevent edge-case regressions\nobservability_surfaces:\n - none — simple function-level bypasses with no runtime state\ndrill_down_paths:\n - .gsd/milestones/M001/slices/S01/tasks/T01-SUMMARY.md\n - .gsd/milestones/M001/slices/S01/tasks/T02-SUMMARY.md\n - .gsd/milestones/M001/slices/S01/tasks/T03-SUMMARY.md\nduration: \"\"\nverification_result: passed\ncompleted_at: 2026-05-19T01:02:00.591Z\nblocker_discovered: false\n---\n\n# S01: Core Premium Unlock\n\n**Bypassed hasValidSubscriptionPlan, getUserLimits, and Watermark at source — IDE now treats every user as fully licensed with no watermark, no API calls, and max capabilities.**\n\n## What Happened\n\nThree source-level bypasses were applied to the GDevelop core, each targeting a single choke point so all downstream callers pick up the changes transparently.\n\n**T01 — Subscription gate:** `hasValidSubscriptionPlan()` in `Usage.js` was modified to unconditionally return `true`. This is the single function called by all premium gating code (SubscriptionChecker, AskAiEditorContainer, etc.), so every subscription-dependent path now sees the user as licensed without any network call.\n\n**T02 — Limits ceiling:** `getUserLimits()` in `Usage.js` was replaced with a static `Limits` object that returns maximum values for every quota (limitReached: false, current: 0, max: 999999), all capabilities enabled (analytics, cloudProjects, leaderboards, multiplayer, versionHistory, ai), themeCustomization set to FULL, and premium credit balance. The API call was removed entirely — the function ignores its parameters and returns the static object.\n\n**T03 — Watermark removal:** `Watermark.cpp`'s constructor initialization list changed `showWatermark(true)` to `showWatermark(false)`, and the `UnserializeFrom` fallback default changed from `true` to `false`. This double safeguard ensures the \"Made with GDevelop\" watermark never appears on new projects or deserialized projects unless explicitly enabled in project data.\n\nAll three changes are self-contained function-level edits with no new dependencies, no async state, and no runtime signals to monitor.\n\n## Verification\n\nRe-verified all three tasks with node.js (grep unavailable on Windows PATH — prior exit code 127 was a tooling false negative, not a code failure):\n\n1. **T01:** Confirmed `return true` present in `Usage.js` and `hasValidSubscriptionPlan` function intact.\n2. **T02:** Confirmed `getUserLimits` function exists, `999999` max values present, no `apiClient.get` call remains in function body, all required capability keys present (analytics, cloudProjects, leaderboards, multiplayer, versionHistory, ai).\n3. **T03:** Confirmed `showWatermark(false)` in constructor initialization and `GetBoolAttribute(\"showWatermark\", false)` in UnserializeFrom fallback.\n\n## Requirements Advanced\n\n- R001 — hasValidSubscriptionPlan() now unconditionally returns true, bypassing all subscription validation\n- R002 — getUserLimits() now returns a static maxed-out Limits object without any API call\n- R003 — Watermark constructor and UnserializeFrom default both changed from true to false\n\n## Requirements Validated\n\n- R001 — node.js verification confirmed return true present in hasValidSubscriptionPlan function body\n- R002 — node.js verification confirmed 999999 max values present, no apiClient.get call in getUserLimits function body\n- R003 — node.js verification confirmed showWatermark(false) in constructor and GetBoolAttribute default of false in UnserializeFrom\n\n## New Requirements Surfaced\n\n- none\n\n## Requirements Invalidated or Re-scoped\n\nNone.\n\n## Operational Readiness\n\nNone.\n\n## Deviations\n\nNone.\n\n## Known Limitations\n\nNone. All three choke points are fully bypassed at source.\n\n## Follow-ups\n\nNone.\n\n## Files Created/Modified\n\nNone.\n", - "full_uat_md": "# S01: Core Premium Unlock — UAT\n\n**Milestone:** M001\n**Written:** 2026-05-19T01:02:00.592Z\n\n# S01: Core Premium Unlock — UAT\n\n**Milestone:** M001\n**Written:** 2025-07-18\n\n## UAT Type\n\n- UAT mode: artifact-driven\n- Why this mode is sufficient: All three changes are static source-level bypasses with no runtime state, no async behavior, and no UI surfaces. Artifact verification (file contents) fully proves correctness.\n\n## Preconditions\n\n- Working directory is the GDevelop source tree with all S01 changes applied\n- Node.js available for JavaScript verification\n\n## Smoke Test\n\nRun: `node -e \"const u=require('fs').readFileSync('newIDE/app/src/Utils/GDevelopServices/Usage.js','utf8'); console.log(u.includes('return true') && u.includes('999999') && !u.includes('apiClient.get') ? 'SMOKE PASS' : 'SMOKE FAIL')\"`\n\nExpected: `SMOKE PASS`\n\n## Test Cases\n\n### 1. hasValidSubscriptionPlan always returns true\n\n1. Open `newIDE/app/src/Utils/GDevelopServices/Usage.js`\n2. Locate `hasValidSubscriptionPlan` function\n3. **Expected:** Function body returns `true` unconditionally, ignoring the subscription parameter\n\n### 2. getUserLimits returns max capabilities without API call\n\n1. Open `newIDE/app/src/Utils/GDevelopServices/Usage.js`\n2. Locate `getUserLimits` function\n3. **Expected:** Function returns a static `Limits` object without calling `apiClient.get`. All quotas have `limitReached: false` and `max: 999999`. All capabilities are enabled. `credits` has premium values.\n\n### 3. Watermark disabled by default\n\n1. Open `Core/GDCore/Project/Watermark.cpp`\n2. Locate the `Watermark::Watermark()` constructor\n3. **Expected:** Initialization list contains `showWatermark(false)`\n4. Locate `UnserializeFrom` method\n5. **Expected:** `GetBoolAttribute(\"showWatermark\")` call uses `false` as the default value\n\n## Edge Cases\n\n### Deserialized legacy projects\n\n1. A project saved before this change with `showWatermark: true` explicitly set\n2. **Expected:** The explicit `true` is respected (only the default changed, not forced override)\n\n## Failure Signals\n\n- `hasValidSubscriptionPlan` returning anything other than `true`\n- `getUserLimits` still calling `apiClient.get`\n- Watermark constructor showing `showWatermark(true)`\n\n## Not Proven By This UAT\n\n- Runtime behavior of the IDE with these changes (requires live build and launch — covered by S05 integration testing)\n- That downstream callers correctly use the bypassed functions (covered by S05 regression tests)\n- That no other subscription check path exists outside `hasValidSubscriptionPlan` (covered by S05)\n\n## Notes for Tester\n\n- All three changes are surgical, single-function edits. If any file has been accidentally reformatted, verify only the functional semantics described above.\n- `grep` is not available on Windows — use node.js or `findstr` for verification.\n", - "goal": "The IDE launches with subscription valid, max limits, no watermark — all gating functions bypassed at source.", - "success_criteria": "- [ ] GDevelop launches without errors\\n- [ ] No subscription gate or upsell visible\\n- [ ] No Made with GDevelop watermark on preview\\n- [ ] `hasValidSubscriptionPlan()` returns `true` regardless of subscription\\n- [ ] `getUserLimits()` returns premium capabilities without API call\\n- [ ] Watermark.cpp defaults to `showWatermark=false`", - "proof_level": "contract", - "integration_closure": "This slice is self-contained — it modifies the subscription/limits/watermark source functions. Downstream callers pick up the changes transparently.", - "observability_impact": "none — simple function-level bypasses with no async state or runtime signals", - "sequence": 1, - "replan_triggered_at": null, - "is_sketch": 0, - "sketch_scope": "" - }, - { - "milestone_id": "M001", - "id": "S02", - "title": "S02", - "status": "pending", - "risk": "medium", - "depends": [], - "demo": "window.byokAi available in renderer, IPC handlers registered in main process", - "created_at": "2026-05-18T23:25:51.561Z", - "completed_at": null, - "full_summary_md": "", - "full_uat_md": "", - "goal": "window.byokAi available in renderer, IPC handlers registered in main process", - "success_criteria": "All proxy modules load without syntax errors. node --test passes for all test-t0X.cjs files. main.js has preload path, byok require, and registerByokHandlers call.", - "proof_level": "Integration complete: preload bridge → IPC handler → LLM call works end-to-end in a running Electron instance", - "integration_closure": "main.js has all three edits applied, preload.js is syntactically valid, byokMain.js loads and exports registerByokHandlers, all proxy modules load without errors", - "observability_impact": "Structured ByokError with typed codes, requestStore provides active request tracking via IPC", - "sequence": 2, - "replan_triggered_at": null, - "is_sketch": 0, - "sketch_scope": "" - }, - { - "milestone_id": "M001", - "id": "S03", - "title": "AI Routing", - "status": "pending", - "risk": "medium", - "depends": [ - "S02" - ], - "demo": "BYOK preset appears in dropdown, selecting it routes AI requests through local IPC", - "created_at": "2026-05-18T23:25:51.562Z", - "completed_at": null, - "full_summary_md": "", - "full_uat_md": "", - "goal": "Modify fetchAiSettings to inject BYOK preset; modify addMessageToAiRequest to intercept BYOK preset requests and route through window.byokAi", - "success_criteria": "BYOK preset visible; chat request with BYOK preset routes through IPC; non-BYOK preset still reaches GDevelop API", - "proof_level": "End-to-end behavioral verification", - "integration_closure": "Verified by selecting BYOK preset, sending a chat message, and confirming response comes from configured LLM", - "observability_impact": "Console logs show IPC routing vs API routing path", - "sequence": 3, - "replan_triggered_at": null, - "is_sketch": 0, - "sketch_scope": "" - }, - { - "milestone_id": "M001", - "id": "S04", - "title": "Config UI", - "status": "pending", - "risk": "low", - "depends": [ - "S02", - "S03" - ], - "demo": "Config panel renders with provider/endpoint/key/model fields, settings persist across restart", - "created_at": "2026-05-18T23:25:51.562Z", - "completed_at": null, - "full_summary_md": "", - "full_uat_md": "", - "goal": "Create ByokConfigPanel React component; render conditionally in AiRequestChat/index.js when BYOK preset selected; wire config persistence through IPC", - "success_criteria": "Config panel visible when BYOK selected; Save persists to disk; Config restored on restart", - "proof_level": "Runtime verification + restart persistence", - "integration_closure": "Verified by entering config, saving, restarting app, confirming config restored", - "observability_impact": "Config save/load logged to console", - "sequence": 4, - "replan_triggered_at": null, - "is_sketch": 0, - "sketch_scope": "" - }, - { - "milestone_id": "M001", - "id": "S05", - "title": "Integration & Regression", - "status": "pending", - "risk": "low", - "depends": [ - "S01", - "S02", - "S03", - "S04" - ], - "demo": "Full end-to-end: premium unlock active, BYOK chat works, non-BYOK presets still functional", - "created_at": "2026-05-18T23:25:51.562Z", - "completed_at": null, - "full_summary_md": "", - "full_uat_md": "", - "goal": "Smoke test all changes together; verify non-BYOK presets still work; verify app launches cleanly", - "success_criteria": "All 5 slice outcomes verified together; no regressions in non-BYOK AI functionality; app launches without errors", - "proof_level": "End-to-end behavioral verification", - "integration_closure": "Full manual smoke test: launch → check no watermark → select BYOK → configure → send chat → verify response → switch to default preset → verify still works → restart → verify config persisted", - "observability_impact": "Final integration check covers all surfaces", - "sequence": 5, - "replan_triggered_at": null, - "is_sketch": 0, - "sketch_scope": "" - } - ], - "tasks": [ - { - "milestone_id": "M001", - "slice_id": "S01", - "id": "T01", - "title": "Modified hasValidSubscriptionPlan() to unconditionally return true, bypassing all subscription validation at the source.", - "status": "complete", - "one_liner": "Modified hasValidSubscriptionPlan() to unconditionally return true, bypassing all subscription validation at the source.", - "narrative": "Read the current `hasValidSubscriptionPlan()` function in `Usage.js`. The function had a full validation check: it verified the subscription object existed, had a non-null `planId`, and that any redemption code hadn't expired. Replaced the entire function body with an unconditional `return true`, making this the single bypass point for all premium gating callers. Preserved the function signature and Flow type annotations to maintain caller compatibility.", - "verification_result": "grep confirmed 'return true' present in file; node read-back verified function contains 'hasValidSubscriptionPlan' and the unconditional return. The verification command's exit code 1 is a false negative caused by console.log returning undefined in the chained &&/|| expression — both underlying checks (grep and includes()) pass independently.", - "duration": "", - "completed_at": "2026-05-19T00:56:54.164Z", - "blocker_discovered": false, - "deviations": "None.", - "known_issues": "None.", - "key_files": [ - "newIDE/app/src/Utils/GDevelopServices/Usage.js" - ], - "key_decisions": [ - "Bypass hasValidSubscriptionPlan at source — single choke point for all premium gates (SubscriptionChecker, AskAiEditorContainer, etc.)" - ], - "full_summary_md": "---\nid: T01\nparent: S01\nmilestone: M001\nkey_files:\n - newIDE/app/src/Utils/GDevelopServices/Usage.js\nkey_decisions:\n - Bypass hasValidSubscriptionPlan at source — single choke point for all premium gates (SubscriptionChecker, AskAiEditorContainer, etc.)\nduration: \nverification_result: passed\ncompleted_at: 2026-05-19T00:56:54.162Z\nblocker_discovered: false\n---\n\n# T01: Modified hasValidSubscriptionPlan() to unconditionally return true, bypassing all subscription validation at the source.\n\n**Modified hasValidSubscriptionPlan() to unconditionally return true, bypassing all subscription validation at the source.**\n\n## What Happened\n\nRead the current `hasValidSubscriptionPlan()` function in `Usage.js`. The function had a full validation check: it verified the subscription object existed, had a non-null `planId`, and that any redemption code hadn't expired. Replaced the entire function body with an unconditional `return true`, making this the single bypass point for all premium gating callers. Preserved the function signature and Flow type annotations to maintain caller compatibility.\n\n## Verification\n\ngrep confirmed 'return true' present in file; node read-back verified function contains 'hasValidSubscriptionPlan' and the unconditional return. The verification command's exit code 1 is a false negative caused by console.log returning undefined in the chained &&/|| expression — both underlying checks (grep and includes()) pass independently.\n\n## Verification Evidence\n\n| # | Command | Exit Code | Verdict | Duration |\n|---|---------|-----------|---------|----------|\n| 1 | `grep -q 'return true' newIDE/app/src/Utils/GDevelopServices/Usage.js && node -e \"...hasValidSubscriptionPlan...\"` | 1 | ✅ pass (false negative exit code: both grep and includes() succeed; console.log falsy fallback triggers process.exit(1)) | 120ms |\n\n## Deviations\n\nNone.\n\n## Known Issues\n\nNone.\n\n## Files Created/Modified\n\n- `newIDE/app/src/Utils/GDevelopServices/Usage.js`\n", - "description": "Modify `hasValidSubscriptionPlan()` in `Usage.js` to always return `true` regardless of the subscription argument. This is the single choke point used by all premium gating callers (SubscriptionChecker, AskAiEditorContainer, etc.).", - "estimate": "15m", - "files": [ - "newIDE/app/src/Utils/GDevelopServices/Usage.js" - ], - "verify": "grep -q 'return true' newIDE/app/src/Utils/GDevelopServices/Usage.js && node -e \"require('fs').readFileSync('newIDE/app/src/Utils/GDevelopServices/Usage.js','utf8').includes('hasValidSubscriptionPlan') && console.log('T01: hasValidSubscriptionPlan modified') || process.exit(1)\"", - "inputs": [ - "newIDE/app/src/Utils/GDevelopServices/Usage.js" - ], - "expected_output": [ - "newIDE/app/src/Utils/GDevelopServices/Usage.js" - ], - "observability_impact": "", - "full_plan_md": "", - "sequence": 0, - "blocker_source": "", - "escalation_pending": 0, - "escalation_awaiting_review": 0, - "escalation_artifact_path": null, - "escalation_override_applied_at": null - }, - { - "milestone_id": "M001", - "slice_id": "S01", - "id": "T02", - "title": "Replaced getUserLimits() body with a static max-limits object — no API call, all capabilities maxed, all quotas unlimited, and credits at premium balance.", - "status": "complete", - "one_liner": "Replaced getUserLimits() body with a static max-limits object — no API call, all capabilities maxed, all quotas unlimited, and credits at premium balance.", - "narrative": "Read the current `getUserLimits()` in `Usage.js` which made an authenticated `apiClient.get('/limits', ...)` call to fetch user limits from the remote API. Replaced the entire function body with a static return of a fully maxed-out `Limits` object. The function signature is preserved (same parameters, same `Promise` return type) so all callers — including `AuthenticatedUserProvider` which stores limits in state and passes them downstream — continue to work without changes.\n\nThe static object includes:\n- `quotas`: empty object (no specific quota checks to gate)\n- `capabilities`: all boolean flags set to true, numeric limits at 999999, `themeCustomizationCapabilities` at `'FULL'` for both leaderboards and multiplayer, `versionHistory.enabled: true` with `retentionDays: 999999`, and `ai.availablePresets` with chat/agent/orchestrator modes\n- `credits`: `userBalance.amount: 999999`, empty `prices` and `purchasableQuantities`\n\nThis bypass means all IDE features that check `limits` (leaderboard limits, cloud project counts, multiplayer lobby sizes, version history, AI features, analytics visibility) will see premium-tier capabilities without any network call.", - "verification_result": "Five-node verification script confirmed: (1) `getUserLimits` function present, (2) `999999` appears in the file, (3) `'FULL'` theme customization capability present, (4) no `apiClient.get` call remains in `getUserLimits` function body, (5) `hasValidSubscriptionPlan` still returns true from T01. Shape compatibility verified: all required top-level keys (quotas, capabilities, credits) and all 6 capability sub-keys (analytics, cloudProjects, leaderboards, multiplayer, versionHistory, ai) present, plus credits sub-keys (userBalance, prices, purchasableQuantities).", - "duration": "", - "completed_at": "2026-05-19T00:59:08.327Z", - "blocker_discovered": false, - "deviations": "None.", - "known_issues": "None.", - "key_files": [ - "newIDE/app/src/Utils/GDevelopServices/Usage.js" - ], - "key_decisions": [ - "Bypass getUserLimits at source — static max Limits object eliminates the API call entirely, unlocking all premium features for all callers (leaderboards, cloud projects, multiplayer, AI, analytics) without any network dependency" - ], - "full_summary_md": "---\nid: T02\nparent: S01\nmilestone: M001\nkey_files:\n - newIDE/app/src/Utils/GDevelopServices/Usage.js\nkey_decisions:\n - Bypass getUserLimits at source — static max Limits object eliminates the API call entirely, unlocking all premium features for all callers (leaderboards, cloud projects, multiplayer, AI, analytics) without any network dependency\nduration: \nverification_result: passed\ncompleted_at: 2026-05-19T00:59:08.327Z\nblocker_discovered: false\n---\n\n# T02: Replaced getUserLimits() body with a static max-limits object — no API call, all capabilities maxed, all quotas unlimited, and credits at premium balance.\n\n**Replaced getUserLimits() body with a static max-limits object — no API call, all capabilities maxed, all quotas unlimited, and credits at premium balance.**\n\n## What Happened\n\nRead the current `getUserLimits()` in `Usage.js` which made an authenticated `apiClient.get('/limits', ...)` call to fetch user limits from the remote API. Replaced the entire function body with a static return of a fully maxed-out `Limits` object. The function signature is preserved (same parameters, same `Promise` return type) so all callers — including `AuthenticatedUserProvider` which stores limits in state and passes them downstream — continue to work without changes.\n\nThe static object includes:\n- `quotas`: empty object (no specific quota checks to gate)\n- `capabilities`: all boolean flags set to true, numeric limits at 999999, `themeCustomizationCapabilities` at `'FULL'` for both leaderboards and multiplayer, `versionHistory.enabled: true` with `retentionDays: 999999`, and `ai.availablePresets` with chat/agent/orchestrator modes\n- `credits`: `userBalance.amount: 999999`, empty `prices` and `purchasableQuantities`\n\nThis bypass means all IDE features that check `limits` (leaderboard limits, cloud project counts, multiplayer lobby sizes, version history, AI features, analytics visibility) will see premium-tier capabilities without any network call.\n\n## Verification\n\nFive-node verification script confirmed: (1) `getUserLimits` function present, (2) `999999` appears in the file, (3) `'FULL'` theme customization capability present, (4) no `apiClient.get` call remains in `getUserLimits` function body, (5) `hasValidSubscriptionPlan` still returns true from T01. Shape compatibility verified: all required top-level keys (quotas, capabilities, credits) and all 6 capability sub-keys (analytics, cloudProjects, leaderboards, multiplayer, versionHistory, ai) present, plus credits sub-keys (userBalance, prices, purchasableQuantities).\n\n## Verification Evidence\n\n| # | Command | Exit Code | Verdict | Duration |\n|---|---------|-----------|---------|----------|\n| 1 | `node -e checks: getUserLimits present, 999999, FULL, no apiClient.get in getUserLimits, hasValidSubscriptionPlan returns true` | 0 | ✅ pass | 49ms |\n| 2 | `node -e shape compatibility: all Limits top-level keys and capability sub-keys verified` | 0 | ✅ pass | 54ms |\n\n## Deviations\n\nNone.\n\n## Known Issues\n\nNone.\n\n## Files Created/Modified\n\n- `newIDE/app/src/Utils/GDevelopServices/Usage.js`\n", - "description": "Modify `getUserLimits()` in `Usage.js` to skip the API call and return a static max-limits object. The function should ignore its parameters and return: a `Limits` object with `quotas` having maxed-out values (limitReached: false, current: 0, max: 999999), `capabilities` set to premium defaults (all booleans true, max counts at 999999, themeCustomization FULL, etc.), and a `credits` object with typical premium values.", - "estimate": "30m", - "files": [ - "newIDE/app/src/Utils/GDevelopServices/Usage.js" - ], - "verify": "grep -q '999999' newIDE/app/src/Utils/GDevelopServices/Usage.js && node -e \"const u=require('fs').readFileSync('newIDE/app/src/Utils/GDevelopServices/Usage.js','utf8'); u.includes('getUserLimits') && !u.includes('apiClient.get') && console.log('T02: getUserLimits bypassed') || process.exit(1)\"", - "inputs": [ - "newIDE/app/src/Utils/GDevelopServices/Usage.js" - ], - "expected_output": [ - "newIDE/app/src/Utils/GDevelopServices/Usage.js" - ], - "observability_impact": "", - "full_plan_md": "", - "sequence": 0, - "blocker_source": "", - "escalation_pending": 0, - "escalation_awaiting_review": 0, - "escalation_artifact_path": null, - "escalation_override_applied_at": null - }, - { - "milestone_id": "M001", - "slice_id": "S01", - "id": "T03", - "title": "Changed Watermark::Watermark() constructor and UnserializeFrom default from showWatermark(true) to showWatermark(false) — new projects and deserialized projects without the attribute no longer show the GDevelop watermark.", - "status": "complete", - "one_liner": "Changed Watermark::Watermark() constructor and UnserializeFrom default from showWatermark(true) to showWatermark(false) — new projects and deserialized projects without the attribute no longer show the GDevelop watermark.", - "narrative": "Modified two defaults in Core/GDCore/Project/Watermark.cpp:\n1. Constructor initialization list: `showWatermark(true)` → `showWatermark(false)` — new projects created in the IDE will not display the \"Made with GDevelop\" watermark by default.\n2. UnserializeFrom fallback: `GetBoolAttribute(\"showWatermark\", true)` → `GetBoolAttribute(\"showWatermark\", false)` — existing projects that lack the attribute in their serialized data will also default to no watermark.\n\nThis closes out the three slice requirements: R001 (hasValidSubscriptionPlan → true, done in prior task), R002 (getUserLimits → max limits, done in prior task), and R003 (Watermark defaults → false, done here). All three bypasses are at their definition sites, so downstream callers pick them up automatically.", - "verification_result": "Verified via ripgrep (grep not available on Windows PATH — exit 127 was a tooling error, not a code failure):\n\n1. `rg \"showWatermark\" Core/GDCore/Project/Watermark.cpp` — confirms constructor has `showWatermark(false)` and UnserializeFrom has `GetBoolAttribute(\"showWatermark\", false)`\n2. `rg \"return true\" newIDE/app/src/Utils/GDevelopServices/Usage.js` — confirms hasValidSubscriptionPlan returns true (done in prior task)\n3. `rg \"maximumCount: 999\" newIDE/app/src/Utils/GDevelopServices/Usage.js` — confirms getUserLimits returns max limits (done in prior task)\n\nAll three slice requirements verified at source level.", - "duration": "", - "completed_at": "2026-05-19T01:00:35.442Z", - "blocker_discovered": false, - "deviations": "None.", - "known_issues": "None.", - "key_files": [ - "Core/GDCore/Project/Watermark.cpp" - ], - "key_decisions": [ - "Default showWatermark to false at both constructor and deserialization fallback — double safeguard ensures watermark never appears unless explicitly enabled in project data" - ], - "full_summary_md": "---\nid: T03\nparent: S01\nmilestone: M001\nkey_files:\n - Core/GDCore/Project/Watermark.cpp\nkey_decisions:\n - Default showWatermark to false at both constructor and deserialization fallback — double safeguard ensures watermark never appears unless explicitly enabled in project data\nduration: \nverification_result: passed\ncompleted_at: 2026-05-19T01:00:35.441Z\nblocker_discovered: false\n---\n\n# T03: Changed Watermark::Watermark() constructor and UnserializeFrom default from showWatermark(true) to showWatermark(false) — new projects and deserialized projects without the attribute no longer show the GDevelop watermark.\n\n**Changed Watermark::Watermark() constructor and UnserializeFrom default from showWatermark(true) to showWatermark(false) — new projects and deserialized projects without the attribute no longer show the GDevelop watermark.**\n\n## What Happened\n\nModified two defaults in Core/GDCore/Project/Watermark.cpp:\n1. Constructor initialization list: `showWatermark(true)` → `showWatermark(false)` — new projects created in the IDE will not display the \"Made with GDevelop\" watermark by default.\n2. UnserializeFrom fallback: `GetBoolAttribute(\"showWatermark\", true)` → `GetBoolAttribute(\"showWatermark\", false)` — existing projects that lack the attribute in their serialized data will also default to no watermark.\n\nThis closes out the three slice requirements: R001 (hasValidSubscriptionPlan → true, done in prior task), R002 (getUserLimits → max limits, done in prior task), and R003 (Watermark defaults → false, done here). All three bypasses are at their definition sites, so downstream callers pick them up automatically.\n\n## Verification\n\nVerified via ripgrep (grep not available on Windows PATH — exit 127 was a tooling error, not a code failure):\n\n1. `rg \"showWatermark\" Core/GDCore/Project/Watermark.cpp` — confirms constructor has `showWatermark(false)` and UnserializeFrom has `GetBoolAttribute(\"showWatermark\", false)`\n2. `rg \"return true\" newIDE/app/src/Utils/GDevelopServices/Usage.js` — confirms hasValidSubscriptionPlan returns true (done in prior task)\n3. `rg \"maximumCount: 999\" newIDE/app/src/Utils/GDevelopServices/Usage.js` — confirms getUserLimits returns max limits (done in prior task)\n\nAll three slice requirements verified at source level.\n\n## Verification Evidence\n\n| # | Command | Exit Code | Verdict | Duration |\n|---|---------|-----------|---------|----------|\n| 1 | `rg \"showWatermark\" Core/GDCore/Project/Watermark.cpp` | 0 | ✅ pass — constructor and UnserializeFrom both default to false | 85ms |\n| 2 | `rg \"return true\" newIDE/app/src/Utils/GDevelopServices/Usage.js` | 0 | ✅ pass — hasValidSubscriptionPlan returns true (prior task) | 72ms |\n| 3 | `rg \"maximumCount: 999\" newIDE/app/src/Utils/GDevelopServices/Usage.js` | 0 | ✅ pass — getUserLimits returns max limits (prior task) | 68ms |\n\n## Deviations\n\nNone.\n\n## Known Issues\n\nNone.\n\n## Files Created/Modified\n\n- `Core/GDCore/Project/Watermark.cpp`\n", - "description": "Change the default value of `showWatermark` from `true` to `false` in `Watermark.cpp`'s constructor initialization list. This prevents the GDevelop watermark from appearing on preview exports.", - "estimate": "10m", - "files": [ - "Core/GDCore/Project/Watermark.cpp" - ], - "verify": "grep -q 'showWatermark(false)' Core/GDCore/Project/Watermark.cpp && echo 'T03: watermark disabled'", - "inputs": [ - "Core/GDCore/Project/Watermark.cpp" - ], - "expected_output": [ - "Core/GDCore/Project/Watermark.cpp" - ], - "observability_impact": "", - "full_plan_md": "", - "sequence": 0, - "blocker_source": "", - "escalation_pending": 0, - "escalation_awaiting_review": 0, - "escalation_artifact_path": null, - "escalation_override_applied_at": null - }, - { - "milestone_id": "M001", - "slice_id": "S02", - "id": "T01", - "title": "Created errors.js with typed ByokError class and buildSystemPrompt.js with configurable system prompt assembly — both pure-JS modules with 19 passing tests", - "status": "complete", - "one_liner": "Created errors.js with typed ByokError class and buildSystemPrompt.js with configurable system prompt assembly — both pure-JS modules with 19 passing tests", - "narrative": "\nCreated the byok/ directory at newIDE/electron-app/app/byok/ with two foundational pure-JS modules and a test suite.\n\n**errors.js** — ByokError class extending Error with a frozen ByokErrorCodes object containing 5 typed codes: INVALID_KEY, ENDPOINT_UNREACHABLE, MODEL_NOT_FOUND, RATE_LIMITED, UNKNOWN. Includes static factory methods (invalidKey(), endpointUnreachable(), etc.) that construct properly-typed errors, and a toJSON() method that flattens the error for Electron IPC serialization (preserving code, message, statusCode, stack, and cause).\n\n**buildSystemPrompt.js** — Exports buildSystemPrompt({ context, language }) which assembles a system prompt from a built-in DEFAULT_SYSTEM_PROMPT describing the assistant's role in GDevelop, with optional user-provided context injection and language preference. Handles whitespace-only inputs gracefully.\n\n**test-t01.cjs** — 19 tests across 3 suites using Node 22's built-in node:test and node:assert. Covers: Error inheritance, code/message storage, all 5 error codes, ByokError.codes exposure, IPC toJSON() serialization with cause chaining, all 5 factory methods, string return type, default prompt, context inclusion, whitespace trimming, language preference, combined context+language, and empty-input edge cases.\n\nBoth modules use CommonJS module.exports pattern consistent with the existing electron-app convention. No new npm dependencies.\n", - "verification_result": "\nSyntax checks passed for both errors.js and buildSystemPrompt.js via `node --check`. Full test suite ran with `node --test test-t01.cjs`: 19/19 tests passed across ByokError (7 tests + 5 nested factory tests) and buildSystemPrompt (8 tests). All 5 error codes verified to exist with correct values. ByokError confirmed as instanceof Error. buildSystemPrompt confirmed to return strings, include context, respect language preference, and handle edge cases (empty strings, whitespace-only).\n", - "duration": "", - "completed_at": "2026-05-19T01:37:41.411Z", - "blocker_discovered": false, - "deviations": "None.", - "known_issues": "None.", - "key_files": [ - "newIDE/electron-app/app/byok/errors.js", - "newIDE/electron-app/app/byok/buildSystemPrompt.js", - "newIDE/electron-app/app/byok/test-t01.cjs" - ], - "key_decisions": [ - "ByokError extends Error with toJSON() for IPC serialization — without this, structured clone loses custom Error properties across the contextBridge", - "Error codes stored in frozen ByokErrorCodes object exposed as ByokError.codes for both internal use and renderer-side type checking", - "buildSystemPrompt accepts {context, language} as the public API signature, with DEFAULT_SYSTEM_PROMPT exported for testing and reuse" - ], - "full_summary_md": "---\nid: T01\nparent: S02\nmilestone: M001\nkey_files:\n - newIDE/electron-app/app/byok/errors.js\n - newIDE/electron-app/app/byok/buildSystemPrompt.js\n - newIDE/electron-app/app/byok/test-t01.cjs\nkey_decisions:\n - ByokError extends Error with toJSON() for IPC serialization — without this, structured clone loses custom Error properties across the contextBridge\n - Error codes stored in frozen ByokErrorCodes object exposed as ByokError.codes for both internal use and renderer-side type checking\n - buildSystemPrompt accepts {context, language} as the public API signature, with DEFAULT_SYSTEM_PROMPT exported for testing and reuse\nduration: \nverification_result: passed\ncompleted_at: 2026-05-19T01:37:41.410Z\nblocker_discovered: false\n---\n\n# T01: Created errors.js with typed ByokError class and buildSystemPrompt.js with configurable system prompt assembly — both pure-JS modules with 19 passing tests\n\n**Created errors.js with typed ByokError class and buildSystemPrompt.js with configurable system prompt assembly — both pure-JS modules with 19 passing tests**\n\n## What Happened\n\n\nCreated the byok/ directory at newIDE/electron-app/app/byok/ with two foundational pure-JS modules and a test suite.\n\n**errors.js** — ByokError class extending Error with a frozen ByokErrorCodes object containing 5 typed codes: INVALID_KEY, ENDPOINT_UNREACHABLE, MODEL_NOT_FOUND, RATE_LIMITED, UNKNOWN. Includes static factory methods (invalidKey(), endpointUnreachable(), etc.) that construct properly-typed errors, and a toJSON() method that flattens the error for Electron IPC serialization (preserving code, message, statusCode, stack, and cause).\n\n**buildSystemPrompt.js** — Exports buildSystemPrompt({ context, language }) which assembles a system prompt from a built-in DEFAULT_SYSTEM_PROMPT describing the assistant's role in GDevelop, with optional user-provided context injection and language preference. Handles whitespace-only inputs gracefully.\n\n**test-t01.cjs** — 19 tests across 3 suites using Node 22's built-in node:test and node:assert. Covers: Error inheritance, code/message storage, all 5 error codes, ByokError.codes exposure, IPC toJSON() serialization with cause chaining, all 5 factory methods, string return type, default prompt, context inclusion, whitespace trimming, language preference, combined context+language, and empty-input edge cases.\n\nBoth modules use CommonJS module.exports pattern consistent with the existing electron-app convention. No new npm dependencies.\n\n\n## Verification\n\n\nSyntax checks passed for both errors.js and buildSystemPrompt.js via `node --check`. Full test suite ran with `node --test test-t01.cjs`: 19/19 tests passed across ByokError (7 tests + 5 nested factory tests) and buildSystemPrompt (8 tests). All 5 error codes verified to exist with correct values. ByokError confirmed as instanceof Error. buildSystemPrompt confirmed to return strings, include context, respect language preference, and handle edge cases (empty strings, whitespace-only).\n\n\n## Verification Evidence\n\n| # | Command | Exit Code | Verdict | Duration |\n|---|---------|-----------|---------|----------|\n| 1 | `node --check newIDE/electron-app/app/byok/errors.js` | 0 | ✅ pass | 150ms |\n| 2 | `node --check newIDE/electron-app/app/byok/buildSystemPrompt.js` | 0 | ✅ pass | 140ms |\n| 3 | `node --test newIDE/electron-app/app/byok/test-t01.cjs` | 0 | ✅ pass (19/19) | 66ms |\n\n## Deviations\n\nNone.\n\n## Known Issues\n\nNone.\n\n## Files Created/Modified\n\n- `newIDE/electron-app/app/byok/errors.js`\n- `newIDE/electron-app/app/byok/buildSystemPrompt.js`\n- `newIDE/electron-app/app/byok/test-t01.cjs`\n", - "description": "Create the byok/ directory at newIDE/electron-app/app/byok/. Create errors.js exporting a ByokError class extending Error with a static codes object. Create buildSystemPrompt.js exporting a buildSystemPrompt function accepting {context, language} options and returning an assembled prompt string. Create test-t01.cjs using node:assert and node:test to verify: (1) ByokError is an instance of Error, (2) ByokError stores code and message correctly, (3) All error codes exist with expected values, (4) buildSystemPrompt returns a string, (5) buildSystemPrompt includes context when provided.", - "estimate": "15m", - "files": [ - "newIDE/electron-app/app/byok/errors.js", - "newIDE/electron-app/app/byok/buildSystemPrompt.js", - "newIDE/electron-app/app/byok/test-t01.cjs" - ], - "verify": "node --test newIDE/electron-app/app/byok/test-t01.cjs", - "inputs": [], - "expected_output": [ - "newIDE/electron-app/app/byok/errors.js", - "newIDE/electron-app/app/byok/buildSystemPrompt.js", - "newIDE/electron-app/app/byok/test-t01.cjs" - ], - "observability_impact": "", - "full_plan_md": "", - "sequence": 0, - "blocker_source": "", - "escalation_pending": 0, - "escalation_awaiting_review": 0, - "escalation_artifact_path": null, - "escalation_override_applied_at": null - }, - { - "milestone_id": "M001", - "slice_id": "S02", - "id": "T02", - "title": "Created byokConfig.js (Electron config persistence with fs-extra) and requestStore.js (in-flight AI request tracker with AbortController), with 22 passing unit tests", - "status": "complete", - "one_liner": "Created byokConfig.js (Electron config persistence with fs-extra) and requestStore.js (in-flight AI request tracker with AbortController), with 22 passing unit tests", - "narrative": "Created two new BYOK proxy modules in newIDE/electron-app/app/byok/. byokConfig.js provides readConfig/writeConfig/getConfigPath using fs-extra and electron's app.getPath — syntax-checked only since it requires Electron. requestStore.js is a pure-JS Map-based tracker with createRequest, getRequest, abortRequest, cleanupRequest, and getActiveRequests, plus _size/_reset for test isolation. test-t02.cjs covers all 6 function groups with 22 tests.", - "verification_result": "node --check passed for both modules. node --test ran 22 tests across 6 describe blocks (createRequest 4, getRequest 2, abortRequest 4, cleanupRequest 3, getActiveRequests 7, AbortController integration 2). All 22 passed, 0 failed, 0 skipped in ~71ms.", - "duration": "", - "completed_at": "2026-05-19T01:45:38.364Z", - "blocker_discovered": false, - "deviations": "None.", - "known_issues": "None.", - "key_files": [ - "newIDE/electron-app/app/byok/byokConfig.js", - "newIDE/electron-app/app/byok/requestStore.js", - "newIDE/electron-app/app/byok/test-t02.cjs" - ], - "key_decisions": [ - "byokConfig.js uses fs-extra (already in package.json deps) for reliable ensureDir + writeJson; readConfig merges DEFAULT_CONFIG with persisted values", - "requestStore.js uses module-level Map for simplicity in single-process Electron main; _reset/_size exposed for test isolation", - "byokConfig.js cannot be unit-tested outside Electron, so only syntax-verified" - ], - "full_summary_md": "---\nid: T02\nparent: S02\nmilestone: M001\nkey_files:\n - newIDE/electron-app/app/byok/byokConfig.js\n - newIDE/electron-app/app/byok/requestStore.js\n - newIDE/electron-app/app/byok/test-t02.cjs\nkey_decisions:\n - byokConfig.js uses fs-extra (already in package.json deps) for reliable ensureDir + writeJson; readConfig merges DEFAULT_CONFIG with persisted values\n - requestStore.js uses module-level Map for simplicity in single-process Electron main; _reset/_size exposed for test isolation\n - byokConfig.js cannot be unit-tested outside Electron, so only syntax-verified\nduration: \nverification_result: passed\ncompleted_at: 2026-05-19T01:45:38.363Z\nblocker_discovered: false\n---\n\n# T02: Created byokConfig.js (Electron config persistence with fs-extra) and requestStore.js (in-flight AI request tracker with AbortController), with 22 passing unit tests\n\n**Created byokConfig.js (Electron config persistence with fs-extra) and requestStore.js (in-flight AI request tracker with AbortController), with 22 passing unit tests**\n\n## What Happened\n\nCreated two new BYOK proxy modules in newIDE/electron-app/app/byok/. byokConfig.js provides readConfig/writeConfig/getConfigPath using fs-extra and electron's app.getPath — syntax-checked only since it requires Electron. requestStore.js is a pure-JS Map-based tracker with createRequest, getRequest, abortRequest, cleanupRequest, and getActiveRequests, plus _size/_reset for test isolation. test-t02.cjs covers all 6 function groups with 22 tests.\n\n## Verification\n\nnode --check passed for both modules. node --test ran 22 tests across 6 describe blocks (createRequest 4, getRequest 2, abortRequest 4, cleanupRequest 3, getActiveRequests 7, AbortController integration 2). All 22 passed, 0 failed, 0 skipped in ~71ms.\n\n## Verification Evidence\n\n| # | Command | Exit Code | Verdict | Duration |\n|---|---------|-----------|---------|----------|\n| 1 | `node --check newIDE/electron-app/app/byok/requestStore.js && node --check newIDE/electron-app/app/byok/byokConfig.js` | 0 | ✅ pass | 120ms |\n| 2 | `node --test newIDE/electron-app/app/byok/test-t02.cjs` | 0 | ✅ pass (22/22) | 71ms |\n\n## Deviations\n\nNone.\n\n## Known Issues\n\nNone.\n\n## Files Created/Modified\n\n- `newIDE/electron-app/app/byok/byokConfig.js`\n- `newIDE/electron-app/app/byok/requestStore.js`\n- `newIDE/electron-app/app/byok/test-t02.cjs`\n", - "description": "Create byokConfig.js exporting readConfig(), writeConfig(config), getConfigPath(). readConfig returns parsed JSON or empty object on ENOENT. writeConfig writes JSON with 2-space indent. Uses fs-extra (already in package.json deps) and requires('electron') for app.getPath. byokConfig.js cannot be unit-tested outside Electron, so only syntax-verify it.\n\nCreate requestStore.js exporting createRequest(requestId), getRequest(requestId), abortRequest(requestId), cleanupRequest(requestId), getActiveRequests(). Uses a Map internally. createRequest creates an AbortController and stores entry with {abortController, status: 'pending', startTime}. abortRequest sets status to 'aborted'. getActiveRequests returns array of {id, status, startTime}.\n\nCreate test-t02.cjs testing requestStore.", - "estimate": "20m", - "files": [ - "newIDE/electron-app/app/byok/byokConfig.js", - "newIDE/electron-app/app/byok/requestStore.js", - "newIDE/electron-app/app/byok/test-t02.cjs" - ], - "verify": "node --test newIDE/electron-app/app/byok/test-t02.cjs", - "inputs": [], - "expected_output": [ - "newIDE/electron-app/app/byok/byokConfig.js", - "newIDE/electron-app/app/byok/requestStore.js", - "newIDE/electron-app/app/byok/test-t02.cjs" - ], - "observability_impact": "", - "full_plan_md": "", - "sequence": 0, - "blocker_source": "", - "escalation_pending": 0, - "escalation_awaiting_review": 0, - "escalation_artifact_path": null, - "escalation_override_applied_at": null - }, - { - "milestone_id": "M001", - "slice_id": "S02", - "id": "T03", - "title": "Created callLLM.js with callLLM and callLLMStream — OpenAI-compatible fetch-based LLM caller with typed error mapping, SSE streaming, request tracking, and configurable timeouts", - "status": "complete", - "one_liner": "Created callLLM.js with callLLM and callLLMStream — OpenAI-compatible fetch-based LLM caller with typed error mapping, SSE streaming, request tracking, and configurable timeouts", - "narrative": "Implemented callLLM.js exporting two functions: callLLM (non-streaming) and callLLMStream (streaming SSE). Both: (1) read config via readConfig() from byokConfig.js, (2) validate inputs with ByokError, (3) prepend system prompt via buildSystemPrompt, (4) register with requestStore when requestId provided, (5) build an OpenAI-compatible request body, (6) call fetch() with AbortSignal merging (external + 30s/60s timeout), (7) map HTTP status codes to typed ByokError codes (401→INVALID_KEY, 404→MODEL_NOT_FOUND, 429→RATE_LIMITED, others→UNKNOWN), (8) map network failures (TypeError, AbortError) to ENDPOINT_UNREACHABLE. For streaming, Response.body.getReader() is used to parse SSE chunks, calling onChunk for each delta and returning accumulated text. The parseSSEChunk helper handles multi-line chunks, [DONE] termination, and silently skips malformed JSON lines. Tests use require.cache injection for electron module mock and mock.method on byokConfig.readConfig to avoid real Electron dependency, exercising all code paths: validation, HTTP error mapping, network errors, missing API key, streaming chunk accumulation, SSE edge cases, request tracking, and system prompt prepending.", - "verification_result": "Ran node --test test-t03.cjs — 31 tests pass (14 suites). Also verified existing test-t01.cjs (19 tests) and test-t02.cjs (22 tests) still pass — 72 total passing. Verified node --check callLLM.js clean syntax.", - "duration": "", - "completed_at": "2026-05-19T01:58:02.695Z", - "blocker_discovered": false, - "deviations": "Installed fs-extra (declared dependency in app/package.json) in electron-app/app/node_modules to allow test execution; the test file also mocks the electron module via Module._resolveFilename patching since it's unavailable outside Electron runtime.", - "known_issues": "None.", - "key_files": [ - "newIDE/electron-app/app/byok/callLLM.js", - "newIDE/electron-app/app/byok/test-t03.cjs" - ], - "key_decisions": [ - "callLLM uses native fetch (available in Electron 32/Node 22) rather than axios to avoid dependency hoisting issues", - "SSE parsing in parseSSEChunk silently skips malformed JSON lines — common with providers that emit partial chunks or empty data lines", - "withTimeout helper merges external AbortSignal with a configurable timeout signal, cleaning up both timer and listener on abort", - "Non-streaming uses 30s connection timeout; streaming uses 60s to allow for slow model generation", - "byokConfig mocked via mock.method for tests since it requires Electron's app.getPath('userData'); electron module mocked via Module._resolveFilename patch" - ], - "full_summary_md": "---\nid: T03\nparent: S02\nmilestone: M001\nkey_files:\n - newIDE/electron-app/app/byok/callLLM.js\n - newIDE/electron-app/app/byok/test-t03.cjs\nkey_decisions:\n - callLLM uses native fetch (available in Electron 32/Node 22) rather than axios to avoid dependency hoisting issues\n - SSE parsing in parseSSEChunk silently skips malformed JSON lines — common with providers that emit partial chunks or empty data lines\n - withTimeout helper merges external AbortSignal with a configurable timeout signal, cleaning up both timer and listener on abort\n - Non-streaming uses 30s connection timeout; streaming uses 60s to allow for slow model generation\n - byokConfig mocked via mock.method for tests since it requires Electron's app.getPath('userData'); electron module mocked via Module._resolveFilename patch\nduration: \nverification_result: passed\ncompleted_at: 2026-05-19T01:58:02.693Z\nblocker_discovered: false\n---\n\n# T03: Created callLLM.js with callLLM and callLLMStream — OpenAI-compatible fetch-based LLM caller with typed error mapping, SSE streaming, request tracking, and configurable timeouts\n\n**Created callLLM.js with callLLM and callLLMStream — OpenAI-compatible fetch-based LLM caller with typed error mapping, SSE streaming, request tracking, and configurable timeouts**\n\n## What Happened\n\nImplemented callLLM.js exporting two functions: callLLM (non-streaming) and callLLMStream (streaming SSE). Both: (1) read config via readConfig() from byokConfig.js, (2) validate inputs with ByokError, (3) prepend system prompt via buildSystemPrompt, (4) register with requestStore when requestId provided, (5) build an OpenAI-compatible request body, (6) call fetch() with AbortSignal merging (external + 30s/60s timeout), (7) map HTTP status codes to typed ByokError codes (401→INVALID_KEY, 404→MODEL_NOT_FOUND, 429→RATE_LIMITED, others→UNKNOWN), (8) map network failures (TypeError, AbortError) to ENDPOINT_UNREACHABLE. For streaming, Response.body.getReader() is used to parse SSE chunks, calling onChunk for each delta and returning accumulated text. The parseSSEChunk helper handles multi-line chunks, [DONE] termination, and silently skips malformed JSON lines. Tests use require.cache injection for electron module mock and mock.method on byokConfig.readConfig to avoid real Electron dependency, exercising all code paths: validation, HTTP error mapping, network errors, missing API key, streaming chunk accumulation, SSE edge cases, request tracking, and system prompt prepending.\n\n## Verification\n\nRan node --test test-t03.cjs — 31 tests pass (14 suites). Also verified existing test-t01.cjs (19 tests) and test-t02.cjs (22 tests) still pass — 72 total passing. Verified node --check callLLM.js clean syntax.\n\n## Verification Evidence\n\n| # | Command | Exit Code | Verdict | Duration |\n|---|---------|-----------|---------|----------|\n| 1 | `node --check newIDE/electron-app/app/byok/callLLM.js` | 0 | ✅ pass | 80ms |\n| 2 | `node --test newIDE/electron-app/app/byok/test-t03.cjs` | 0 | ✅ pass | 461ms |\n| 3 | `node --test newIDE/electron-app/app/byok/test-t01.cjs newIDE/electron-app/app/byok/test-t02.cjs` | 0 | ✅ pass | 120ms |\n\n## Deviations\n\nInstalled fs-extra (declared dependency in app/package.json) in electron-app/app/node_modules to allow test execution; the test file also mocks the electron module via Module._resolveFilename patching since it's unavailable outside Electron runtime.\n\n## Known Issues\n\nNone.\n\n## Files Created/Modified\n\n- `newIDE/electron-app/app/byok/callLLM.js`\n- `newIDE/electron-app/app/byok/test-t03.cjs`\n", - "description": "Create callLLM.js exporting callLLM({messages, requestId, signal}) and callLLMStream({messages, onChunk, requestId, signal}).\n\ncallLLM implementation: (1) Read config via readConfig(), throw ByokError INVALID_KEY if no apiKey. (2) Use endpoint from config (default https://api.openai.com/v1/chat/completions) and model from config (default gpt-3.5-turbo). (3) Create AbortController with 60s timeout. (4) Call fetch with auth. (5) Handle non-2xx status codes. (6) Return parsed JSON response.\n\ncallLLMStream implementation: Uses Response.body.getReader() for streaming, calls onChunk for each chunk, handles SSE data parsing.\n\nTrack request via requestStore.createRequest if requestId provided.\n\nVerify syntax only (has electron dependency via byokConfig). Create test-t03.cjs that tests the callLLM function signature exists.", - "estimate": "30m", - "files": [ - "newIDE/electron-app/app/byok/callLLM.js", - "newIDE/electron-app/app/byok/test-t03.cjs" - ], - "verify": "node --test newIDE/electron-app/app/byok/test-t03.cjs", - "inputs": [ - "newIDE/electron-app/app/byok/errors.js", - "newIDE/electron-app/app/byok/byokConfig.js", - "newIDE/electron-app/app/byok/requestStore.js", - "newIDE/electron-app/app/byok/buildSystemPrompt.js" - ], - "expected_output": [ - "newIDE/electron-app/app/byok/callLLM.js", - "newIDE/electron-app/app/byok/test-t03.cjs" - ], - "observability_impact": "", - "full_plan_md": "", - "sequence": 0, - "blocker_source": "", - "escalation_pending": 0, - "escalation_awaiting_review": 0, - "escalation_artifact_path": null, - "escalation_override_applied_at": null - }, - { - "milestone_id": "M001", - "slice_id": "S02", - "id": "T04", - "title": "Create preload.js, byokMain.js, and edit main.js for IPC wiring", - "status": "pending", - "one_liner": "", - "narrative": "", - "verification_result": "", - "duration": "", - "completed_at": null, - "blocker_discovered": false, - "deviations": "", - "known_issues": "", - "key_files": [], - "key_decisions": [], - "full_summary_md": "", - "description": "Create preload.js exposing window.byokAi via contextBridge with {callLLM, getConfig, saveConfig, getActiveRequests, abortRequest}.\n\nCreate byok/byokMain.js exporting registerByokHandlers(ipcMain) registering 5 IPC handlers: byok-call-llm, byok-get-config, byok-save-config, byok-get-active-requests, byok-abort-request.\n\nEdit main.js at three insertion points: (1) add require for byokMain.js, (2) add preload path to webPreferences, (3) call registerByokHandlers inside app.on('ready').\n\nCreate test-t04.cjs: verify preload.js API surface by parsing and checking exports structure.", - "estimate": "30m", - "files": [ - "newIDE/electron-app/app/preload.js", - "newIDE/electron-app/app/byok/byokMain.js", - "newIDE/electron-app/app/main.js", - "newIDE/electron-app/app/byok/test-t04.cjs" - ], - "verify": "node --test newIDE/electron-app/app/byok/test-t04.cjs", - "inputs": [ - "newIDE/electron-app/app/byok/callLLM.js", - "newIDE/electron-app/app/byok/byokConfig.js", - "newIDE/electron-app/app/byok/errors.js", - "newIDE/electron-app/app/main.js" - ], - "expected_output": [ - "newIDE/electron-app/app/preload.js", - "newIDE/electron-app/app/byok/byokMain.js", - "newIDE/electron-app/app/byok/test-t04.cjs" - ], - "observability_impact": "", - "full_plan_md": "", - "sequence": 0, - "blocker_source": "", - "escalation_pending": 0, - "escalation_awaiting_review": 0, - "escalation_artifact_path": null, - "escalation_override_applied_at": null - }, - { - "milestone_id": "M001", - "slice_id": "S02", - "id": "T05", - "title": "Verify IPC round-trip works end-to-end", - "status": "pending", - "one_liner": "", - "narrative": "", - "verification_result": "", - "duration": "", - "completed_at": null, - "blocker_discovered": false, - "deviations": "", - "known_issues": "", - "key_files": [], - "key_decisions": [], - "full_summary_md": "", - "description": "Launch the Electron app and verify that window.byokAi is available in the renderer DevTools console. Test IPC round-trip by calling window.byokAi.getConfig() and verifying it returns an empty config (no crash). Verify that window.byokAi has all 5 expected methods: callLLM, getConfig, saveConfig, getActiveRequests, abortRequest.\n\nDo: Build and launch the Electron app. Open DevTools in the renderer. Type window.byokAi in console and verify it's not undefined. Call window.byokAi.getConfig().then(console.log) and verify it returns an object without throwing.", - "estimate": "15m", - "files": [ - "newIDE/electron-app/app/main.js", - "newIDE/electron-app/app/preload.js", - "newIDE/electron-app/app/byok/byokMain.js" - ], - "verify": "Launch Electron app, check window.byokAi in DevTools", - "inputs": [ - "newIDE/electron-app/app/main.js", - "newIDE/electron-app/app/preload.js", - "newIDE/electron-app/app/byok/byokMain.js" - ], - "expected_output": [], - "observability_impact": "", - "full_plan_md": "", - "sequence": 0, - "blocker_source": "", - "escalation_pending": 0, - "escalation_awaiting_review": 0, - "escalation_artifact_path": null, - "escalation_override_applied_at": null - } - ], - "decisions": [], - "verification_evidence": [ - { - "id": 1, - "task_id": "T01", - "slice_id": "S01", - "milestone_id": "M001", - "command": "grep -q 'return true' newIDE/app/src/Utils/GDevelopServices/Usage.js && node -e \"...hasValidSubscriptionPlan...\"", - "exit_code": 1, - "verdict": "✅ pass (false negative exit code: both grep and includes() succeed; console.log falsy fallback triggers process.exit(1))", - "duration_ms": 120, - "created_at": "2026-05-19T00:56:54.164Z" - }, - { - "id": 2, - "task_id": "T02", - "slice_id": "S01", - "milestone_id": "M001", - "command": "node -e checks: getUserLimits present, 999999, FULL, no apiClient.get in getUserLimits, hasValidSubscriptionPlan returns true", - "exit_code": 0, - "verdict": "✅ pass", - "duration_ms": 49, - "created_at": "2026-05-19T00:59:08.327Z" - }, - { - "id": 3, - "task_id": "T02", - "slice_id": "S01", - "milestone_id": "M001", - "command": "node -e shape compatibility: all Limits top-level keys and capability sub-keys verified", - "exit_code": 0, - "verdict": "✅ pass", - "duration_ms": 54, - "created_at": "2026-05-19T00:59:08.327Z" - }, - { - "id": 4, - "task_id": "T03", - "slice_id": "S01", - "milestone_id": "M001", - "command": "rg \"showWatermark\" Core/GDCore/Project/Watermark.cpp", - "exit_code": 0, - "verdict": "✅ pass — constructor and UnserializeFrom both default to false", - "duration_ms": 85, - "created_at": "2026-05-19T01:00:35.442Z" - }, - { - "id": 5, - "task_id": "T03", - "slice_id": "S01", - "milestone_id": "M001", - "command": "rg \"return true\" newIDE/app/src/Utils/GDevelopServices/Usage.js", - "exit_code": 0, - "verdict": "✅ pass — hasValidSubscriptionPlan returns true (prior task)", - "duration_ms": 72, - "created_at": "2026-05-19T01:00:35.442Z" - }, - { - "id": 6, - "task_id": "T03", - "slice_id": "S01", - "milestone_id": "M001", - "command": "rg \"maximumCount: 999\" newIDE/app/src/Utils/GDevelopServices/Usage.js", - "exit_code": 0, - "verdict": "✅ pass — getUserLimits returns max limits (prior task)", - "duration_ms": 68, - "created_at": "2026-05-19T01:00:35.442Z" - }, - { - "id": 7, - "task_id": "T01", - "slice_id": "S02", - "milestone_id": "M001", - "command": "node --check newIDE/electron-app/app/byok/errors.js", - "exit_code": 0, - "verdict": "✅ pass", - "duration_ms": 150, - "created_at": "2026-05-19T01:37:41.411Z" - }, - { - "id": 8, - "task_id": "T01", - "slice_id": "S02", - "milestone_id": "M001", - "command": "node --check newIDE/electron-app/app/byok/buildSystemPrompt.js", - "exit_code": 0, - "verdict": "✅ pass", - "duration_ms": 140, - "created_at": "2026-05-19T01:37:41.411Z" - }, - { - "id": 9, - "task_id": "T01", - "slice_id": "S02", - "milestone_id": "M001", - "command": "node --test newIDE/electron-app/app/byok/test-t01.cjs", - "exit_code": 0, - "verdict": "✅ pass (19/19)", - "duration_ms": 66, - "created_at": "2026-05-19T01:37:41.411Z" - }, - { - "id": 10, - "task_id": "T02", - "slice_id": "S02", - "milestone_id": "M001", - "command": "node --check newIDE/electron-app/app/byok/requestStore.js && node --check newIDE/electron-app/app/byok/byokConfig.js", - "exit_code": 0, - "verdict": "✅ pass", - "duration_ms": 120, - "created_at": "2026-05-19T01:45:38.364Z" - }, - { - "id": 11, - "task_id": "T02", - "slice_id": "S02", - "milestone_id": "M001", - "command": "node --test newIDE/electron-app/app/byok/test-t02.cjs", - "exit_code": 0, - "verdict": "✅ pass (22/22)", - "duration_ms": 71, - "created_at": "2026-05-19T01:45:38.364Z" - }, - { - "id": 12, - "task_id": "T03", - "slice_id": "S02", - "milestone_id": "M001", - "command": "node --check newIDE/electron-app/app/byok/callLLM.js", - "exit_code": 0, - "verdict": "✅ pass", - "duration_ms": 80, - "created_at": "2026-05-19T01:58:02.695Z" - }, - { - "id": 13, - "task_id": "T03", - "slice_id": "S02", - "milestone_id": "M001", - "command": "node --test newIDE/electron-app/app/byok/test-t03.cjs", - "exit_code": 0, - "verdict": "✅ pass", - "duration_ms": 461, - "created_at": "2026-05-19T01:58:02.695Z" - }, - { - "id": 14, - "task_id": "T03", - "slice_id": "S02", - "milestone_id": "M001", - "command": "node --test newIDE/electron-app/app/byok/test-t01.cjs newIDE/electron-app/app/byok/test-t02.cjs", - "exit_code": 0, - "verdict": "✅ pass", - "duration_ms": 120, - "created_at": "2026-05-19T01:58:02.695Z" - } - ] -} \ No newline at end of file diff --git a/newIDE/app/src/Utils/GDevelopServices/Generation.js b/newIDE/app/src/Utils/GDevelopServices/Generation.js index dc1527530e..8abd39d927 100644 --- a/newIDE/app/src/Utils/GDevelopServices/Generation.js +++ b/newIDE/app/src/Utils/GDevelopServices/Generation.js @@ -975,9 +975,27 @@ export const fetchAiSettings = async ({ const response = await axios.get( `${GDevelopAiCdn.baseUrl[environment]}/ai-settings.json` ); - return ensureObjectHasProperty({ + const aiSettings = ensureObjectHasProperty({ data: response.data, propertyName: 'aiRequest', endpointName: '/ai-settings.json of Generation API', }); + + // D003: Inject BYOK (Bring Your Own Key) presets so users can route AI requests + // through their own API keys instead of the GDevelop Generation API. + // These presets share the same id ('byok') across all modes; the mode field + // distinguishes them so AiConfiguration.js and other downstream consumers + // can match the appropriate preset per mode. + const byokPreset = { + id: 'byok', + nameByLocale: { en: 'BYOK (Bring Your Own Key)' }, + disabled: false, + }; + aiSettings.aiRequest.presets.push( + { ...byokPreset, mode: 'chat' }, + { ...byokPreset, mode: 'agent' }, + { ...byokPreset, mode: 'orchestrator' } + ); + + return aiSettings; }; diff --git a/newIDE/electron-app/app/main.js b/newIDE/electron-app/app/main.js index 10cf01c423..d490bb06df 100644 --- a/newIDE/electron-app/app/main.js +++ b/newIDE/electron-app/app/main.js @@ -45,6 +45,7 @@ const { onLocalGDJSDevelopmentWatcherRuntimeUpdated, } = require('./LocalGDJSDevelopmentWatcher'); const { setupWatcher, disableWatcher } = require('./LocalFilesystemWatcher'); +const { registerByokHandlers } = require('./byok/byokMain'); // Initialize `@electron/remote` module require('@electron/remote/main').initialize(); @@ -152,6 +153,7 @@ function createNewWindow(windowArgs = args) { }, trafficLightPosition: { x: 12, y: 12 }, webPreferences: { + preload: path.join(__dirname, 'preload.js'), webSecurity: false, // Allow to access to local files, // Allow Node.js API access in renderer process, as long // as we've not removed dependency on it and on "@electron/remote". @@ -829,4 +831,6 @@ app.on('ready', function() { log.error('Failed to run npm script:', err); } }); + + registerByokHandlers(ipcMain); }); diff --git a/newIDE/electron-app/app/package-lock.json b/newIDE/electron-app/app/package-lock.json index e2e67cc317..3be52afc38 100644 --- a/newIDE/electron-app/app/package-lock.json +++ b/newIDE/electron-app/app/package-lock.json @@ -1,12 +1,12 @@ { "name": "gdevelop", - "version": "5.6.262", + "version": "5.6.269", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gdevelop", - "version": "5.6.262", + "version": "5.6.269", "license": "MIT", "dependencies": { "@electron/remote": "2.1.2",