// ==UserScript== // @name Ghost in the Loop // @namespace https://github.com/MShneur/ghost-in-the-loop // @version 8.0.0 // @description πŸ‘» AI workflow engine β€” auto-proceed, pipelines, personas, export, diagnostics, roadmap autopilot, handoff capsules. ChatGPT Β· Claude Β· Perplexity Β· Gemini Β· DeepSeek Β· Copilot Β· Grok Β· Manus + 13 more. // @author Michael S (CTRL-AI) β€” Architecture by Claude // @match https://chatgpt.com/* // @match https://chat.openai.com/* // @match https://www.perplexity.ai/* // @match https://gemini.google.com/* // @match https://chat.deepseek.com/* // @match https://copilot.microsoft.com/* // @match https://grok.com/* // @match https://claude.ai/* // @match https://manus.im/* // @match https://www.manus.im/* // @match https://chat.mistral.ai/* // @match https://kimi.com/* // @match https://www.kimi.com/* // @match https://kimi.moonshot.cn/* // @match https://chat.qwen.ai/* // @match https://meta.ai/* // @match https://www.meta.ai/* // @match https://poe.com/* // @match https://huggingface.co/chat* // @match https://you.com/* // @match https://pi.ai/* // @match https://chat.z.ai/* // @match https://genspark.ai/* // @match https://www.genspark.ai/* // @match https://chat.minimax.io/* // @match https://lmarena.ai/* // @match https://duck.ai/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_setClipboard // @grant GM_notification // @updateURL https://raw.githubusercontent.com/MShneur/ghost-in-the-loop/main/ghost-in-the-loop.user.js // @downloadURL https://raw.githubusercontent.com/MShneur/ghost-in-the-loop/main/ghost-in-the-loop.user.js // @run-at document-idle // @license AGPL-3.0 // ==/UserScript== (() => { 'use strict'; if (window.__GITL_V8__) return; window.__GITL_V8__ = true; /* ═══════════════════════════════════════════════════════════════ LAYER 0 β€” CONSTANTS ═══════════════════════════════════════════════════════════════ */ const VER = '8.0.0'; const SUPPORT_URL = 'https://github.com/sponsors/MShneur'; const REPORT_REPO = 'MShneur/ghost-in-the-loop'; // for pre-filled issue URL transport const REPORT_WORKER_URL = ''; // set to a relay endpoint to enable silent auto-submit; empty = disabled const SIGIL_PROCEED = '[[GITL::PROCEED]]'; const SIGIL_HALT = '[[GITL::HALT]]'; const LEGACY_PROCEED = 'PROCEED'; const LEGACY_HALT = 'SYSTEM_HALT'; const MIN_RESPONSE_LEN = 50; /* Send-confirmation watchdog (v7.1): after a send, generation must actually start within this window. Guards the "Enter swallowed by a notification focus-steal" failure where the script thinks it sent but the platform never began generating. */ const SEND_CONFIRM_MS = 9000; // grace for generation to begin (covers slow first-token) const SEND_MAX_RETRIES = 2; // re-fire attempts before pausing /* ═══════════════════════════════════════════════════════════════ LAYER 0.5 β€” BOOT SAFETY + TAB LOCK + FOCUS GUARD Fixes v7.0-alpha loading failures: race conditions, multi-tab conflicts, background token burn. Sources: Kimi Deep Dive, Software Architect GPT, HTML/CSS GPT ═══════════════════════════════════════════════════════════════ */ const GITL_TAB_ID = crypto.randomUUID?.() || `tab-${Date.now()}-${Math.random().toString(16).slice(2)}`; let _tabLockInterval = null; /* safeBoot: guarantees document.body exists before any DOM work. If body isn't ready, retries via rAF. Catches and logs boot errors. */ function safeBoot(fn) { const boot = () => { try { if (!document.body) { requestAnimationFrame(boot); return; } fn(); } catch (err) { console.error('[GITL] boot failed:', err); try { GM_setValue('lastBootError', JSON.stringify({ msg: String(err?.message||err), stack: String(err?.stack||''), at: new Date().toISOString() })); } catch(_){} } }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot, { once: true }); } else { boot(); } } /* Tab lock: prevents multi-tab race conditions. Only one GITL instance per conversation route can run the loop engine. Uses GM_getValue heartbeat with 8s expiry. */ function _tabLockKey() { return `gitl:lock:${location.hostname}:${location.pathname.split('/').slice(0,3).join('/')}`; } function claimTabLock() { const key = _tabLockKey(); const now = Date.now(); try { const raw = GM_getValue(key, null); const lock = raw ? JSON.parse(raw) : null; if (lock && lock.tabId !== GITL_TAB_ID && (now - lock.ts < 8000)) { return false; // another tab owns it } } catch(_){} GM_setValue(key, JSON.stringify({ tabId: GITL_TAB_ID, ts: now })); return true; } function releaseTabLock() { try { const key = _tabLockKey(); const raw = GM_getValue(key, null); if (raw) { const lock = JSON.parse(raw); if (lock.tabId === GITL_TAB_ID) GM_setValue(key, ''); } } catch(_){} } function startTabHeartbeat() { if (_tabLockInterval) clearInterval(_tabLockInterval); _tabLockInterval = setInterval(() => { if (!claimTabLock()) { // lost ownership β€” pause if running if (typeof GHOST !== 'undefined' && GHOST.loop.state === 'RUNNING') { GHOST.loop.state = 'PAUSED'; GHOST.loop.detail = '⚠ Tab lock lost β€” paused'; if (typeof render === 'function') render(); } } }, 5000); } /* Focus guard: prevents background tabs from burning tokens by auto-sending prompts while user isn't looking. */ function isTabSafeToAct() { if (!document.hasFocus()) return false; if (document.hidden) return false; return claimTabLock(); } /* Pre-send safety gate: called before every engineSend. Returns { ok, reason } */ function assertInteractionSafe() { if (!document.hasFocus() && typeof GHOST !== 'undefined' && GHOST.loop.state === 'RUNNING') { return { ok: false, reason: 'tab-not-focused' }; } if (!claimTabLock()) { return { ok: false, reason: 'tab-lock-held-by-other' }; } return { ok: true, reason: 'ok' }; } /* Cleanup on tab close */ if (typeof window !== 'undefined') { window.addEventListener('beforeunload', () => { releaseTabLock(); if (_tabLockInterval) clearInterval(_tabLockInterval); }); } /* ═══════════════════════════════════════════════════════════════ LAYER 0.7 β€” NETWORK INTERCEPTOR (S1) Captures AI responses from fetch/XHR streams BEFORE they hit the DOM. Supplements DOM-based detection β€” does NOT replace it. Sources: Gemini Phase 0, Kimi Deep Dive, DeepSeek cascade ═══════════════════════════════════════════════════════════════ */ const GITL_NET = { bus: new EventTarget(), lastChunk: '', lastComplete: '', capturedAt: 0, active: false, AI_ENDPOINTS: [ '/backend-api/conversation', // ChatGPT '/api/organizations', // Claude '/socket.io/', // Perplexity '/api/v1/chat/completions', // DeepSeek / OpenAI-compat '/chat/conversation', // HuggingChat '/api/chat', // Generic '/bard', // Gemini '/turn/', // Copilot ], _isChat(url) { if (!url) return false; const s = typeof url === 'string' ? url : url?.url || String(url); return this.AI_ENDPOINTS.some(ep => s.includes(ep)); }, _emit(raw, isDone) { if (raw === '[DONE]') isDone = true; this.lastChunk = raw; this.capturedAt = Date.now(); if (isDone) this.lastComplete = raw; this.bus.dispatchEvent(new CustomEvent('gitl:net', { detail: { raw, isDone, ts: Date.now() } })); }, install() { if (this.active) return; this.active = true; /* Fetch proxy β€” captures SSE / JSON streams */ const origFetch = window.fetch; const self = this; window.fetch = async function(...args) { const response = await origFetch.apply(this, args); if (self._isChat(args[0])) { try { const cloned = response.clone(); if (cloned.body) { const reader = cloned.body.getReader(); const decoder = new TextDecoder('utf-8'); (async () => { let buf = ''; try { while (true) { const { done, value } = await reader.read(); if (done) { if (buf) self._emit(buf, true); break; } buf += decoder.decode(value, { stream: true }); const lines = buf.split('\n'); buf = lines.pop() || ''; for (const line of lines) { const trimmed = line.trim(); if (trimmed.startsWith('data: ')) { self._emit(trimmed.slice(6), false); } } } } catch(_) { /* stream aborted β€” normal on navigation */ } })(); } } catch(err) { console.warn('[GITL] fetch intercept error:', err); } } return response; }; /* XHR proxy β€” fallback for platforms not using fetch */ const origOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method, url, ...rest) { this._gitlUrl = url; return origOpen.call(this, method, url, ...rest); }; const origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function(...args) { if (self._isChat(this._gitlUrl)) { this.addEventListener('load', function() { if (this.status >= 200 && this.status < 300 && this.responseText) { self._emit(this.responseText, true); } }); } return origSend.apply(this, args); }; console.log('[GITL] Network interceptor active'); } }; /* Install immediately β€” safe even before DOM */ GITL_NET.install(); /* ═══════════════════════════════════════════════════════════════ LAYER 1 β€” PLATFORM ADAPTERS (all DOM access lives here) The loop engine NEVER touches the DOM directly. ═══════════════════════════════════════════════════════════════ */ const PROFILES = { chatgpt: { host: /chatgpt\.com|chat\.openai\.com/, label: 'ChatGPT', input: ['#prompt-textarea','div[contenteditable="true"][id="prompt-textarea"]','textarea[data-id="root"]','textarea'], send: ['button[data-testid="send-button"]','button[aria-label="Send prompt"]','button[aria-label="Send"]','form button[type="submit"]','button[class*="send"]'], stop: ['button[aria-label="Stop generating"]','button[data-testid="stop-button"]'], assistant: ['div[data-message-author-role="assistant"]','article [data-message-author-role="assistant"]'], continueLabels: ['Continue generating','Continue'], useCE: false, useNS: true }, perplexity: { host: /perplexity\.ai/, label: 'Perplexity', input: ['textarea[placeholder*="Ask"]','textarea[placeholder*="Follow"]','div[contenteditable="true"][role="textbox"]','div[class*="ProseMirror"]','textarea:not([disabled])'], send: ['button[aria-label="Submit"]','button[aria-label="Send"]','button[type="submit"]'], stop: ['button[aria-label="Stop"]','[data-testid="stop-button"]'], assistant: ['div[class*="prose"]','div[dir="auto"][class*="break-words"]','.pb-md > div'], continueLabels: [], useCE: true, useNS: false }, gemini: { host: /gemini\.google\.com/, label: 'Gemini', input: ['div.ql-editor[contenteditable="true"]','rich-textarea div[contenteditable="true"]','div[contenteditable="true"]','textarea'], send: ['button[aria-label="Send message"]','button[aria-label*="Send"]','button.send-button'], stop: ['button[aria-label*="Stop"]'], assistant: ['model-response message-content','message-content','div[class*="model-response"]'], continueLabels: [], useCE: true, useNS: false }, deepseek: { host: /chat\.deepseek\.com/, label: 'DeepSeek', input: ['textarea[placeholder]','#chat-input','textarea'], send: ['div[class*="send"]','button[class*="send"]','button[aria-label*="Send"]'], stop: ['div[class*="stop"]','button[class*="stop"]'], assistant: ['div[class*="markdown"]'], continueLabels: [], useCE: false, useNS: false }, copilot: { host: /copilot\.microsoft\.com/, label: 'Copilot', input: ['textarea#userInput','#searchbox','textarea[placeholder*="message"]','textarea'], send: ['button[aria-label="Submit"]','button[title="Submit"]'], stop: ['button[aria-label="Stop Responding"]'], assistant: ['cib-message-group[source="bot"]'], continueLabels: [], useCE: false, useNS: false }, grok: { host: /grok\.com/, label: 'Grok', input: ['textarea[placeholder*="Ask"]','textarea','div[contenteditable="true"]'], send: ['button[aria-label="Send"]','button[type="submit"]'], stop: ['button[aria-label="Stop"]'], assistant: ['div[class*="message"][class*="bot"]','div[data-role="assistant"]'], continueLabels: [], useCE: false, useNS: false }, claude: { host: /claude\.ai/, label: 'Claude', input: ['div[contenteditable="true"].ProseMirror','div[contenteditable="true"][aria-label*="message"]','div.ProseMirror','div[contenteditable="true"]'], send: ['button[aria-label="Send Message"]','button[type="submit"]','button[aria-label*="Send"]'], stop: ['button[aria-label="Stop Response"]'], assistant: ['div[data-is-streaming]','div.font-claude-message','.claude-message'], continueLabels: [], useCE: true, useNS: false }, manus: { host: /manus\.im/, label: 'Manus', // Verified against real Manus DOM: Tiptap ProseMirror input; Monaco code viewer has a decoy
` : ``; const delBtn = wf.custom ? `` : ''; const wsBar = `
`; return `
${_esc(wf.desc)}
${!isManual ? `
How this works: press β–Ά Start below and Ghost runs all ${wf.stages.length} stages in order, moving to the next each time the AI says it's done. Or tap a single stage's INSERT to drop just that prompt into the chat yourself. ${GHOST.workflow.pauseBetween ? '

⏸ Pause between is ON β€” Ghost stops after each stage so you can review or switch models, then press β–Ά to continue.' : ''}
${running||GHOST.workflow.active ? `
` : ''}
Stage ${wf.stages.length?(GHOST.workflow.stageIndex+1):'β€”'} of ${wf.stages.length} ${GHOST.workflow.active?'Β· running':''}
${stages} ${delBtn} ` : stages} ${form}${wsBar}`; } /* Maps each tab to its help section so a per-tab ? deep-links correctly. */ const TAB_HELP = { run:'run', auto:'auto', flow:'flow', personas:'roles', export:'export', settings:'setup' }; const HELP_SECTIONS = { start: { label: 'Start', html: ` What is Ghost?
You give the AI a big task. Ghost keeps pressing "continue" for you β€” through every step β€” until the AI says it's truly done.

