@hanzlaa/rcode 4.12.1 → 4.14.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.
- package/AGENTS.md +1 -1
- package/CLAUDE.md +1 -1
- package/CONTRIBUTING.md +1 -0
- package/cli/doctor.js +40 -5
- package/cli/install.js +6 -1
- package/dist/rcode.js +87 -87
- package/package.json +1 -1
- package/rcode/agents/rcode-hussain-pm.md +37 -3
- package/rcode/agents/rcode-orchestrator.md +91 -0
- package/rcode/agents/rcode-project-researcher.md +19 -1
- package/rcode/agents/rules/executor/correctness-hazard-scan.md +98 -0
- package/rcode/agents/rules/executor/execution-flow.md +8 -0
- package/rcode/agents/rules/executor/self-check.md +8 -0
- package/rcode/agents/rules/orchestrator/contract.md +76 -0
- package/rcode/agents/rules/sprint-checker/dimensions.md +38 -0
- package/rcode/agents/rules/verifier/reachability-check.md +45 -2
- package/rcode/bin/lib/progress.cjs +41 -13
- package/rcode/bin/lib/roadmap.cjs +62 -22
- package/rcode/bin/lib/state-digest.cjs +88 -0
- package/rcode/bin/rcode-hooks.cjs +192 -23
- package/rcode/bin/rcode-tools.cjs +278 -7
- package/rcode/references/REFERENCES_INDEX.md +3 -1
- package/rcode/references/agent-shared-rules.md +123 -0
- package/rcode/references/code-reviewer-playbook.md +5 -0
- package/rcode/references/executor-playbook.md +2 -0
- package/rcode/references/github-comment-style.md +57 -0
- package/rcode/references/persona-executor-mode.md +61 -0
- package/rcode/references/planner-playbook.md +11 -0
- package/rcode/references/questioning.md +100 -2
- package/rcode/references/response-style.md +21 -4
- package/rcode/references/roadmapper-playbook.md +14 -0
- package/rcode/references/verifier-playbook.md +26 -0
- package/rcode/skills/SKILLS_INDEX.md +1 -1
- package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/references.md +7 -0
- package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/rules/merge-strategy.md +19 -3
- package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/templates/wave-prompt.md +3 -1
- package/rcode/skills/agents/{raees-orchestrator → orchestrator}/SKILL.md +1 -1
- package/rcode/team.yaml +20 -1
- package/rcode/workflows/audit-worktrees.md +15 -1
- package/rcode/workflows/execute-verify-phase-goal.md +58 -2
- package/rcode/workflows/execute.md +43 -8
- package/rcode/workflows/new-project-define-requirements.md +36 -0
- package/rcode/workflows/new-project-research-decision.md +61 -1
- package/rcode/workflows/new-project.md +95 -6
- package/rcode/workflows/plan-research-validation.md +8 -2
- package/rcode/workflows/plan-spawn-planner.md +32 -4
- package/rcode/workflows/plan.md +208 -18
- package/rcode/workflows/pr-branch.md +2 -0
- package/rcode/workflows/research-phase.md +12 -4
- package/rcode/workflows/resume-work.md +18 -0
- package/rcode/workflows/ship.md +4 -0
- package/rcode/workflows/verify-phase.md +40 -0
- package/server/dashboard.js +57 -17
- package/server/lib/html/client/components/OrchPanel.js +6 -2
- package/server/lib/html/client/components/XtermPanel.js +7 -2
- package/server/lib/html/client/orchestrator.js +58 -21
- package/server/lib/html/client/views/MemoryView.js +59 -3
- package/server/lib/html/css.js +40 -0
- package/server/lib/html/shell.js +10 -4
- package/server/lib/scanner.js +150 -3
- package/server/lib/view-only.js +32 -0
- package/server/orchestrator.js +63 -4
- /package/rcode/skills/agents/{raees-orchestrator → orchestrator}/references.md +0 -0
package/server/orchestrator.js
CHANGED
|
@@ -46,7 +46,10 @@ try { pty = require('@lydell/node-pty'); } catch { /* handled in handleRun */ }
|
|
|
46
46
|
let WebSocketServer = null;
|
|
47
47
|
try { ({ WebSocketServer } = require('ws')); } catch { /* handled at boot */ }
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
// #1037 — ORCH_PORT is only the STARTING point for this process's own
|
|
50
|
+
// free-port scan (see the listen/retry block near the bottom), not a fixed
|
|
51
|
+
// bind target. `let` because a busy starting port bumps this upward.
|
|
52
|
+
let PORT = parseInt(process.env.ORCH_PORT || '7718', 10);
|
|
50
53
|
// Use the project root passed by the dashboard (RCODE_DIR → parent, or explicit
|
|
51
54
|
// PROJECT_ROOT env var). Fall back to cwd so standalone orchestrator runs work.
|
|
52
55
|
// NEVER use __dirname-relative path — that resolves to the npm package dir when
|
|
@@ -56,6 +59,11 @@ const PROJECT_ROOT = process.env.PROJECT_ROOT
|
|
|
56
59
|
|| process.cwd();
|
|
57
60
|
const CLAUDE_BIN = process.env.CLAUDE_BIN || 'claude';
|
|
58
61
|
|
|
62
|
+
// #967 — view-only mode gate. See server/lib/view-only.js for rationale:
|
|
63
|
+
// the refusal has to be server-side because hiding the Run button doesn't
|
|
64
|
+
// stop a script or misdirected agent from calling POST /api/run directly.
|
|
65
|
+
const { isViewOnly } = require('./lib/view-only');
|
|
66
|
+
|
|
59
67
|
// Per-session auth token — see authed(). The dashboard passes ORCH_TOKEN in
|
|
60
68
|
// via env; standalone runs generate one and print it on boot.
|
|
61
69
|
const AUTH_TOKEN = process.env.ORCH_TOKEN || crypto.randomBytes(24).toString('hex');
|
|
@@ -330,6 +338,28 @@ function authed(req) {
|
|
|
330
338
|
return crypto.timingSafeEqual(a, b);
|
|
331
339
|
}
|
|
332
340
|
|
|
341
|
+
// #1037 — a request's declared PROJECT_ROOT must match this orchestrator's
|
|
342
|
+
// own, so a client pointed at the wrong orchestrator instance (stale cached
|
|
343
|
+
// port, a second dashboard's browser tab, a hand-crafted request) is rejected
|
|
344
|
+
// instead of silently driving another project's repository. The dashboard's
|
|
345
|
+
// bundled client always sends this header; a request missing it is treated
|
|
346
|
+
// the same as a mismatch — fail closed, not fail open.
|
|
347
|
+
function projectRootMatches(req) {
|
|
348
|
+
const declared = req.headers && req.headers['x-project-root'];
|
|
349
|
+
return typeof declared === 'string' && declared === PROJECT_ROOT;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Same check as projectRootMatches(), but for the WebSocket upgrade path,
|
|
353
|
+
// where the browser cannot set a custom header — the root travels as a query
|
|
354
|
+
// param instead (same reason the auth token does — see authed()).
|
|
355
|
+
function projectRootMatchesUpgrade(req) {
|
|
356
|
+
const url = req.url || '';
|
|
357
|
+
const qIdx = url.indexOf('?');
|
|
358
|
+
if (qIdx === -1) return false;
|
|
359
|
+
const declared = new URLSearchParams(url.slice(qIdx + 1)).get('root');
|
|
360
|
+
return typeof declared === 'string' && declared === PROJECT_ROOT;
|
|
361
|
+
}
|
|
362
|
+
|
|
333
363
|
function validStoryId(id) {
|
|
334
364
|
return typeof id === 'string'
|
|
335
365
|
&& id.length > 0
|
|
@@ -498,6 +528,12 @@ function handleHistory(res) {
|
|
|
498
528
|
}
|
|
499
529
|
|
|
500
530
|
async function handleRun(req, res) {
|
|
531
|
+
// #967 — refused before any body parsing / spawn work: view-only mode is
|
|
532
|
+
// a hard server-side gate, not a UI convenience.
|
|
533
|
+
if (isViewOnly(PROJECT_ROOT)) {
|
|
534
|
+
json(res, 403, { error: 'view-only mode is enabled (dashboard.view_only) — runs are disabled' });
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
501
537
|
const body = await parseBody(req);
|
|
502
538
|
const storyId = String(body.storyId || '').trim();
|
|
503
539
|
if (!validStoryId(storyId)) { json(res, 400, { error: 'invalid storyId' }); return; }
|
|
@@ -776,6 +812,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
776
812
|
}
|
|
777
813
|
|
|
778
814
|
if (!authed(req)) { json(res, 401, { error: 'unauthorized' }); return; }
|
|
815
|
+
// #1037 — reject any request whose declared PROJECT_ROOT doesn't match ours.
|
|
816
|
+
if (!projectRootMatches(req)) { json(res, 403, { error: 'project root mismatch' }); return; }
|
|
779
817
|
|
|
780
818
|
if (method === 'GET' && pathOnly === '/api/status') { json(res, 200, { ok: true, sessions: sessions.size }); return; }
|
|
781
819
|
if (method === 'GET' && pathOnly === '/api/runners') { await handleRunners(res); return; }
|
|
@@ -802,20 +840,38 @@ if (WebSocketServer) {
|
|
|
802
840
|
const url = req.url || '';
|
|
803
841
|
const pathOnly = url.indexOf('?') === -1 ? url : url.slice(0, url.indexOf('?'));
|
|
804
842
|
if (!pathOnly.startsWith('/ws/') || !authed(req)) { socket.destroy(); return; }
|
|
843
|
+
if (!projectRootMatchesUpgrade(req)) { socket.destroy(); return; }
|
|
805
844
|
const storyId = decodeURIComponent(pathOnly.slice('/ws/'.length));
|
|
806
845
|
if (!validStoryId(storyId)) { socket.destroy(); return; }
|
|
807
846
|
wss.handleUpgrade(req, socket, head, ws => attachWebSocket(ws, storyId));
|
|
808
847
|
});
|
|
809
848
|
}
|
|
810
849
|
|
|
850
|
+
// #1037 — own free-port scan instead of exiting on the first conflict. The
|
|
851
|
+
// dashboard passes ORCH_PORT as a STARTING point (its own final bound port,
|
|
852
|
+
// not a fixed constant); if that's also taken (another orchestrator, a stray
|
|
853
|
+
// process) we walk upward rather than giving up immediately. Only once the
|
|
854
|
+
// scan is exhausted do we exit(2) so the dashboard's respawn loop stays off.
|
|
855
|
+
const MAX_PORT_ATTEMPTS = 20;
|
|
856
|
+
let portAttempts = 0;
|
|
857
|
+
|
|
811
858
|
server.on('error', err => {
|
|
859
|
+
if (err.code === 'EADDRINUSE' && portAttempts < MAX_PORT_ATTEMPTS) {
|
|
860
|
+
portAttempts++;
|
|
861
|
+
console.error(`[orch] port ${PORT} in use, trying ${PORT + 1}...`);
|
|
862
|
+
PORT++;
|
|
863
|
+
server.listen(PORT, '127.0.0.1');
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
812
866
|
if (err.code === 'EADDRINUSE') {
|
|
813
|
-
|
|
814
|
-
//
|
|
815
|
-
|
|
867
|
+
console.error(`[orch] no free port found after ${MAX_PORT_ATTEMPTS} attempts. Set ORCH_PORT=<N> env var to use a different range. Exiting.`);
|
|
868
|
+
// Tell the dashboard (if it spawned us over IPC) so it can serve
|
|
869
|
+
// orchPort: null instead of advertising a port nothing is listening on.
|
|
870
|
+
if (process.send) process.send({ type: 'orch-error', reason: 'no-free-port' });
|
|
816
871
|
process.exit(2);
|
|
817
872
|
}
|
|
818
873
|
console.error('[orch] server error:', err.message);
|
|
874
|
+
if (process.send) process.send({ type: 'orch-error', reason: err.message });
|
|
819
875
|
process.exit(1);
|
|
820
876
|
});
|
|
821
877
|
|
|
@@ -829,4 +885,7 @@ server.listen(PORT, '127.0.0.1', () => {
|
|
|
829
885
|
console.log(' WS: ' + (WebSocketServer ? 'ready' : 'ws MISSING'));
|
|
830
886
|
console.log(' POST /api/run GET /api/sessions GET /api/runners WS /ws/<id>');
|
|
831
887
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
888
|
+
// Report the FINAL bound port back to the dashboard over IPC, since a busy
|
|
889
|
+
// starting port means PORT may have moved during the scan above.
|
|
890
|
+
if (process.send) process.send({ type: 'orch-ready', port: PORT });
|
|
832
891
|
});
|
|
File without changes
|