@hanzlaa/rcode 4.12.0 → 4.13.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 (56) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +1 -1
  3. package/CONTRIBUTING.md +1 -0
  4. package/cli/doctor.js +40 -5
  5. package/cli/install.js +6 -1
  6. package/dist/rcode.js +87 -87
  7. package/package.json +1 -1
  8. package/rcode/agents/rcode-hussain-pm.md +37 -3
  9. package/rcode/agents/rcode-orchestrator.md +91 -0
  10. package/rcode/agents/rules/executor/correctness-hazard-scan.md +98 -0
  11. package/rcode/agents/rules/executor/execution-flow.md +8 -0
  12. package/rcode/agents/rules/executor/self-check.md +8 -0
  13. package/rcode/agents/rules/orchestrator/contract.md +76 -0
  14. package/rcode/agents/rules/sprint-checker/dimensions.md +38 -0
  15. package/rcode/agents/rules/verifier/reachability-check.md +45 -2
  16. package/rcode/bin/lib/progress.cjs +41 -13
  17. package/rcode/bin/lib/state-digest.cjs +88 -0
  18. package/rcode/bin/rcode-hooks.cjs +192 -23
  19. package/rcode/bin/rcode-tools.cjs +94 -4
  20. package/rcode/references/REFERENCES_INDEX.md +3 -1
  21. package/rcode/references/agent-shared-rules.md +87 -0
  22. package/rcode/references/code-reviewer-playbook.md +5 -0
  23. package/rcode/references/executor-playbook.md +2 -0
  24. package/rcode/references/github-comment-style.md +57 -0
  25. package/rcode/references/persona-executor-mode.md +61 -0
  26. package/rcode/references/response-style.md +21 -4
  27. package/rcode/references/verifier-playbook.md +14 -0
  28. package/rcode/skills/SKILLS_INDEX.md +1 -1
  29. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/references.md +7 -0
  30. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/rules/merge-strategy.md +19 -3
  31. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/templates/wave-prompt.md +3 -1
  32. package/rcode/skills/agents/{raees-orchestrator → orchestrator}/SKILL.md +1 -1
  33. package/rcode/team.yaml +20 -1
  34. package/rcode/templates/settings-hooks.json +12 -12
  35. package/rcode/workflows/audit-worktrees.md +15 -1
  36. package/rcode/workflows/enable-hooks.md +6 -0
  37. package/rcode/workflows/execute-verify-phase-goal.md +58 -2
  38. package/rcode/workflows/execute.md +37 -8
  39. package/rcode/workflows/plan-research-validation.md +8 -2
  40. package/rcode/workflows/plan-spawn-planner.md +32 -4
  41. package/rcode/workflows/plan.md +138 -10
  42. package/rcode/workflows/pr-branch.md +2 -0
  43. package/rcode/workflows/research-phase.md +12 -4
  44. package/rcode/workflows/ship.md +4 -0
  45. package/rcode/workflows/verify-phase.md +40 -0
  46. package/server/dashboard.js +57 -17
  47. package/server/lib/html/client/components/OrchPanel.js +6 -2
  48. package/server/lib/html/client/components/XtermPanel.js +7 -2
  49. package/server/lib/html/client/orchestrator.js +58 -21
  50. package/server/lib/html/client/views/MemoryView.js +59 -3
  51. package/server/lib/html/css.js +40 -0
  52. package/server/lib/html/shell.js +10 -4
  53. package/server/lib/scanner.js +150 -3
  54. package/server/lib/view-only.js +32 -0
  55. package/server/orchestrator.js +63 -4
  56. /package/rcode/skills/agents/{raees-orchestrator → orchestrator}/references.md +0 -0
@@ -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
- const PORT = parseInt(process.env.ORCH_PORT || '7718', 10);
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
- // Exit with code 2 so the dashboard knows this is a port-conflict (not a crash)
814
- // and can suppress the restart loop + print a one-time user hint.
815
- console.error(`[orch] port ${PORT} already in use. Set ORCH_PORT=<N> env var to use a different port. Exiting without retry.`);
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
  });