The 30-second version:
1. Type your task in the chat box
2. Press the big β–Ά
3. Walk away β˜•

How does it know when to stop?
Ghost teaches the AI two signals: [[GITL::PROCEED]] = "more to do", [[GITL::HALT]] = "finished". Ghost reads them and acts.` }, run: { label: 'Run', html: ` The Run tab is command center.

Strategy dropdown:
Β· Step by step β€” AI works in batches, Ghost continues each one
Β· Plan first β€” AI plans before working, then batches
Β· Autopilot β€” AI researches, writes its own plan, Ghost runs every step

Buttons: β–Ά start/resume Β· ⏸ pause Β· βŠ• reground (re-anchor AI to the original task if you see drift). Full stop is in Advanced β–Ύ.

Personas line: shows your active persona or committee. Tap "edit" to jump to the Personas tab.

Q: It stopped and shows "drift checkpoint"?
That's the drift guard catching a long run. It's a grounding pause so an unattended run cannot wander off-task. Three choices:
Β· β–Ά Continue β€” run more
Β· βŠ• Reground β€” re-anchor the AI to the task it started on
Β· βœ‹ Stop & wait β€” pause for your instructions
You can edit the cap inline, toggle the guard off, or tap ↻ to reset.` }, auto: { label: 'Auto', html: ` The Auto tab = fire & forget.

