@hanzlaa/rcode 3.4.33 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/AGENTS.md +6 -6
  2. package/CONTRIBUTING.md +2 -0
  3. package/LICENSE +21 -0
  4. package/README.md +66 -403
  5. package/cli/doctor.js +87 -1
  6. package/cli/install.js +122 -31
  7. package/cli/lib/schemas.cjs +318 -0
  8. package/cli/postinstall.js +19 -3
  9. package/dist/rcode.js +316 -23
  10. package/package.json +14 -4
  11. package/rihal/agents/rihal-cross-platform-auditor.md +1 -1
  12. package/rihal/agents/rihal-dep-auditor.md +1 -1
  13. package/rihal/agents/rihal-docs-auditor.md +3 -145
  14. package/rihal/agents/rihal-i18n-auditor.md +1 -1
  15. package/rihal/agents/rihal-nyquist-auditor.md +4 -156
  16. package/rihal/agents/rihal-observability-auditor.md +1 -1
  17. package/rihal/bin/rihal-hooks.cjs +394 -4
  18. package/rihal/bin/rihal-tools.cjs +891 -24
  19. package/rihal/commands/create-prd.md +18 -0
  20. package/rihal/commands/execute-milestone.md +18 -0
  21. package/rihal/commands/plan-milestone.md +18 -0
  22. package/rihal/commands/scaffold-milestone.md +18 -0
  23. package/rihal/commands/scaffold-skill.md +18 -0
  24. package/rihal/references/REFERENCES_INDEX.md +49 -7
  25. package/rihal/references/agent-contracts.md +10 -0
  26. package/rihal/references/design-tokens.md +98 -0
  27. package/rihal/references/docs-auditor-playbook.md +148 -0
  28. package/rihal/references/git-preflight.md +117 -0
  29. package/rihal/references/iterative-retrieval.md +85 -0
  30. package/rihal/references/nyquist-auditor-playbook.md +157 -0
  31. package/rihal/references/workstream-flag.md +2 -2
  32. package/rihal/skills/actions/1-analysis/rihal-prfaq/SKILL.md +9 -0
  33. package/rihal/skills/actions/4-implementation/rihal-checkpoint-preview/SKILL.md +9 -0
  34. package/rihal/skills/actions/4-implementation/rihal-ci/SKILL.md +4 -0
  35. package/rihal/skills/actions/4-implementation/rihal-code-review/steps/step-02-review.md +2 -2
  36. package/rihal/skills/actions/4-implementation/rihal-harden/SKILL.md +4 -0
  37. package/rihal/skills/actions/4-implementation/rihal-migrate/SKILL.md +4 -0
  38. package/rihal/skills/agents/haitham-frontend/SKILL.md +2 -0
  39. package/rihal/templates/settings-hooks.json +39 -0
  40. package/rihal/workflows/check-todos.md +4 -0
  41. package/rihal/workflows/code-review-fix.md +4 -3
  42. package/rihal/workflows/code-review.md +1 -1
  43. package/rihal/workflows/debug.md +1 -1
  44. package/rihal/workflows/dev-story.md +4 -0
  45. package/rihal/workflows/diff.md +2 -2
  46. package/rihal/workflows/do.md +16 -8
  47. package/rihal/workflows/docs-update.md +2 -2
  48. package/rihal/workflows/enable-hooks.md +6 -1
  49. package/rihal/workflows/execute-milestone.md +139 -0
  50. package/rihal/workflows/execute-regression-gates.md +1 -1
  51. package/rihal/workflows/execute-sprint.md +54 -2
  52. package/rihal/workflows/execute-verify-phase-goal.md +31 -4
  53. package/rihal/workflows/execute-waves.md +33 -5
  54. package/rihal/workflows/execute.md +40 -6
  55. package/rihal/workflows/help.md +1 -1
  56. package/rihal/workflows/import.md +1 -1
  57. package/rihal/workflows/lens-audit.md +39 -23
  58. package/rihal/workflows/list-workspaces.md +1 -1
  59. package/rihal/workflows/map-codebase.md +4 -4
  60. package/rihal/workflows/new-milestone.md +18 -1
  61. package/rihal/workflows/new-project-research.md +53 -1
  62. package/rihal/workflows/new-workspace.md +1 -1
  63. package/rihal/workflows/plan-milestone.md +105 -0
  64. package/rihal/workflows/plan-research-validation.md +1 -1
  65. package/rihal/workflows/plan-spawn-planner.md +1 -1
  66. package/rihal/workflows/plan.md +31 -3
  67. package/rihal/workflows/plant-seed.md +6 -0
  68. package/rihal/workflows/quick.md +11 -5
  69. package/rihal/workflows/research-phase.md +24 -0
  70. package/rihal/workflows/scaffold-milestone.md +60 -0
  71. package/rihal/workflows/scaffold-skill.md +137 -0
  72. package/rihal/workflows/scan.md +1 -1
  73. package/rihal/workflows/session-report.md +43 -3
  74. package/rihal/workflows/verify-work.md +3 -3
  75. package/server/dashboard.js +154 -5
  76. package/server/lib/html/client/agents-data.js +27 -0
  77. package/server/lib/html/client/app.js +15 -0
  78. package/server/lib/html/client/components/App.js +211 -0
  79. package/server/lib/html/client/components/OrchPanel.js +293 -0
  80. package/server/lib/html/client/components/Sidebar.js +73 -0
  81. package/server/lib/html/client/components/Topbar.js +53 -0
  82. package/server/lib/html/client/components/XtermPanel.js +220 -0
  83. package/server/lib/html/client/components/shared.js +330 -0
  84. package/server/lib/html/client/icons-client.js +85 -0
  85. package/server/lib/html/client/orchestrator.js +279 -0
  86. package/server/lib/html/client/preact.js +34 -0
  87. package/server/lib/html/client/store.js +91 -0
  88. package/server/lib/html/client/util.js +186 -0
  89. package/server/lib/html/client/views/AgentsView.js +83 -0
  90. package/server/lib/html/client/views/DecisionsView.js +102 -0
  91. package/server/lib/html/client/views/FilesView.js +223 -0
  92. package/server/lib/html/client/views/KanbanView.js +236 -0
  93. package/server/lib/html/client/views/MemoryView.js +157 -0
  94. package/server/lib/html/client/views/MilestonesView.js +136 -0
  95. package/server/lib/html/client/views/OrchestrationView.js +167 -0
  96. package/server/lib/html/client/views/OverviewView.js +221 -0
  97. package/server/lib/html/client/views/PhasesView.js +184 -0
  98. package/server/lib/html/client/views/RoadmapView.js +238 -0
  99. package/server/lib/html/client/views/SprintsView.js +178 -0
  100. package/server/lib/html/client/views/TasksView.js +148 -0
  101. package/server/lib/html/client.js +42 -1064
  102. package/server/lib/html/css.js +2266 -466
  103. package/server/lib/html/icons.js +68 -0
  104. package/server/lib/html/shell.js +16 -210
  105. package/server/lib/scanner.js +109 -0
  106. package/server/orchestrator.js +362 -0
