Repository navigation
Expand file tree
/
Copy pathindex.html
More file actions
263 lines (238 loc) · 15.4 KB
/
Copy pathindex.html
File metadata and controls
263 lines (238 loc) · 15.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>quadloop — persistent agent loop</title>
<meta name="description" content="quadloop is a persistent, human-in-the-loop agent harness: it keeps a Cursor LLM agent alive across long tasks and exposes it over four channels — Cursor, Telegram, a local-PC shell, and a browser Web UI.">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⟳</text></svg>">
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700;800&display=swap');
:root{
--bg:#fcfcfc; --text:#1a1a1a; --muted:#555; --light:#999;
--border:#e0e0e0; --surface:#f7f7f7;
--blue:#0984e3; --green:#00b894; --coral:#e17055; --purple:#6c5ce7; --teal:#00cec9;
--maxw:880px;
}
*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth}
body{
font-family:'JetBrains Mono',ui-monospace,monospace;
background:var(--bg);color:var(--text);
font-size:13px;line-height:1.8;font-weight:300;
max-width:var(--maxw);margin:0 auto;padding:0 24px 96px;
-webkit-font-smoothing:antialiased;
}
a{color:var(--blue);text-decoration:none;border-bottom:1px solid transparent}
a:hover{border-bottom-color:var(--blue)}
code{font-family:inherit;background:var(--surface);padding:1px 6px;border:1px solid var(--border)}
pre{font-family:inherit;background:var(--surface);border:1px solid var(--border);border-left:3px solid var(--teal);padding:16px 18px;overflow-x:auto;line-height:1.6;font-size:12.5px}
pre code{background:none;border:none;padding:0}
h1{font-size:1.6rem;font-weight:800;text-transform:uppercase;letter-spacing:-0.03em;line-height:1.2;margin:0}
h2{font-size:1.1rem;font-weight:700;text-transform:uppercase;letter-spacing:-0.01em;
border-bottom:2px solid var(--blue);padding-bottom:10px;margin:64px 0 20px}
h3{font-size:0.82rem;font-weight:700;text-transform:uppercase;color:var(--muted);margin:28px 0 8px}
h4{font-size:0.75rem;font-weight:700;text-transform:uppercase;color:var(--blue);margin:22px 0 6px;letter-spacing:0.02em}
p{color:var(--muted);font-weight:300}
p strong,li strong{color:var(--text);font-weight:500}
ul{list-style:none}
li{color:var(--muted);font-weight:300;padding-left:18px;position:relative}
li::before{content:"·";color:var(--light);position:absolute;left:4px;font-weight:700}
/* nav */
.site-nav{display:flex;gap:1.5rem;align-items:center;border-bottom:1px solid var(--border);padding:22px 0;margin-bottom:48px}
.site-nav .brand{font-weight:800;letter-spacing:-0.03em;text-transform:uppercase;font-size:1rem;color:var(--text)}
.site-nav .brand a{border:none}
.site-nav .spacer{flex:1}
.site-nav a{color:var(--muted);font-size:0.78rem;text-transform:uppercase;letter-spacing:0.04em;border:none}
.site-nav a:hover{color:var(--blue);border:none}
.site-nav .gh{color:var(--purple)}
/* hero */
.hero{margin-bottom:8px}
.hero h1{font-size:2.4rem;margin-bottom:14px}
.hero .tag{font-size:0.92rem;color:var(--muted);font-weight:400;max-width:640px;margin-bottom:28px;line-height:1.7}
.stats{display:flex;gap:0;border:1px solid var(--border);border-left:3px solid var(--purple);background:var(--surface);margin:24px 0 8px}
.stats .stat{flex:1;padding:14px 16px;border-right:1px solid var(--border)}
.stats .stat:last-child{border-right:none}
.stats .n{font-size:1.3rem;font-weight:800;color:var(--blue);line-height:1}
.stats .l{font-size:0.68rem;text-transform:uppercase;color:var(--light);letter-spacing:0.06em;margin-top:6px}
/* components */
.card{background:var(--surface);border:1px solid var(--border);border-left:3px solid var(--purple);padding:18px 20px;margin:14px 0}
.highlight{background:#fef5f3;border:1px solid #f3d6cc;border-left:4px solid var(--coral);padding:18px 20px;margin:14px 0}
.highlight p{color:#7a3b2e}
.example{border:1px solid var(--border);border-left:3px solid var(--teal);padding:16px 18px;margin:14px 0;background:var(--bg)}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin:18px 0}
@media(max-width:640px){.grid{grid-template-columns:1fr}.stats{flex-direction:column}.stats .stat{border-right:none;border-bottom:1px solid var(--border)}.stats .stat:last-child{border-bottom:none}}
/* diagram */
.diagram{border:1px solid var(--border);background:var(--surface);padding:26px 18px;margin:18px 0}
.flow{display:flex;flex-direction:column;align-items:center;gap:0}
.box{border:1px solid var(--border);background:var(--bg);padding:12px 18px;text-align:center;min-width:180px;font-size:0.78rem}
.box .lab{font-weight:700;color:var(--text);text-transform:uppercase;letter-spacing:0.02em;font-size:0.72rem}
.box .sub{color:var(--light);font-size:0.66rem;margin-top:3px}
.box.bus{border-left:3px solid var(--purple);min-width:300px}
.box.brain{border-left:3px solid var(--blue)}
.arrow{color:var(--light);font-size:0.8rem;padding:6px 0;font-weight:700}
.row{display:flex;gap:10px;justify-content:center;flex-wrap:wrap}
.channels{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:18px 0}
@media(max-width:640px){.channels{grid-template-columns:1fr 1fr}}
.ch{border:1px solid var(--border);border-top:3px solid var(--teal);background:var(--bg);padding:14px 12px;text-align:center}
.ch .ico{font-size:1.2rem}
.ch .name{font-weight:700;text-transform:uppercase;font-size:0.72rem;letter-spacing:0.03em;margin-top:6px}
.ch .desc{color:var(--light);font-size:0.66rem;margin-top:3px;line-height:1.5}
/* table */
table{width:100%;border-collapse:collapse;margin:18px 0;font-size:0.78rem}
th,td{text-align:left;border-bottom:1px solid var(--border);padding:9px 12px;vertical-align:top}
th{text-transform:uppercase;font-size:0.68rem;color:var(--light);font-weight:700;letter-spacing:0.05em;border-bottom:2px solid var(--blue)}
td{color:var(--muted);font-weight:300}
td code{font-size:0.74rem}
tr:hover td{color:var(--text)}
/* rules */
.rule{border:1px solid var(--border);border-left:4px solid var(--green);background:var(--surface);padding:14px 18px;margin:12px 0}
.rule .k{font-weight:700;color:var(--green);text-transform:uppercase;font-size:0.7rem;letter-spacing:0.04em}
.rule p{margin-top:4px}
footer{margin-top:64px;padding-top:24px;border-top:1px solid var(--border);color:var(--light);font-size:0.74rem;text-align:center}
footer a{color:var(--muted)}
.kicker{text-transform:uppercase;font-size:0.7rem;color:var(--blue);letter-spacing:0.08em;font-weight:700;margin-bottom:10px}
</style>
</head>
<body>
<nav class="site-nav">
<span class="brand"><a href="#top">⟳ quadloop</a></span>
<span class="spacer"></span>
<a href="#how">how it works</a>
<a href="#channels">channels</a>
<a href="#install">install</a>
<a href="#cli">cli</a>
<a href="#faq">faq</a>
<a class="gh" href="https://github.com/triggeredcode/quadloop">github ↗</a>
</nav>
<header class="hero" id="top">
<div class="kicker">persistent agent harness · quad-channel</div>
<h1>quadloop</h1>
<p class="tag">A local-first runtime that keeps a Cursor LLM agent <strong>alive across long tasks</strong>
and exposes it over four channels — Cursor chat, Telegram, a local-PC shell, and a browser
Web UI — with up to 5 parallel sessions and a script-enforced acknowledgment gate. It is
<strong>not an agent</strong>; it is the I/O substrate, persistence layer, and keep-alive loop.</p>
<div class="stats">
<div class="stat"><div class="n">4</div><div class="l">channels</div></div>
<div class="stat"><div class="n">5</div><div class="l">sessions</div></div>
<div class="stat"><div class="n">0</div><div class="l">dependencies</div></div>
<div class="stat"><div class="n"><1s</div><div class="l">web ui push</div></div>
</div>
</header>
<h2 id="why">why</h2>
<div class="highlight">
<p>AI coding agents tend to <strong>end their turn and collapse</strong> the moment a task
finishes or a question goes unanswered — exactly when you'd want them to keep working, ask, or
wait. quadloop flips that: the loop only ends when <strong>you</strong> say so, and every
significant update goes through a <strong>blocking acknowledgment gate</strong> so the agent
can't race ahead of your decisions.</p>
</div>
<h2 id="how">how it works</h2>
<p>The agent (in Cursor) drives the loop by issuing one-shot <code>quadloop</code> shell commands.
Three long-lived local processes serve it. All coordination is <strong>filesystem IPC</strong>
under <code>~/.cache/quadloop/</code> — mailbox files, an append-only JSONL log under
<code>fcntl.flock</code>, no daemon, sub-millisecond on one machine.</p>
<div class="diagram">
<div class="flow">
<div class="box brain">
<div class="lab">Cursor LLM</div>
<div class="sub">the brain · makes all decisions</div>
</div>
<div class="arrow">│ shell tool calls</div>
<div class="box">
<div class="lab">quadloop CLI</div>
<div class="sub">init · check · send · ack · exec · pc</div>
</div>
<div class="arrow">│ reads / writes</div>
<div class="box bus">
<div class="lab">filesystem IPC bus · ~/.cache/quadloop</div>
<div class="sub">sessions.json · active/sN.mailbox · message_log.jsonl · outbox_index.jsonl</div>
</div>
<div class="arrow">│</div>
<div class="row">
<div class="box"><div class="lab">_poller.py</div><div class="sub">Telegram long-poll</div></div>
<div class="box"><div class="lab">_web.py</div><div class="sub">HTTP + SSE · :8787</div></div>
<div class="box"><div class="lab">_broadcast.py</div><div class="sub">quad-channel fanout</div></div>
</div>
</div>
</div>
<p>Outbound fanout writes the log entry synchronously (so the Web UI sees it in one SSE tick),
then dispatches Telegram on a daemon thread and records the outgoing <code>message_id</code> in
the outbox so future replies route back to the same session.</p>
<h2 id="channels">the four channels</h2>
<div class="channels">
<div class="ch"><div class="ico">💬</div><div class="name">Cursor</div><div class="desc">the agent's home; talks via shell tool calls</div></div>
<div class="ch"><div class="ico">✈️</div><div class="name">Telegram</div><div class="desc">inbound long-poll · outbound async HTTP</div></div>
<div class="ch"><div class="ico">⌨️</div><div class="name">Local PC</div><div class="desc">quadloop pc · !cmd / exec in session cwd</div></div>
<div class="ch"><div class="ico">🌐</div><div class="name">Web UI</div><div class="desc">HTTP + SSE · multi-pane · light/dark</div></div>
</div>
<p>Routing precedence keeps the right message in the right session:
<strong>reply-to → /sN prefix → sticky → active pane → disambiguation</strong>. The native macOS
app is a WebKit wrapper around the Web UI plus a real <code>UNUserNotificationCenter</code>
forwarder — not a fifth transport.</p>
<h2 id="rules">the persistent loop</h2>
<p>Persistence is <strong>prompt-enforced, not code-enforced</strong>. The runtime stays small,
deterministic, and LLM-free; the agent owns the judgment of when to poll, wait, or end.</p>
<div class="rule"><span class="k">Rule A · never end the turn</span><p>The agent keeps making tool calls (even just <code>quadloop check</code>) to stay alive. The session only ends on <code>/done</code> or "done".</p></div>
<div class="rule"><span class="k">Rule B · the ack gate blocks</span><p><code>quadloop ack</code> sends, then blocks the shell up to 60s polling for your reply (45s nudge). The agent waits by construction, not by memory.</p></div>
<div class="rule"><span class="k">Rule C · formatting</span><p>Short, well-formatted messages. The web renderer treats underscore-bearing tokens as code, so the agent formats accordingly.</p></div>
<h2 id="install">install</h2>
<p>Python 3.9+ (stdlib only). A Telegram bot from <a href="https://t.me/BotFather">@BotFather</a>
and your chat id. macOS only if you want the native app.</p>
<pre><code>git clone https://github.com/triggeredcode/quadloop.git
cd quadloop
./scripts/setup.sh # symlink onto PATH, make cache dir, check creds
export QUADLOOP_DIR="$PWD" # so the macOS app's Scripts menu finds scripts/
export TG_BOT_TOKEN="..." # from @BotFather
export TG_CHAT_ID="..." # your chat id
quadloop init --alias "fix-login-bug"
open http://localhost:8787</code></pre>
<p>Or install as a Cursor skill: clone to <code>~/.cursor/skills/quadloop</code> — the bundled
<a href="https://github.com/triggeredcode/quadloop/blob/main/SKILL.md">SKILL.md</a> auto-loads the
persistent-loop protocol. Full steps in the
<a href="https://github.com/triggeredcode/quadloop/blob/main/README.md">README</a>.</p>
<h2 id="cli">cli quick reference</h2>
<table>
<thead><tr><th>command</th><th>what it does</th></tr></thead>
<tbody>
<tr><td><code>init</code></td><td>create a session (+ poller + web)</td></tr>
<tr><td><code>init-env</code></td><td>like init, but prints <code>eval</code> exports</td></tr>
<tr><td><code>check</code></td><td>read new inbound; keep the loop alive</td></tr>
<tr><td><code>ack "msg"</code></td><td>send + block ≤60s for the human's reply</td></tr>
<tr><td><code>send "msg"</code></td><td>fan out to all channels</td></tr>
<tr><td><code>pc "text"</code></td><td>append a local-PC note (no network)</td></tr>
<tr><td><code>exec</code> / <code>!cmd</code></td><td>run shell in the session cwd, broadcast result</td></tr>
<tr><td><code>status</code> · <code>doctor</code></td><td>health report + auto-heal</td></tr>
<tr><td><code>web --bg</code></td><td>start the Web UI on :8787</td></tr>
<tr><td><code>supervise --daemon</code></td><td>watchdog with exponential backoff</td></tr>
<tr><td><code>stop</code></td><td>clean shutdown of poller + web</td></tr>
</tbody>
</table>
<p><code>quadloop --help</code> lists all ~30 subcommands. Helpers in
<a href="https://github.com/triggeredcode/quadloop/blob/main/scripts/README.md">scripts/</a>
cover install, build, restart, and diagnosis.</p>
<h2 id="features">features</h2>
<div class="grid">
<div class="card"><h4>up to 5 sessions</h4><p>Isolated mailboxes, working dirs, colors, and per-channel notification toggles. A 6th init queues until a slot frees.</p></div>
<div class="card"><h4>sub-second web UI</h4><p>Multi-pane split/merge, light/dark, inline Markdown, image lightbox, video, draft persistence, typing indicators, read receipts.</p></div>
<div class="card"><h4>local mode</h4><p>Silence Telegram outbound while keeping web ↔ agent traffic and inbound from your phone.</p></div>
<div class="card"><h4>self-healing</h4><p>Heartbeat health states (🟢/🟡/🔴), <code>doctor</code> auto-heal, and a <code>supervise</code> watchdog with backoff.</p></div>
</div>
<h2 id="faq">faq</h2>
<h4>Is this an AI agent?</h4>
<p>No. quadloop is the <strong>harness + communication switchboard</strong>. The LLM (in Cursor)
is the agent/brain; quadloop gives it durable, multi-channel, human-supervised I/O and a
keep-alive loop.</p>
<h4>Does it phone home?</h4>
<p>No. The only network is the Telegram Bot API and loopback HTTP for the Web UI. State stays on
your machine in <code>~/.cache/quadloop/</code> — nothing is uploaded to a cloud vector store or
any analytics service.</p>
<h4>Can I use it without Cursor?</h4>
<p>Yes. Any caller that can run shell commands can drive <code>quadloop</code>. <code>SKILL.md</code>
is only needed if you want Cursor to auto-load the persistent-loop protocol.</p>
<footer>
quadloop · <a href="https://github.com/triggeredcode/quadloop">github.com/triggeredcode/quadloop</a>
· MIT license · this page is a showcase — the full docs live in the repo.
</footer>
</body>
</html>