Roadmap (AI plans): pick Roadmap on Run, press β–Ά. The AI studies your task, writes a numbered plan, and Ghost executes every step + a final synthesis. Watch steps get βœ“ here.

Queue (you plan): write your own steps β€” one box each, οΌ‹ to add more β€” and hit β–Ά Run queue.

Q: Roadmap vs Workflow?
Workflow = you know the recipe, same stages every time.
Roadmap = the AI invents the plan for THIS task.
Example, "build a landing page": a workflow always runs draft→critique→refine; a roadmap might plan research→copy→HTML→styling→review, because that's what this task needed.` }, flow: { label: 'Flow', html: ` The Flow tab runs fixed multi-stage recipes (e.g. Draft → Critique → Polish).

To run one:
1. Pick a workflow from the dropdown
2. Type your task in the chat box
3. Press β–Ά Start workflow
Ghost runs every stage in order, advancing each time the AI HALTs.

INSERT button (the small vertical tab on each stage): drops just that one stage's prompt into the chat box, so you can run a single stage by hand instead of the whole sequence.

Pause between stages: OFF = Ghost runs start-to-finish. ON = Ghost stops after each stage so you can review β€” or switch the model (that's how Lens Relay works: swap model at each pause, press β–Ά to continue).` }, roles: { label: 'Personas', html: ` The Personas tab shapes how the AI approaches your task.

Basic: pick a persona from the dropdown β€” Red Team attacks the work, Researcher digs deep, Devil's Advocate challenges every claim. The persona framing is injected into your first prompt.

Committee mode (toggle at top): select multiple personas. The AI simulates all perspectives on every response, then synthesizes a consensus with disagreements preserved.

Per-task toggle: re-inject the committee framing on every step, not just the first.
Final review toggle: after all work completes, the committee does one final review pass before halting.

On Perplexity, Round Table becomes a REAL round table: switch models between turns, each model gives independent assessment naming who goes next.` }, export: { label: 'Export', html: ` Three buttons, three jobs:

⬇ Export β€” the full record. The whole conversation as a file (with πŸ’­ thinking logs). For archiving and reading.

🀝 Handoff β€” moving to another model? Ghost asks THIS AI to write a structured briefing in-chat (mission, decisions, failures, next steps). Paste it into the new model. The AI's own summary beats a raw transcript β€” decisions don't get buried.

πŸ›Ÿ Rescue β€” the chat is full, stuck, or won't respond, so you can't ask it anything. Ghost scrapes the state + last 10 messages verbatim + resumption instructions into a file. Paste into a fresh chat and keep going.

Working chat β†’ Handoff. Dead chat β†’ Rescue. Records β†’ Export.` }, setup: { label: 'Setup', html: ` The Setup tab:
Β· Max rounds β€” drift-guard cap on auto-continues
Β· Notify β€” desktop alert when done (great with β˜•)
Β· Position β€” corners, bottom bar, ▐ Dock (slim right-edge tab that never covers the chat), or ☰ Gold menu (the same slim tab on the left edge, opposite most sites' own menu, styled gold)
Β· Skin β€” Classic (current), New (Aurora glass), or Custom (upload your own .gitl skin)
Β· Accent β€” hue slider to tint the interface any color you want

πŸ”„ Re-detect (top of panel): if Ghost says it can't find the chat box β€” common after switching between the browser and the app, or between tabs β€” tap πŸ”„. It re-finds the input without reloading the page, so you don't have to hop between chats to wake it up.

Advanced β–Ύ hides the power tools: custom signal words, per-site selector overrides (Custom sites), and Diagnostics β†’ Probe, which live-tests Ghost's connection to the page β€” your first stop when a platform misbehaves.` }, posture: { label: 'Posture', html: ` Thinking posture = how much room the AI has to grow its own plan. You pick it up front, like a reasoning dial β€” Ghost never guesses. It works with any mode (Loop / Think / Roadmap).

Standard β€” locked. The AI does exactly the steps it declared, nothing more. Most predictable; best when you know the scope.

Evolving (a.k.a. adaptive) β€” the AI may add steps while working, but only when it hits a real blocker or a gap that would otherwise make it fail the goal β€” and it must justify each addition in one line. It can't wander into unrelated topics, and it stays under the drift-guard ceiling.

Extended (a.k.a. review) β€” the AI runs the plan locked, then does one gap-check at the end: what's missing or unanswered against the original goal. It fills only genuinely valuable holes, then stops. If nothing's missing, it says so and halts.

All three keep the drift guard as the hard ceiling β€” if the AI hits it, it stops and reports the biggest unresolved gap instead of padding. (Wording based on current best-practice research: OpenAI/Anthropic planning guidance, ReAct/Reflexion, Self-Refine, and agent guardrail patterns.)` }, workshop: { label: 'Workshop', html: ` Make Ghost yours β€” and share it.

Custom personas (Roles tab) and custom workflows (Flow tab) are yours to create. Tap οΌ‹ Create, give it a name and either a persona framing or one stage per line. Custom items show a β˜… and sit right beside the built-ins.

⬇ Export bundles all your custom personas + workflows into one .gitl.json file. ⬆ Import loads someone else's bundle β€” it only ever ADDS (your existing items and the built-ins are never overwritten; name clashes auto-rename).

🌐 Share with the community:
Β· Post your .gitl.json in GitHub Discussions: ghost-in-the-loop/discussions
Β· Or open an issue tagged workshop to suggest it for the built-in library

Good packs get folded into future releases so everyone benefits.` }, feedback: { label: 'Feedback', html: ` Found a bug? Have an idea?

Open an issue: github.com/MShneur/ghost-in-the-loop

Please include:
Β· Ghost version (v${VER}) and the platform
Β· What you did, what you expected, what happened
Β· Setup β†’ Advanced β†’ Diagnostics β†’ Probe output β€” it tells us exactly what Ghost can and can't see

⭐ A star on GitHub helps more people find Ghost.
β™‘ And if Ghost saved you real time: support its development β€” entirely optional, it stays free either way.` } }; function renderInfoTab() { const sec = GHOST.ui.helpSec || 'start'; const pills = Object.entries(HELP_SECTIONS).map(([k, s]) => ``).join(''); return `
${pills}
${HELP_SECTIONS[sec].html}
`; } function renderAutoTab() { const R = GHOST.roadmap; // Active roadmap β†’ live progress rows with βœ“ / β–Ά / Β· if (R.steps.length) { const rows = R.steps.map((s,i) => { const mark = i < R.index ? 'βœ“' : i === R.index ? 'β–Ά' : 'Β·'; return `
${mark}${i+1}. ${s.replace(/
`; }).join(''); return `
πŸ—Ί ROADMAP β€” step ${Math.min(R.index+1,R.steps.length)} of ${R.steps.length}
${rows}
`; } // No roadmap β†’ step editor: one input per step, + to add const d = GHOST.ui.qDraft; const rows = d.map((s,i) => `
${i+1}.
`).join(''); return `
πŸ—Ί Autopilot. Pick Roadmap on the Run tab and press β–Ά β€” the AI plans this task itself. Or write your own steps below; each gets a βœ“ as it completes.
PROMPT QUEUE
${rows}
`; } function renderPersonasTab() { const sel = GHOST.persona.selected || ['none']; const comm = GHOST.persona.committee; const creating = GHOST.ui.wsNewPersona; const allP = allPersonas(); const activeP = sel.filter(s=>s&&s!=='none'); // Basic: single persona selector with preview const opts = Object.entries(allP).map(([k,v]) => ``).join(''); const curKey = !comm && activeP.length===1 ? activeP[0] : null; const curP = curKey ? allP[curKey] : null; // Committee: multi-select rows const committeeRows = comm ? activeP.map((k,i) => { const p = allP[k]; const rowOpts = Object.entries(allP).filter(([id])=>id!=='none').map(([id,v]) => ``).join(''); return `
${p?_esc(p.inject.slice(0,80))+(p.inject.length>80?'…':''):'Unknown persona'}
`; }).join('') : ''; return `
${!comm ? `
${curP ? `
${_esc(curP.label)}${curP.custom?' β˜… custom':''}
${_esc(curP.inject.slice(0,200))}${curP.inject.length>200?'…':''}
` : '
No persona active β€” the AI uses its default behavior.
'} ` : `
COMMITTEE MEMBERS (${activeP.length})
${committeeRows}
Per-task = each step runs with the committee framing. Final review = after the last step, the committee reviews and synthesizes.
`}
${creating ? `
` : ``}
`; } function renderExportTab() { const fn = buildFilename('export'); const adv = GHOST.ui.expAdv; return `
Export = full record. Handoff = the AI writes a briefing in-chat for the next model. Rescue = chat won't respond anymore β€” scrape the tail + instructions into a file for a fresh chat.
${adv ? `
${fn}
` : ''}`; } function renderSettingsTab() { const adv = GHOST.ui.cfgAdv; return `
${['top-left','top-right','bot-left','bot-right','bottom-bar','dock','dock-left'].map(p=> `` ).join('')}
${adv ? `
${GHOST.ui.showSites ? `
Per-host selector overrides (JSON). Also add the site under Tampermonkey β†’ script settings β†’ User matches.
` : ''}
${GHOST.ui.showDiag ? renderDiag() : ''}` : ''}
β™‘ Support Ghost Β· free forever
`; } function renderDiag() { const L = GHOST.loop; const h = typeof platformHealth === 'function' ? platformHealth() : null; const lines = [ h ? `Health: ${h.badge} ${h.score}/100 (in:${h.input?'βœ“':'βœ—'} send:${h.send?'βœ“':'βœ—'} read:${h.assistantCount} net:${h.netActive?'βœ“':'βœ—'})` : '', `Adapter: ${DIAG.adapter}`, `Platform: ${PLAT.label}`, `Selector: ${DIAG.selector || 'β€”'}`, `Send path: ${DIAG.sendPath || 'β€”'}`, `Signal: ${L.lastSignal} (${L.lastConfidence}) ${DIAG.lastSignal}`, `Tail: ${DIAG.lastTail ? DIAG.lastTail.slice(-50) : 'β€”'}`, `Round: ${L.round} / ${L.maxRounds}`, `State: ${L.state}`, `Stale: ${L.staleTicks}`, `Tick: ${L.lastActivity ? Math.round((Date.now()-L.lastActivity)/1000)+'s ago' : 'β€”'}`, `Tab: ${GITL_TAB_ID.slice(0,8)}`, DIAG.probe ? `Probe:\n${DIAG.probe}` : '', DIAG.errors.length ? `Errors:\n${DIAG.errors.slice(0,5).join('\n')}` : '' ].filter(Boolean).join('\n'); return `
${lines}
`; } function applyPosition(pos) { const G = '14px'; panel.style.top = panel.style.bottom = panel.style.left = panel.style.right = 'auto'; panel.style.width = '268px'; panel.classList.remove('pos-bb'); if (pos==='top-right'){panel.style.top=G;panel.style.right=G} else if(pos==='top-left'){panel.style.top=G;panel.style.left=G} else if(pos==='bot-right'){panel.style.bottom=G;panel.style.right=G} else if(pos==='bot-left'){panel.style.bottom=G;panel.style.left=G} else if(pos==='bottom-bar'){panel.classList.add('pos-bb')} else if(pos==='dock'){panel.style.top='30%';panel.style.right='0';panel.style.width=''} else if(pos==='dock-left'){panel.style.top='30%';panel.style.left='0';panel.style.width=''} } function renderReportBadge() { // v7.1: a report just landed β€” surface it. Switch to Run tab so the // banner is visible, then re-render. try { if (typeof GHOST === 'undefined' || !GHOST.ui) return; if (GHOST.report) GHOST.ui.tab = 'run'; if (typeof panel !== 'undefined' && panel) render(); } catch(_){} } function render() { const L = GHOST.loop, tab = GHOST.ui.tab, col = GHOST.ui.collapsed; const isDock = GHOST.ui.position==='dock' || GHOST.ui.position==='dock-left'; panel.className = [col?'collapsed':'', GHOST.ui.position==='bottom-bar'?'pos-bb':'', GHOST.ui.position==='dock'?'pos-dock':'', GHOST.ui.position==='dock-left'?'pos-dock pos-dock-left':''].filter(Boolean).join(' '); const qc = statColor(); const ql = L.state==='RUNNING'?'Running…':L.state==='LIMIT'?`β–Ά ${L.maxRounds} reached β€” tap for ${L.limitStep} more`:L.state==='PAUSED'?'Paused':L.state==='COMPLETE'?'Done':'Idle'; const qIcon = L.state==='RUNNING'?'⏸':'β–Ά'; const qCls = L.state==='RUNNING'?'pause':L.state==='LIMIT'?'play limit':'play'; // Compact dock status: step/round + drift guard remaining (editable) const dockStat = (()=>{ if (L.state==='IDLE'||L.state==='COMPLETE') return ''; const p = L.lastProgress; const line1 = p ? `${p.step}/${p.total}` : (L.round ? `R${L.round}` : ''); const left = L.driftEnabled ? Math.max(0, L.maxRounds - L.round) : null; const line2 = left !== null ? `${left}` : ''; return [line1,line2].filter(Boolean).join('
'); })(); panel.innerHTML = `
${(typeof platformHealth==='function'?platformHealth().badge:'') + ' ' + PLAT.label}
${ql} ${dockStat?`${dockStat}`:''}
πŸ“
${TAB_HELP[tab] && tab!=='info' ? `` : ''} ${tab==='run'?renderRunTab():''}${tab==='auto'?renderAutoTab():''}${tab==='info'?renderInfoTab():''}${tab==='flow'?renderFlowTab():''} ${tab==='personas'?renderPersonasTab():''}${tab==='export'?renderExportTab():''} ${tab==='settings'?renderSettingsTab():''}
`; bindEvents(); applyPosition(GHOST.ui.position); } /* ═══════════════════════════════════════════════════════════════ EVENT BINDING ═══════════════════════════════════════════════════════════════ */ function bindEvents() { const $ = s => panel.querySelector(s); const $$ = s => panel.querySelectorAll(s); $('#g-col')?.addEventListener('click', () => { GHOST.ui.collapsed=!GHOST.ui.collapsed; _save('panelCollapsed',GHOST.ui.collapsed); render(); }); // Docked + collapsed: the whole strip is the expand target (the play button stays play) if ((GHOST.ui.position==='dock' || GHOST.ui.position==='dock-left') && GHOST.ui.collapsed) { panel.addEventListener('click', e => { if (e.target.closest('#g-quick') || e.target.closest('#g-col')) return; GHOST.ui.collapsed = false; _save('panelCollapsed', false); render(); }, { once: true }); } $('#g-quick')?.addEventListener('click', primaryAction); $('#g-projname')?.addEventListener('change', e => { GHOST.project.name = e.target.value.trim(); GHOST.project.slug = GHOST.project.name.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,''); _save('projectName',GHOST.project.name); _save('projectSlug',GHOST.project.slug); if (GHOST.ui.tab==='export') render(); }); $$('.g-tab').forEach(b => b.addEventListener('click', () => { GHOST.ui.tab=b.dataset.t; render(); })); $('#g-tabhelp')?.addEventListener('click', function(){ GHOST.ui.prevTab = GHOST.ui.tab; GHOST.ui.helpSec = this.dataset.h; GHOST.ui.tab = 'info'; render(); }); // Run tab β€” strategy dropdown $$('.g-md').forEach(b => b.addEventListener('click', () => { if (GHOST.loop.state==='RUNNING') return; GHOST.loop.payloadMode=b.dataset.m; GHOST.loop.needsPayload=true; _save('payloadMode',GHOST.loop.payloadMode); render(); })); $('#g-strategy')?.addEventListener('change', e => { if (GHOST.loop.state==='RUNNING') return; GHOST.loop.payloadMode=e.target.value; GHOST.loop.needsPayload=true; _save('payloadMode',GHOST.loop.payloadMode); render(); }); $('#run-adv')?.addEventListener('click', () => { GHOST.ui.runAdv=!GHOST.ui.runAdv; render(); }); $('#g-goto-personas')?.addEventListener('click', e => { e.preventDefault(); GHOST.ui.tab='personas'; render(); }); $('#g-reground')?.addEventListener('click', () => { if (GHOST.loop.state==='RUNNING'||GHOST.loop.state==='PAUSED') regroundLoop(); }); $$('.g-pst').forEach(b => b.addEventListener('click', () => { if (GHOST.loop.state==='RUNNING') return; GHOST.loop.posture=b.dataset.pst; _save('posture',GHOST.loop.posture); render(); })); $('#g-posture-help')?.addEventListener('click', () => { GHOST.ui.prevTab=GHOST.ui.tab; GHOST.ui.helpSec='posture'; GHOST.ui.tab='info'; render(); }); $('#g-play')?.addEventListener('click', primaryAction); $('#g-limit-go')?.addEventListener('click', extendLimit); $('#g-limit-reground')?.addEventListener('click', regroundLoop); $('#g-limit-wait')?.addEventListener('click', () => enginePause('βœ‹ Stopped at drift checkpoint β€” β–Ά to resume')); $('#g-drift-tog')?.addEventListener('click', function(){ this.classList.toggle('on'); GHOST.loop.driftEnabled=this.classList.contains('on'); _save('driftEnabled',GHOST.loop.driftEnabled); render(); }); $('#g-drift-max')?.addEventListener('change', e => { const v=parseInt(e.target.value,10); if(v>0&&v<=999){GHOST.loop.maxRounds=v; _save('maxRounds',v); render();} }); $('#g-drift-max')?.addEventListener('click', e => e.stopPropagation()); $('#g-dk-max')?.addEventListener('change', e => { const v=parseInt(e.target.value,10); if(v>0&&v<=999){GHOST.loop.maxRounds=v; _save('maxRounds',v); render();} }); $('#g-dk-max')?.addEventListener('click', e => e.stopPropagation()); $('#g-dk-reset')?.addEventListener('click', e => { e.stopPropagation(); GHOST.loop.round=0; GHOST.loop.detail='↻ Drift guard reset'; render(); }); $('#g-cnt-reset')?.addEventListener('click', () => { GHOST.loop.round = 0; Timeline.record('drift_guard_reset', { cap: GHOST.loop.maxRounds }); GHOST.loop.detail = '↻ Drift guard reset'; render(); }); $('#g-pause')?.addEventListener('click', pauseLoop); $('#g-stop')?.addEventListener('click', stopLoop); $('#g-rep-copy')?.addEventListener('click', function(){ Reporter.copy().then(ok => { this.textContent = ok ? 'βœ“ Copied' : 'βœ• Failed'; setTimeout(()=>{ this.textContent='πŸ“‹ Copy'; }, 1500); }); }); $('#g-rep-issue')?.addEventListener('click', () => Reporter.openIssue()); $('#g-rep-x')?.addEventListener('click', () => { GHOST.report = null; Reporter.last = null; render(); }); $('#g-peek-btn')?.addEventListener('click', () => { const p=$('#g-peek'),b=$('#g-peek-btn'); if(p&&b){p.classList.toggle('open'); b.textContent=p.classList.contains('open')?'β–Ύ Hide prompt':'β–Έ What gets injected';} }); // Flow tab $('#wf-sel')?.addEventListener('change', e => { GHOST.workflow.selected=e.target.value; GHOST.workflow.stageIndex=0; GHOST.workflow.active=e.target.value!=='none'; _save('wfSelected',GHOST.workflow.selected); _save('wfStage',0); render(); }); $('#wf-pause')?.addEventListener('click', function(){ this.classList.toggle('on'); GHOST.workflow.pauseBetween=this.classList.contains('on'); _save('wfPause',GHOST.workflow.pauseBetween); render(); }); $('#wf-reset')?.addEventListener('click', () => { GHOST.workflow.stageIndex=0; GHOST.workflow.active=GHOST.workflow.selected!=='none'; _save('wfStage',0); render(); }); $('#wf-start')?.addEventListener('click', startWorkflow); $('#wf-do-pause')?.addEventListener('click', () => { if(GHOST.loop.state==='RUNNING') pauseLoop(); }); $('#wf-do-stop')?.addEventListener('click', () => { GHOST.workflow.active=false; GHOST.workflow.stageIndex=0; _save('wfStage',0); stopLoop(); }); $$('.g-wf-ins').forEach(b => b.addEventListener('click', () => { const wf = allWorkflows()[GHOST.workflow.selected] || WORKFLOW_LIBRARY.none; const stage = wf.stages[+b.dataset.ins]; if (stage) insertPrompt(stage, b); })); $('#ws-w-new')?.addEventListener('click', () => { GHOST.ui.wsNewWorkflow = true; render(); }); $('#ws-w-cancel')?.addEventListener('click', () => { GHOST.ui.wsNewWorkflow = false; render(); }); $('#ws-w-save')?.addEventListener('click', () => { const label = ($('#ws-w-label')?.value || '').trim(); const desc = ($('#ws-w-desc')?.value || '').trim(); const stages = ($('#ws-w-stages')?.value || '').split('\n').map(s => s.trim()).filter(s => s.length > 1); if (!label || !stages.length) { GHOST.loop.detail = '⚠ Name and at least one stage line are required'; render(); return; } const id = Workshop.addWorkflow(label, desc, stages); GHOST.ui.wsNewWorkflow = false; GHOST.workflow.selected = id; GHOST.workflow.stageIndex = 0; _save('wfSelected', id); _save('wfStage', 0); GHOST.loop.detail = `βœ“ Created workflow "${label}" (${stages.length} stages)`; render(); }); $('#ws-w-del')?.addEventListener('click', function(){ const id = GHOST.workflow.selected; if (this.dataset.confirm === '1') { Workshop.removeWorkflow(id); GHOST.workflow.selected = 'none'; GHOST.workflow.stageIndex = 0; GHOST.workflow.active = false; _save('wfSelected','none'); _save('wfStage',0); render(); } else { this.dataset.confirm = '1'; this.textContent = 'βœ• Tap again to confirm delete'; } }); // Personas tab const _saveSel = () => _save('persona', JSON.stringify(GHOST.persona.selected)); $('#p-comm-tog')?.addEventListener('click', function(){ this.classList.toggle('on'); GHOST.persona.committee=this.classList.contains('on'); _save('personaCommittee',GHOST.persona.committee); if(GHOST.persona.committee&&GHOST.persona.selected.filter(s=>s&&s!=='none').length<2){ GHOST.persona.selected=GHOST.persona.selected.filter(s=>s&&s!=='none'); if(!GHOST.persona.selected.length) GHOST.persona.selected=['researcher','redteam']; _saveSel(); } render(); }); $('#p-single')?.addEventListener('change', e => { GHOST.persona.selected=[e.target.value]; _saveSel(); render(); }); $('#p-run')?.addEventListener('click', () => { GHOST.ui.tab='run'; startLoop(); }); $('#p-pertask')?.addEventListener('click', function(){ this.classList.toggle('on'); GHOST.persona.perTask=this.classList.contains('on'); _save('personaPerTask',GHOST.persona.perTask); }); $('#p-review')?.addEventListener('click', function(){ this.classList.toggle('on'); GHOST.persona.finalReview=this.classList.contains('on'); _save('personaFinalReview',GHOST.persona.finalReview); }); // Committee multi-select rows $$('.g-cm-sel').forEach(sel => sel.addEventListener('change', e => { const i=+sel.dataset.ci; const active=GHOST.persona.selected.filter(s=>s&&s!=='none'); if(i b.addEventListener('click', () => { const i=+b.dataset.ci; const active=GHOST.persona.selected.filter(s=>s&&s!=='none'); active.splice(i,1); GHOST.persona.selected=active.length?active:['none']; _saveSel(); render(); })); $('#p-cm-add')?.addEventListener('click', () => { const active=GHOST.persona.selected.filter(s=>s&&s!=='none'); const all=Object.keys(allPersonas()).filter(k=>k!=='none'&&!active.includes(k)); if(all.length) active.push(all[0]); GHOST.persona.selected=active; _saveSel(); render(); }); // Workshop: create/import/export (same as before, updated for array) $('#ws-p-new')?.addEventListener('click', () => { GHOST.ui.wsNewPersona = true; render(); }); $('#ws-p-cancel')?.addEventListener('click', () => { GHOST.ui.wsNewPersona = false; render(); }); $('#ws-p-save')?.addEventListener('click', () => { const label = ($('#ws-p-label')?.value || '').trim(); const inject = ($('#ws-p-inject')?.value || '').trim(); if (!label || !inject) { GHOST.loop.detail = '⚠ Name and framing are both required'; render(); return; } const id = Workshop.addPersona(label, inject); GHOST.ui.wsNewPersona = false; if(GHOST.persona.committee){ GHOST.persona.selected.push(id); } else { GHOST.persona.selected=[id]; } _saveSel(); GHOST.loop.detail = `βœ“ Created persona "${label}"`; render(); }); $('#ws-import')?.addEventListener('click', workshopImport); $('#ws-export')?.addEventListener('click', workshopExport); $('#ws-submit')?.addEventListener('click', () => { GHOST.ui.prevTab = GHOST.ui.tab; GHOST.ui.helpSec = 'workshop'; GHOST.ui.tab = 'info'; render(); }); // Export tab $('#exp-fmt')?.addEventListener('change', e => { GHOST.export.format=e.target.value; _save('expFormat',e.target.value); render(); }); $('#exp-flt')?.addEventListener('change', e => { GHOST.export.filter=e.target.value; _save('expFilter',e.target.value); }); $('#exp-roles')?.addEventListener('click', function(){ this.classList.toggle('on'); GHOST.export.includeRoles=this.classList.contains('on'); _save('expRoles',GHOST.export.includeRoles); }); $('#exp-slug')?.addEventListener('change', e => { GHOST.export.customSlug=e.target.value.trim(); _save('expSlug',GHOST.export.customSlug); render(); }); $('#g-export')?.addEventListener('click', runExport); $('#g-capsule')?.addEventListener('click', () => { exportCapsuleV2(); }); $('#exp-think')?.addEventListener('click', function(){ this.classList.toggle('on'); GHOST.export.thinking=this.classList.contains('on'); _save('expThinking',GHOST.export.thinking); }); $('#g-handoff')?.addEventListener('click', handoffInChat); $('#g-rescue')?.addEventListener('click', exportRescue); $('#g-backup')?.addEventListener('click', backupConfig); $('#g-restore')?.addEventListener('click', () => $('#g-restore-file')?.click()); $('#g-restore-file')?.addEventListener('change', e => { const f = e.target.files?.[0]; if (!f) return; const r = new FileReader(); r.onload = () => { const st = $('#g-restore-status'); if (st) { st.style.display='block'; st.textContent = restoreConfig(String(r.result)); } }; r.readAsText(f); }); // Auto tab β€” roadmap / queue $$('.g-qin').forEach(inp => inp.addEventListener('change', e => { const i = +e.target.dataset.qi; GHOST.ui.qDraft[i] = e.target.value; _save('qDraft', JSON.stringify(GHOST.ui.qDraft)); })); $$('.g-qdel').forEach(b => b.addEventListener('click', e => { const i = +e.target.dataset.qd; GHOST.ui.qDraft.splice(i,1); if (!GHOST.ui.qDraft.length) GHOST.ui.qDraft = ['']; _save('qDraft', JSON.stringify(GHOST.ui.qDraft)); render(); })); $('#q-add')?.addEventListener('click', () => { GHOST.ui.qDraft.push(''); render(); setTimeout(()=>{ const ins=$$('.g-qin'); ins[ins.length-1]?.focus(); },50); }); $('#q-start')?.addEventListener('click', () => { const steps = GHOST.ui.qDraft.map(s=>s.trim()).filter(Boolean); if (steps.length) startQueue(steps.join('\n')); }); $('#rm-clear')?.addEventListener('click', () => { resetRoadmap(); render(); }); // Settings tab $('#cfg-max')?.addEventListener('change', e => { const v=parseInt(e.target.value,10); if(v>0&&v<=999){GHOST.loop.maxRounds=v; _save('maxRounds',v);} }); $('#cfg-win')?.addEventListener('change', e => { const v=parseInt(e.target.value,10); if(v>=200&&v<=1200){GHOST.signals.windowSize=v; _save('sigWindow',v);} }); $('#cfg-cp')?.addEventListener('change', e => { GHOST.signals.customProceed=e.target.value; _save('customProceed',e.target.value); }); $('#cfg-cs')?.addEventListener('change', e => { GHOST.signals.customStop=e.target.value; _save('customStop',e.target.value); }); $('#cfg-snd')?.addEventListener('click', function(){ this.classList.toggle('on'); GHOST.ui.soundOn=this.classList.contains('on'); _save('soundOn',GHOST.ui.soundOn); }); $('#cfg-ntf')?.addEventListener('click', function(){ this.classList.toggle('on'); GHOST.ui.notifyOn=this.classList.contains('on'); _save('notifyOn',GHOST.ui.notifyOn); if (GHOST.ui.notifyOn) { try { if (typeof Notification !== 'undefined' && Notification.permission === 'default') Notification.requestPermission(); } catch(_){} } }); $$('.g-pos').forEach(b => b.addEventListener('click', () => { GHOST.ui.position=b.dataset.pos; _save('panelPosition',GHOST.ui.position); applyPosition(GHOST.ui.position); render(); })); $('#cfg-diag')?.addEventListener('click', function(){ this.classList.toggle('on'); GHOST.ui.showDiag=this.classList.contains('on'); render(); }); $('#g-probe')?.addEventListener('click', () => { DIAG.runProbe(); render(); }); $('#g-report-now')?.addEventListener('click', () => { DIAG.runProbe(); Reporter.capture('manual', 'User-triggered problem report'); }); $('#cfg-sites-tog')?.addEventListener('click', function(){ this.classList.toggle('on'); GHOST.ui.showSites=this.classList.contains('on'); render(); }); $('#cfg-sites')?.addEventListener('change', e => { const raw = e.target.value.trim(), st = $('#cfg-sites-status'); if (!raw) { _save('customSites',''); if(st) st.textContent='Cleared. Reload the page to apply.'; return; } try { JSON.parse(raw); _save('customSites', raw); if(st) st.textContent='βœ“ Saved. Reload the page to apply.'; } catch(err) { if(st) st.textContent='⚠ Invalid JSON β€” not saved.'; } }); $('#cfg-qs')?.addEventListener('click', () => { GHOST.ui.firstRun=true; _save('firstRun',true); GHOST.ui.tab='run'; render(); }); $('#cfg-skin')?.addEventListener('change', e => { GHOST.ui.skinTheme=e.target.value; _save('skinTheme',GHOST.ui.skinTheme); render(); }); $('#cfg-hue')?.addEventListener('input', e => { GHOST.ui.accentHue=parseInt(e.target.value,10); _save('accentHue',GHOST.ui.accentHue); render(); }); $('#g-redetect')?.addEventListener('click', function(){ this.classList.add('spin'); const ok = reDetect(); setTimeout(() => this.classList.remove('spin'), 600); }); $('#g-info')?.addEventListener('click', () => { GHOST.ui.tab = GHOST.ui.tab==='info' ? 'run' : 'info'; render(); }); $('#g-info-back')?.addEventListener('click', () => { GHOST.ui.tab = GHOST.ui.prevTab || 'run'; GHOST.ui.prevTab = null; render(); }); $$('.g-hpill').forEach(b => b.addEventListener('click', e => { GHOST.ui.helpSec = e.target.dataset.h; render(); })); $('#cfg-adv')?.addEventListener('click', () => { GHOST.ui.cfgAdv=!GHOST.ui.cfgAdv; _save('cfgAdv',GHOST.ui.cfgAdv); render(); }); $('#exp-adv')?.addEventListener('click', () => { GHOST.ui.expAdv=!GHOST.ui.expAdv; _save('expAdv',GHOST.ui.expAdv); render(); }); $('#g-onb-done')?.addEventListener('click', () => { GHOST.ui.firstRun=false; _save('firstRun',false); render(); }); bindDrag(); } function bindDrag() { const hdr = panel.querySelector('#g-drag'); if (!hdr) return; let dragging=false, ox=0, oy=0; hdr.addEventListener('mousedown', e => { if(e.button!==0)return; dragging=true; ox=e.clientX-panel.getBoundingClientRect().left; oy=e.clientY-panel.getBoundingClientRect().top; e.preventDefault(); }); document.addEventListener('mousemove', e => { if(!dragging)return; panel.style.left=`${e.clientX-ox}px`; panel.style.top=`${e.clientY-oy}px`; panel.style.right='auto'; panel.style.bottom='auto'; }); document.addEventListener('mouseup', () => { dragging=false; }); } /* ═══════════════════════════════════════════════════════════════ KEYBOARD SHORTCUTS ═══════════════════════════════════════════════════════════════ */ document.addEventListener('keydown', e => { if(e.altKey&&e.key.toLowerCase()==='p'){e.preventDefault(); primaryAction();} if(e.altKey&&e.key.toLowerCase()==='s'){e.preventDefault(); stopLoop();} }); /* ═══════════════════════════════════════════════════════════════ MUTATION OBSERVER (gated by sendInProgress to prevent double-fire) ═══════════════════════════════════════════════════════════════ */ let _mutDebounce; /* ═══════════════════════════════════════════════════════════════ BOOT β€” wrapped in safeBoot to prevent v7.0-alpha loading failures ═══════════════════════════════════════════════════════════════ */ safeBoot(() => { // Observer watches childList (new nodes) AND a narrow set of attributes // (style/class/hidden), so a Continue button revealed via CSS β€” not just // one freshly inserted β€” also triggers the auto-click fast-path. // Loop tick (setInterval) remains the primary driver; this is a fast-path. new MutationObserver(() => { if (GHOST.loop.state !== 'RUNNING' || GHOST.loop.isSending) return; clearTimeout(_mutDebounce); _mutDebounce = setTimeout(() => { GHOST.loop.lastActivity = Date.now(); Adapter.clickContinue(); }, 300); }).observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class', 'hidden', 'disabled', 'aria-hidden'] }); startTabHeartbeat(); claimTabLock(); GhostBus.init(); Workshop.load(); injectStyles(); mountPanel(); render(); Timeline.record('boot', { version: VER, platform: PLAT.label, tab: GITL_TAB_ID.slice(0,8) }); console.log(`[Ghost in the Loop v${VER}] ${PLAT.label} | ${DIAG.adapter} | tab:${GITL_TAB_ID.slice(0,8)}`); }); })();