@@ -0,0 +1,362 @@
1
+ /**
2
+ * Rihal Local Orchestrator — port 7718
3
+ *
4
+ * Spawns interactive `claude` sessions inside a real pseudo-terminal
5
+ * (node-pty) and bridges each one to the browser over a WebSocket.
6
+ * The browser renders the raw terminal with xterm.js, so the session
7
+ * is fully interactive — the user types, Claude responds, just like a
8
+ * local terminal.
9
+ *
10
+ * HTTP (control plane):
11
+ * POST /api/run { storyId, cmd? } → spawn a PTY session
12
+ * POST /api/stop { storyId } → SIGTERM the PTY
13
+ * GET /api/sessions → list all sessions
14
+ * WebSocket (data plane):
15
+ * /ws/<storyId>?token=... → live terminal I/O
16
+ *
17
+ * Wire protocol (JSON each frame):
18
+ * server→client { t:'o', d } terminal output
19
+ * { t:'s', s } status change (running|done|exited|stopped|error)
20
+ * { t:'hist', d } scrollback replay on connect
21
+ * client→server { t:'i', d } keystroke input
22
+ * { t:'r', cols, rows } resize
23
+ */
24
+
25
+ 'use strict';
26
+
27
+ const http = require('http');
28
+ const path = require('path');
29
+ const crypto = require('crypto');
30
+ const { execFile } = require('child_process');
31
+
32
+ // @lydell/node-pty ships prebuilt binaries and never invokes node-gyp, so a
33
+ // plain `npm install` works on any common platform with no build toolchain.
34
+ // It is still an optionalDependency: on an unsupported platform the require
35
+ // throws, the orchestrator stays up, and /api/run reports a clear error
36
+ // instead of crashing — `npx rcode` keeps working everywhere.
37
+ let pty = null;
38
+ try { pty = require('@lydell/node-pty'); } catch { /* handled in handleRun */ }
39
+
40
+ let WebSocketServer = null;
41
+ try { ({ WebSocketServer } = require('ws')); } catch { /* handled at boot */ }
42
+
43
+ const PORT = parseInt(process.env.ORCH_PORT || '7718', 10);
44
+ const PROJECT_ROOT = path.resolve(__dirname, '..');
45
+ const CLAUDE_BIN = process.env.CLAUDE_BIN || 'claude';
46
+
47
+ // Per-session auth token — see authed(). The dashboard passes ORCH_TOKEN in
48
+ // via env; standalone runs generate one and print it on boot.
49
+ const AUTH_TOKEN = process.env.ORCH_TOKEN || crypto.randomBytes(24).toString('hex');
50
+
51
+ // storyId must be a safe single path segment — no separators, no traversal.
52
+ const STORY_ID_RE = /^[A-Za-z0-9._-]+$/;
53
+
54
+ // Command allowlist — the SECURITY BOUNDARY for the dashboard command runner.
55
+ // Only commands listed here may be launched via the UI command picker.
56
+ // Slash-commands that launch dev work (rihal-dev-story, rihal-execute, etc.)
57
+ // are NOT listed here; they are composed by the UI itself via storyId, not
58
+ // by the command runner. This list covers read-mostly and informational rihal
59
+ // slash-commands that are safe to run from the browser without further context.
60
+ const COMMAND_ALLOWLIST = new Set([
61
+ '/rihal-init',
62
+ '/rihal-status',
63
+ '/rihal-progress',
64
+ '/rihal-help',
65
+ '/rihal-health',
66
+ '/rihal-next',
67
+ '/rihal-show',
68
+ '/rihal-list-plans',
69
+ '/rihal-sprint-status',
70
+ '/rihal-config',
71
+ '/rihal-diff',
72
+ '/rihal-stats',
73
+ ]);
74
+
75
+ // Cap kept-in-memory scrollback per session so a long run can't grow unbounded.
76
+ const SCROLLBACK_MAX = 256 * 1024;
77
+
78
+ // Map<storyId, Session>
79
+ // Session: { proc, status, startTime, cmd, cols, rows, scrollback, wsClients:Set }
80
+ const sessions = new Map();
81
+
82
+ // ── helpers ──────────────────────────────────────────────────────────────────
83
+
84
+ function json(res, code, body) {
85
+ res.writeHead(code, { 'Content-Type': 'application/json' });
86
+ res.end(JSON.stringify(body));
87
+ }
88
+
89
+ // Constant-time token check. Token arrives as `Authorization: Bearer <t>`
90
+ // (HTTP) or `?token=<t>` (WebSocket upgrade — the browser cannot set
91
+ // headers on a WebSocket handshake).
92
+ function authed(req) {
93
+ let presented = null;
94
+ const auth = req.headers && req.headers.authorization;
95
+ if (auth && auth.startsWith('Bearer ')) {
96
+ presented = auth.slice('Bearer '.length);
97
+ } else {
98
+ const qIdx = (req.url || '').indexOf('?');
99
+ if (qIdx !== -1) {
100
+ presented = new URLSearchParams((req.url || '').slice(qIdx + 1)).get('token');
101
+ }
102
+ }
103
+ if (typeof presented !== 'string') return false;
104
+ const a = Buffer.from(presented);
105
+ const b = Buffer.from(AUTH_TOKEN);
106
+ if (a.length !== b.length) return false;
107
+ return crypto.timingSafeEqual(a, b);
108
+ }
109
+
110
+ function validStoryId(id) {
111
+ return typeof id === 'string'
112
+ && id.length > 0
113
+ && id.length <= 128
114
+ && !id.includes('..')
115
+ && STORY_ID_RE.test(id);
116
+ }
117
+
118
+ function parseBody(req) {
119
+ return new Promise(resolve => {
120
+ let buf = '';
121
+ req.on('data', c => buf += c);
122
+ req.on('end', () => { try { resolve(JSON.parse(buf)); } catch { resolve({}); } });
123
+ });
124
+ }
125
+
126
+ // Send one wire frame to every WebSocket client attached to a session.
127
+ function wsSend(s, obj) {
128
+ const payload = JSON.stringify(obj);
129
+ for (const ws of s.wsClients) {
130
+ try { ws.send(payload); } catch { s.wsClients.delete(ws); }
131
+ }
132
+ }
133
+
134
+ function setStatus(s, status) {
135
+ s.status = status;
136
+ wsSend(s, { t: 's', s: status });
137
+ }
138
+
139
+ // Set of working-tree files with uncommitted changes. A session's
140
+ // "files changed" is the current dirty set minus the set captured when it
141
+ // started — an estimate of what that session touched.
142
+ function gitModified() {
143
+ return new Promise(resolve => {
144
+ execFile('git', ['-C', PROJECT_ROOT, 'status', '--porcelain'],
145
+ { timeout: 5000 }, (err, stdout) => {
146
+ if (err) { resolve(new Set()); return; }
147
+ const set = new Set();
148
+ for (const line of String(stdout).split('\n')) {
149
+ const f = line.slice(3).trim();
150
+ if (f) set.add(f);
151
+ }
152
+ resolve(set);
153
+ });
154
+ });
155
+ }
156
+
157
+ // A running session that has produced no terminal output for this long is
158
+ // almost certainly waiting for the user (a question, or end of a turn).
159
+ const IDLE_THRESHOLD_MS = 20000;
160
+
161
+ // ── route handlers ────────────────────────────────────────────────────────────
162
+
163
+ async function handleSessions(res) {
164
+ const current = await gitModified();
165
+ const now = Date.now();
166
+ const out = [];
167
+ for (const [id, s] of sessions) {
168
+ const start = s.filesAtStart || new Set();
169
+ let changed = 0;
170
+ for (const f of current) if (!start.has(f)) changed++;
171
+ const idleMs = now - (s.lastDataAt || now);
172
+ out.push({
173
+ storyId: id,
174
+ status: s.status,
175
+ pid: s.proc ? s.proc.pid : null,
176
+ cmd: s.cmd,
177
+ startTime: s.startTime,
178
+ clients: s.wsClients.size,
179
+ filesChanged: changed,
180
+ idleSeconds: Math.floor(idleMs / 1000),
181
+ waiting: s.status === 'running' && idleMs > IDLE_THRESHOLD_MS,
182
+ });
183
+ }
184
+ json(res, 200, { sessions: out });
185
+ }
186
+
187
+ async function handleRun(req, res) {
188
+ const body = await parseBody(req);
189
+ const storyId = String(body.storyId || '').trim();
190
+ if (!validStoryId(storyId)) { json(res, 400, { error: 'invalid storyId' }); return; }
191
+
192
+ // Gate the allowlist on command-runner sessions only.
193
+ // Command-runner sessions always use a storyId with the "cmd-" prefix
194
+ // (e.g. "cmd-rihal-init"). Existing dev-run sessions use storyIds such as
195
+ // "phase-33", "sprint-33.1", or a raw task id — never "cmd-*" — and MUST NOT
196
+ // be gated here, even though they also supply body.cmd explicitly.
197
+ // This prefix check is the authoritative discriminant between the two call paths.
198
+ // NOTE: The gate fires for ANY cmd- storyId — a missing or empty body.cmd is
199
+ // also rejected. Previously the truthiness check on body.cmd allowed falsy values
200
+ // to bypass the allowlist and fall through to the /rihal-dev-story fallback.
201
+ if (storyId.startsWith('cmd-')) {
202
+ const reqCmd = typeof body.cmd === 'string' ? body.cmd.trim() : '';
203
+ if (!reqCmd || !COMMAND_ALLOWLIST.has(reqCmd)) {
204
+ json(res, 403, { error: 'command not in allowlist', cmd: reqCmd });
205
+ return;
206
+ }
207
+ }
208
+
209
+ if (!pty) {
210
+ json(res, 503, { error: 'interactive terminal unavailable on this platform — run: pnpm add @lydell/node-pty' });
211
+ return;
212
+ }
213
+
214
+ const existing = sessions.get(storyId);
215
+ if (existing && existing.status === 'running') {
216
+ json(res, 409, { error: 'already running', pid: existing.proc && existing.proc.pid });
217
+ return;
218
+ }
219
+ // Replacing a finished session — drop any sockets still attached.
220
+ if (existing) { for (const ws of existing.wsClients) { try { ws.close(); } catch {} } }
221
+
222
+ // Initial prompt. `claude [prompt]` starts an interactive session that
223
+ // processes the prompt, then waits for further input — exactly the
224
+ // run-then-communicate flow we want.
225
+ const cmd = String(body.cmd || `/rihal-dev-story ${storyId}`);
226
+ const cols = 120, rows = 30;
227
+
228
+ let proc;
229
+ try {
230
+ proc = pty.spawn(CLAUDE_BIN, [cmd, '--dangerously-skip-permissions'], {
231
+ name: 'xterm-color',
232
+ cols, rows,
233
+ cwd: PROJECT_ROOT,
234
+ env: process.env,
235
+ });
236
+ } catch (err) {
237
+ json(res, 500, { error: 'spawn failed: ' + err.message });
238
+ return;
239
+ }
240
+
241
+ const s = {
242
+ proc, status: 'running', cmd, cols, rows,
243
+ startTime: new Date().toISOString(),
244
+ lastDataAt: Date.now(),
245
+ scrollback: '',
246
+ wsClients: new Set(),
247
+ filesAtStart: new Set(),
248
+ };
249
+ sessions.set(storyId, s);
250
+ // Snapshot the dirty working tree so /api/sessions can report how many
251
+ // files this session has changed since it began.
252
+ gitModified().then(set => { s.filesAtStart = set; });
253
+
254
+ proc.onData(d => {
255
+ s.lastDataAt = Date.now();
256
+ s.scrollback += d;
257
+ if (s.scrollback.length > SCROLLBACK_MAX) {
258
+ s.scrollback = s.scrollback.slice(-SCROLLBACK_MAX);
259
+ }
260
+ wsSend(s, { t: 'o', d });
261
+ });
262
+
263
+ proc.onExit(({ exitCode, signal }) => {
264
+ const status = signal ? 'stopped' : (exitCode === 0 ? 'done' : 'exited');
265
+ setStatus(s, status);
266
+ });
267
+
268
+ json(res, 200, { storyId, pid: proc.pid, status: 'running' });
269
+ }
270
+
271
+ async function handleStop(req, res) {
272
+ const body = await parseBody(req);
273
+ const storyId = String(body.storyId || '').trim();
274
+ if (!validStoryId(storyId)) { json(res, 400, { error: 'invalid storyId' }); return; }
275
+ const s = sessions.get(storyId);
276
+ if (!s) { json(res, 404, { error: 'no session' }); return; }
277
+ try { s.proc.kill(); } catch {}
278
+ setStatus(s, 'stopped');
279
+ json(res, 200, { storyId, status: 'stopped' });
280
+ }
281
+
282
+ // ── WebSocket data plane ───────────────────────────────────────────────────────
283
+
284
+ function attachWebSocket(ws, storyId) {
285
+ const s = sessions.get(storyId);
286
+ if (!s) {
287
+ ws.send(JSON.stringify({ t: 's', s: 'error' }));
288
+ ws.close();
289
+ return;
290
+ }
291
+
292
+ s.wsClients.add(ws);
293
+ // Replay history so a late-joining client sees the session so far.
294
+ if (s.scrollback) ws.send(JSON.stringify({ t: 'hist', d: s.scrollback }));
295
+ ws.send(JSON.stringify({ t: 's', s: s.status }));
296
+
297
+ ws.on('message', raw => {
298
+ let msg;
299
+ try { msg = JSON.parse(raw.toString()); } catch { return; }
300
+ if (msg.t === 'i' && typeof msg.d === 'string' && s.status === 'running') {
301
+ try { s.proc.write(msg.d); } catch {}
302
+ } else if (msg.t === 'r' && s.status === 'running') {
303
+ const cols = parseInt(msg.cols, 10), rows = parseInt(msg.rows, 10);
304
+ if (cols > 0 && rows > 0) {
305
+ s.cols = cols; s.rows = rows;
306
+ try { s.proc.resize(cols, rows); } catch {}
307
+ }
308
+ }
309
+ });
310
+
311
+ ws.on('close', () => s.wsClients.delete(ws));
312
+ ws.on('error', () => s.wsClients.delete(ws));
313
+ }
314
+
315
+ // ── server ────────────────────────────────────────────────────────────────────
316
+
317
+ const server = http.createServer(async (req, res) => {
318
+ const method = req.method || '';
319
+ const url = req.url || '';
320
+
321
+ // CORS — the dashboard is served from a different port (7717), so every
322
+ // browser call here is cross-origin. The loopback bind + token are what
323
+ // gate access; a wildcard origin is safe with no cookies involved.
324
+ res.setHeader('Access-Control-Allow-Origin', '*');
325
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
326
+ res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type');
327
+ if (method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
328
+
329
+ if (!authed(req)) { json(res, 401, { error: 'unauthorized' }); return; }
330
+
331
+ const pathOnly = url.indexOf('?') === -1 ? url : url.slice(0, url.indexOf('?'));
332
+
333
+ if (method === 'GET' && pathOnly === '/api/sessions') { await handleSessions(res); return; }
334
+ if (method === 'POST' && pathOnly === '/api/run') { await handleRun(req, res); return; }
335
+ if (method === 'POST' && pathOnly === '/api/stop') { await handleStop(req, res); return; }
336
+
337
+ res.writeHead(404); res.end('Not found');
338
+ });
339
+
340
+ // WebSocket upgrade — authenticate, validate the storyId, then hand off.
341
+ if (WebSocketServer) {
342
+ const wss = new WebSocketServer({ noServer: true });
343
+ server.on('upgrade', (req, socket, head) => {
344
+ const url = req.url || '';
345
+ const pathOnly = url.indexOf('?') === -1 ? url : url.slice(0, url.indexOf('?'));
346
+ if (!pathOnly.startsWith('/ws/') || !authed(req)) { socket.destroy(); return; }
347
+ const storyId = decodeURIComponent(pathOnly.slice('/ws/'.length));
348
+ if (!validStoryId(storyId)) { socket.destroy(); return; }
349
+ wss.handleUpgrade(req, socket, head, ws => attachWebSocket(ws, storyId));
350
+ });
351
+ }
352
+
353
+ server.listen(PORT, '127.0.0.1', () => {
354
+ console.log('\n🤖 Rihal Orchestrator');
355
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
356
+ console.log(' Port: ' + PORT + ' (127.0.0.1, loopback only)');
357
+ console.log(' Token: ' + AUTH_TOKEN);
358
+ console.log(' PTY: ' + (pty ? 'node-pty ready' : 'node-pty MISSING'));
359
+ console.log(' WS: ' + (WebSocketServer ? 'ready' : 'ws MISSING'));
360
+ console.log(' POST /api/run GET /api/sessions WS /ws/<id>');
361
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
362
+ });