@hanzlaa/rcode 3.4.33 → 3.5.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 (80) 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 +8 -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 +52 -5
  76. package/server/lib/html/client.js +723 -11
  77. package/server/lib/html/css.js +2046 -466
  78. package/server/lib/html/shell.js +227 -134
  79. package/server/lib/scanner.js +33 -0
  80. package/server/orchestrator.js +438 -0
@@ -14,6 +14,10 @@ function renderClientJs(state) {
14
14
  last_session: state.raw?.last_session || null,
15
15
  chains: state.raw?.chains || [],
16
16
  workstreams: state.raw?.workstreams || [],
17
+ // #12 — passthrough scanner-computed fields (absent values stay undefined,
18
+ // both UI blocks below guard with `if (S.pendingHandoff)` / `if (S.memoryBank…)`).
19
+ pendingHandoff: state.pendingHandoff || null,
20
+ memoryBank: state.memoryBank || null,
17
21
  });
18
22
 
19
23
  return `<script>
@@ -316,11 +320,40 @@ function renderOverview() {
316
320
  chainsHtml += '</div></section>';
317
321
  }
318
322
 
323
+ // #12 — pending handoff banner (shown only when .rihal/HANDOFF.json present).
324
+ // Read-only — the dashboard never resumes; user runs /rihal-resume-work.
325
+ let handoffHtml = '';
326
+ if (S.pendingHandoff) {
327
+ const ho = S.pendingHandoff;
328
+ const when = ho.ts ? humanDate(ho.ts) : '';
329
+ const summary = ho.summary ? ' — ' + esc(ho.summary).slice(0, 120) : '';
330
+ const where = ho.sprint ? ' [sprint ' + esc(ho.sprint) + ']' :
331
+ ho.phase ? ' [phase ' + esc(ho.phase) + ']' : '';
332
+ handoffHtml = '<section style="border-left:4px solid var(--accent-orange,#f59e0b);padding-left:var(--space-3);">' +
333
+ '<h2>⚠ Pending Handoff</h2><div class="body">' +
334
+ '<div>' + (when ? esc(when) : '') + where + summary + '</div>' +
335
+ (ho.resume_hint ? '<div style="margin-top:var(--space-2);color:var(--text-secondary);font-size:var(--text-sm);">' + esc(ho.resume_hint) + '</div>' : '') +
336
+ '<div style="margin-top:var(--space-3);font-size:var(--text-sm);"><code>/rihal-resume-work</code></div>' +
337
+ '</div></section>';
338
+ }
339
+
340
+ // #12 — memory bank summary (shown only when .rihal/context/active.md present).
341
+ let memoryHtml = '';
342
+ if (S.memoryBank && S.memoryBank.active) {
343
+ const m = S.memoryBank.active;
344
+ memoryHtml = '<section><h2>🧠 Memory Bank</h2><div class="body">' +
345
+ '<div class="attr-grid">' +
346
+ attr('active.md', m.lines + ' lines · ' + Math.round(m.bytes / 1024 * 10) / 10 + ' KB') +
347
+ attr('Updated', humanDate(m.updated)) +
348
+ '</div></div></section>';
349
+ }
350
+
319
351
  const el = document.getElementById('view-overview-dynamic');
320
352
  // Overview hints
321
353
  var oHints = [cmdHint('/rihal-next', 'What should I do next?'), cmdHint('/rihal-status', 'Quick project status'), cmdHint('/rihal-council', 'Ask the team a question')];
322
354
  if (curSprint) { oHints = sprintHints(curSprint).concat(oHints); }
323
- if (el) el.innerHTML = sprintProgressHtml + velocityHtml + councilHtml + chainsHtml + lastSessionHtml + cmdAccordion(oHints);
355
+ if (S.pendingHandoff) { oHints.unshift(cmdHint('/rihal-resume-work', 'Resume from the pending handoff')); }
356
+ if (el) el.innerHTML = handoffHtml + sprintProgressHtml + memoryHtml + velocityHtml + councilHtml + chainsHtml + lastSessionHtml + cmdAccordion(oHints);
324
357
  }
325
358
 
326
359
  function renderRoadmap() {
@@ -482,8 +515,11 @@ function renderPhases(subId) {
482
515
  // #282: completed_at date
483
516
  (p.completed_at ? attr('Completed', humanDate(p.completed_at)) : '') + '</div></div>' +
484
517
  '<div style="margin-bottom:var(--space-4);">' + progressBar(done, stories.length) + '</div>' +
485
- // #283: View plan file button
486
- '<div style="margin-bottom:var(--space-6);"><button class="back-btn" onclick="viewPlanFile(\\'' + esc(p.id) + '\\')">📄 View plan file →</button></div>' +
518
+ '<div class="term-action-bar">' +
519
+ '<button class="term-run-btn" onclick="runAndOpenTerm(\\'phase-' + esc(p.id) + '\\',\\'/rihal-execute\\',\\'Phase ' + esc(p.id) + '\\')">▶ Run Phase</button>' +
520
+ '<button class="term-run-btn outline" onclick="openTermPanel(\\'phase-' + esc(p.id) + '\\',\\'Phase ' + esc(p.id) + '\\')">📟 Terminal</button>' +
521
+ '<button class="back-btn" onclick="viewPlanFile(\\\'' + esc(p.id) + '\\\')">📄 View plan file →</button>' +
522
+ '</div>' +
487
523
  velocityHtml +
488
524
  '<div class="view-title" style="margin-top:var(--space-6)">Sprints</div>' +
489
525
  '<div class="phase-list">' + (sps.length ? sps.map(s => sprintCard(Object.assign({},s,{phaseId:p.id,phaseName:p.name}))).join('') :
@@ -531,8 +567,12 @@ function renderSprints(subId) {
531
567
  (s.started_at ? attr('Started', humanDate(s.started_at)) : '') +
532
568
  (s.completed_at ? attr('Completed', humanDate(s.completed_at)) : '') + '</div></div>' +
533
569
  // #289: progress bar
534
- '<div style="margin-bottom:var(--space-6);">' + progressBar(done, stories.length) + '</div>' +
535
- '<div class="view-title" style="margin-top:var(--space-6)">Tasks</div>' +
570
+ '<div style="margin-bottom:var(--space-4);">' + progressBar(done, stories.length) + '</div>' +
571
+ '<div class="term-action-bar">' +
572
+ '<button class="term-run-btn" onclick="runAndOpenTerm(\\'sprint-' + esc(s.id) + '\\',\\'/rihal-execute-sprint ' + esc(s.id) + '\\',\\'Sprint ' + esc(s.id) + '\\')">▶ Run Sprint</button>' +
573
+ '<button class="term-run-btn outline" onclick="openTermPanel(\\'sprint-' + esc(s.id) + '\\',\\'Sprint ' + esc(s.id) + '\\')">📟 Terminal</button>' +
574
+ '</div>' +
575
+ '<div class="view-title" style="margin-top:var(--space-4)">Tasks</div>' +
536
576
  '<div class="phase-list">' + (stories.length ? stories.map(taskCard).join('') :
537
577
  '<div class="empty">No tasks in this sprint yet.<div class="empty-action">Run /rihal-create-story to add tasks</div></div>') + '</div>' +
538
578
  acHtml + cmdAccordion(sprintHints(s));
@@ -622,6 +662,512 @@ function sortTasks() {
622
662
  if (el) el.innerHTML = sort === 'default' ? renderTasksGrouped(tasks) : tasks.map(taskCard).join('');
623
663
  }
624
664
 
665
+ // ── Kanban + Orchestrator ────────────────────────────────────────────────────
666
+ // Run/Stop talk to orchestrator on :7718.
667
+ // Terminals render in the side panel (#orch-panel), not inline in cards.
668
+ var ORCH = 'http://localhost:7718';
669
+ var _orchStreams = {}; // Map<storyId, EventSource>
670
+ var _panelActive = null; // currently active storyId in panel
671
+ var _sessions = {}; // Map<storyId, { title, termEl, fileOpBuf[] }>
672
+
673
+ function kanbanCol(status) {
674
+ if (status === 'done' || status === 'completed') return 'done';
675
+ if (status === 'in_progress' || status === 'active' || status === 'running') return 'in_progress';
676
+ if (status === 'blocked') return 'blocked';
677
+ return 'todo';
678
+ }
679
+
680
+ // ── Kanban render ────────────────────────────────────────────────
681
+ function renderKanban() {
682
+ const el = document.getElementById('view-kanban');
683
+ if (!el) return;
684
+ const tasks = allTasks();
685
+ const cols = [
686
+ { id: 'todo', label: 'Todo', cssClass: 'col-todo' },
687
+ { id: 'in_progress', label: 'In Progress', cssClass: 'col-prog' },
688
+ { id: 'blocked', label: 'Blocked', cssClass: 'col-blocked' },
689
+ { id: 'done', label: 'Done', cssClass: 'col-done' },
690
+ ];
691
+ const buckets = { todo: [], in_progress: [], blocked: [], done: [] };
692
+ for (const t of tasks) buckets[kanbanCol(t.status)].push(t);
693
+
694
+ // Topbar
695
+ let h = '<div class="kanban-topbar">' +
696
+ '<div class="kanban-topbar-title">' +
697
+ '<span class="orch-status-dot" id="orch-dot"></span>' +
698
+ 'Kanban' +
699
+ '</div>' +
700
+ '<div class="kanban-topbar-actions">' +
701
+ '<button class="kanban-refresh-btn" onclick="refreshOrchestratorStatus()">⟳ Sync</button>' +
702
+ '<button class="kanban-refresh-btn" onclick="openOrchPanel(null)" style="margin-left:4px;">⊞ Sessions</button>' +
703
+ '</div>' +
704
+ '</div>';
705
+
706
+ if (!tasks.length) {
707
+ el.innerHTML = h + '<div class="empty" style="margin:24px;">' +
708
+ 'No stories yet.<div class="empty-action">/rihal-plan to generate tasks</div></div>';
709
+ return;
710
+ }
711
+
712
+ h += '<div class="kanban-board">';
713
+ for (const col of cols) {
714
+ const items = buckets[col.id];
715
+ h += '<div class="kanban-col ' + col.cssClass + '" data-col="' + col.id + '">' +
716
+ '<div class="kanban-col-head">' +
717
+ '<span class="col-label"><span class="col-status-dot"></span>' + esc(col.label) + '</span>' +
718
+ '<span class="kanban-count">' + items.length + '</span>' +
719
+ '</div>' +
720
+ '<div class="kanban-col-body">';
721
+ for (const t of items) {
722
+ const c = kanbanCol(t.status);
723
+ const sid = esc(t.id || '');
724
+ const canRun = c === 'todo' || c === 'blocked';
725
+ const isRunning = c === 'in_progress';
726
+ const pts = t.points ? t.points + 'p' : null;
727
+ const phase = t.phaseId ? 'P' + t.phaseId : null;
728
+ const sprintMeta = [pts, phase].filter(Boolean).join(' · ');
729
+ const actionBtn = sid
730
+ ? (canRun
731
+ ? '<button class="kanban-run-btn" data-action="run">▶ Run</button>'
732
+ : isRunning
733
+ ? '<button class="kanban-stop-btn" data-action="stop">■ Stop</button>' +
734
+ '<button class="kanban-view-btn" data-action="view">↗ View</button>'
735
+ : '<button class="kanban-view-btn" data-action="view">↗ Logs</button>')
736
+ : '';
737
+ h += '<div class="kanban-card s-' + c + (isRunning ? ' running' : '') +
738
+ '" data-story-id="' + sid + '" draggable="true">' +
739
+ '<div class="kanban-card-header">' +
740
+ '<div class="kanban-card-title">' + esc(t.title || t.id || 'Untitled') + '</div>' +
741
+ (sid ? '<div class="kanban-card-id">' + sid.slice(0, 8) + '</div>' : '') +
742
+ '</div>' +
743
+ (sprintMeta ? '<div class="kanban-card-meta">' +
744
+ '<span class="kanban-card-sprint">' + esc(sprintMeta) + '</span>' +
745
+ '<span class="kanban-card-status">' + esc(col.label) + '</span>' +
746
+ '</div>' : '') +
747
+ (isRunning ? '<div class="card-run-indicator" id="run-ind-' + sid + '">' +
748
+ '<span class="run-pulse"></span>running' +
749
+ '</div>' : '') +
750
+ (actionBtn ? '<div class="kanban-card-actions">' + actionBtn + '</div>' : '') +
751
+ '</div>';
752
+ }
753
+ h += '</div></div>'; // .kanban-col-body + .kanban-col
754
+ }
755
+ h += '</div>'; // .kanban-board
756
+
757
+ el.innerHTML = h;
758
+ wireKanbanDnd();
759
+ refreshOrchestratorStatus();
760
+ }
761
+
762
+ function getCard(sid) { return document.querySelector('[data-story-id="' + sid + '"]'); }
763
+
764
+ // ── Orchestrator panel ───────────────────────────────────────────
765
+
766
+ function _ensureSession(storyId, title) {
767
+ if (_sessions[storyId]) return _sessions[storyId];
768
+ var termEl = document.createElement('div');
769
+ termEl.style.cssText = 'padding:16px 20px;font-family:var(--font-mono);font-size:12px;line-height:1.6;min-height:100%;';
770
+ _sessions[storyId] = { title: title || storyId, termEl, fileOpBuf: [] };
771
+ return _sessions[storyId];
772
+ }
773
+
774
+ function createPanelTab(storyId, title) {
775
+ _ensureSession(storyId, title);
776
+ var tabs = document.getElementById('orch-tabs');
777
+ if (!tabs) return;
778
+ // Remove placeholder
779
+ var ph = tabs.querySelector('.orch-term-empty');
780
+ if (ph) ph.remove();
781
+ if (document.getElementById('orch-tab-' + storyId)) return;
782
+ var tab = document.createElement('button');
783
+ tab.className = 'orch-tab';
784
+ tab.id = 'orch-tab-' + storyId;
785
+ tab.dataset.storyId = storyId;
786
+ var shortTitle = (title || storyId).slice(0, 20);
787
+ tab.innerHTML =
788
+ '<span class="tab-status-dot starting" id="tdot-' + storyId + '"></span>' +
789
+ '<span>' + esc(shortTitle) + '</span>' +
790
+ '<button class="orch-tab-close" data-sid="' + storyId + '" title="Close">✕</button>';
791
+ tab.onclick = function(e) {
792
+ if (e.target.dataset.sid) { closePanelTab(e.target.dataset.sid); return; }
793
+ activatePanelTab(storyId);
794
+ };
795
+ tabs.appendChild(tab);
796
+ }
797
+
798
+ function activatePanelTab(storyId) {
799
+ document.querySelectorAll('.orch-tab').forEach(function(t) { t.classList.remove('active'); });
800
+ var tab = document.getElementById('orch-tab-' + storyId);
801
+ if (tab) tab.classList.add('active');
802
+ _panelActive = storyId;
803
+
804
+ var body = document.getElementById('orch-term-body');
805
+ var sess = _sessions[storyId];
806
+ if (body) {
807
+ body.innerHTML = '';
808
+ if (sess) {
809
+ body.appendChild(sess.termEl);
810
+ body.scrollTop = body.scrollHeight;
811
+ }
812
+ }
813
+
814
+ var filesEl = document.getElementById('orch-files');
815
+ if (filesEl && sess) {
816
+ filesEl.style.display = sess.fileOpBuf.length ? '' : 'none';
817
+ while (filesEl.children.length > 1) filesEl.removeChild(filesEl.lastChild);
818
+ sess.fileOpBuf.forEach(function(fo) { filesEl.appendChild(_renderFileOpEl(fo)); });
819
+ }
820
+
821
+ var stopBtn = document.getElementById('orch-stop-btn');
822
+ if (stopBtn) stopBtn.style.display = _orchStreams[storyId] ? '' : 'none';
823
+
824
+ var statusEl = document.getElementById('orch-session-status');
825
+ if (statusEl) {
826
+ var running = Object.keys(_orchStreams).length;
827
+ statusEl.textContent = running > 0 ? running + ' running' : '';
828
+ }
829
+ }
830
+
831
+ function setTabStatus(storyId, status) {
832
+ var dot = document.getElementById('tdot-' + storyId);
833
+ if (dot) dot.className = 'tab-status-dot ' + (status || 'starting');
834
+ }
835
+
836
+ function openOrchPanel(storyId) {
837
+ var panel = document.getElementById('orch-panel');
838
+ if (panel) panel.classList.add('open');
839
+ if (storyId && _sessions[storyId]) activatePanelTab(storyId);
840
+ else if (storyId) {
841
+ // Show empty state if no session yet
842
+ var body = document.getElementById('orch-term-body');
843
+ if (body) body.innerHTML = '<div class="orch-term-empty"><div>No output yet for ' + esc(storyId) + '</div></div>';
844
+ }
845
+ }
846
+
847
+ function closeOrchPanel() {
848
+ var panel = document.getElementById('orch-panel');
849
+ if (panel) panel.classList.remove('open');
850
+ _panelActive = null;
851
+ }
852
+
853
+ function closePanelTab(storyId) {
854
+ var tab = document.getElementById('orch-tab-' + storyId);
855
+ if (tab) tab.remove();
856
+ delete _sessions[storyId];
857
+ if (_orchStreams[storyId]) { _orchStreams[storyId].close(); delete _orchStreams[storyId]; }
858
+ if (_panelActive === storyId) {
859
+ var remaining = document.querySelectorAll('.orch-tab');
860
+ if (remaining.length > 0) {
861
+ activatePanelTab(remaining[0].dataset.storyId);
862
+ } else {
863
+ var tabs = document.getElementById('orch-tabs');
864
+ if (tabs) tabs.innerHTML = '<div class="orch-term-empty" style="padding:6px 8px;font-size:11px;">No active sessions</div>';
865
+ var body = document.getElementById('orch-term-body');
866
+ if (body) body.innerHTML = '<div class="orch-term-empty"><div>Select a session or run a story card</div></div>';
867
+ closeOrchPanel();
868
+ }
869
+ }
870
+ }
871
+
872
+ function stopActiveSession() {
873
+ if (_panelActive) stopStory(_panelActive);
874
+ }
875
+
876
+ function clearActiveTerminal() {
877
+ if (!_panelActive || !_sessions[_panelActive]) return;
878
+ _sessions[_panelActive].termEl.innerHTML = '';
879
+ _sessions[_panelActive].fileOpBuf = [];
880
+ var filesEl = document.getElementById('orch-files');
881
+ if (filesEl) {
882
+ filesEl.style.display = 'none';
883
+ while (filesEl.children.length > 1) filesEl.removeChild(filesEl.lastChild);
884
+ }
885
+ }
886
+
887
+ function openCleanSessions() {
888
+ fetch(ORCH + '/api/clean-sessions', { method: 'POST',
889
+ headers: { 'Content-Type': 'application/json' },
890
+ body: JSON.stringify({ olderThanDays: 7 }) })
891
+ .then(function(r) { return r.json(); })
892
+ .then(function(d) { showToast('Cleaned ' + (d.removed || 0) + ' sessions'); })
893
+ .catch(function() { showToast('Clean sessions: start orchestrator first'); });
894
+ }
895
+
896
+ function _renderFileOpEl(fileOp) {
897
+ var div = document.createElement('div');
898
+ div.className = 'kt-file';
899
+ var opClass = fileOp.op === 'write' ? 'op-w' : fileOp.op === 'bash' ? 'op-b' : 'op-r';
900
+ var opLabel = fileOp.op === 'write' ? '✎' : fileOp.op === 'bash' ? '$' : '👁';
901
+ var label = fileOp.path || fileOp.cmd || fileOp.tool;
902
+ div.innerHTML = '<span class="op-icon ' + opClass + '">' + opLabel + '</span> ' + esc(String(label));
903
+ return div;
904
+ }
905
+
906
+ // ── Terminal append (redirect to panel) ─────────────────────────
907
+
908
+ function appendCardChunk(storyId, chunk) {
909
+ var sess = _sessions[storyId];
910
+ if (!sess) return;
911
+ var last = sess.termEl.lastElementChild;
912
+ if (!last || !last.classList.contains('kt-stream')) {
913
+ last = document.createElement('div');
914
+ last.className = 'kt-stream';
915
+ sess.termEl.appendChild(last);
916
+ }
917
+ last.textContent += chunk;
918
+ if (_panelActive === storyId) {
919
+ var body = document.getElementById('orch-term-body');
920
+ if (body) body.scrollTop = body.scrollHeight;
921
+ }
922
+ }
923
+
924
+ function appendCardLog(storyId, line) {
925
+ var sess = _sessions[storyId];
926
+ if (!sess) return;
927
+ var div = document.createElement('div');
928
+ var cls = 'kt-line';
929
+ if (line.startsWith('⚙')) cls += ' tool';
930
+ else if (line.startsWith('⚠')) cls += ' warn';
931
+ else if (line.startsWith('✗')) cls += ' err';
932
+ else if (line.startsWith('✅')) cls += ' done-line';
933
+ else if (line.startsWith('▶') || line.startsWith('◉') || line.startsWith('■')) cls += ' meta';
934
+ div.className = cls;
935
+ div.textContent = line;
936
+ sess.termEl.appendChild(div);
937
+ if (_panelActive === storyId) {
938
+ var body = document.getElementById('orch-term-body');
939
+ if (body) body.scrollTop = body.scrollHeight;
940
+ }
941
+ }
942
+
943
+ function appendCardFileOp(storyId, fileOp) {
944
+ var sess = _sessions[storyId];
945
+ if (!sess) return;
946
+ sess.fileOpBuf.push(fileOp);
947
+ if (_panelActive === storyId) {
948
+ var filesEl = document.getElementById('orch-files');
949
+ if (filesEl) { filesEl.style.display = ''; filesEl.appendChild(_renderFileOpEl(fileOp)); }
950
+ }
951
+ }
952
+
953
+ // ── Run / stop ───────────────────────────────────────────────────
954
+
955
+ function runStory(storyId) {
956
+ if (!storyId) return;
957
+ var card = getCard(storyId);
958
+ var title = card ? (card.querySelector('.kanban-card-title') || {}).textContent : storyId;
959
+ createPanelTab(storyId, title || storyId);
960
+ openOrchPanel(storyId);
961
+ appendCardLog(storyId, '▶ Starting: ' + storyId);
962
+
963
+ fetch(ORCH + '/api/run', {
964
+ method: 'POST',
965
+ headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + (window.__ORCH_TOKEN__ || '') },
966
+ body: JSON.stringify({ storyId }),
967
+ })
968
+ .then(function(r) { return r.json(); })
969
+ .then(function(data) {
970
+ if (data.error) { appendCardLog(storyId, '✗ ' + data.error); setTabStatus(storyId, 'error'); return; }
971
+ appendCardLog(storyId, '▶ pid ' + data.pid);
972
+ setTabStatus(storyId, 'running');
973
+ moveKanbanCard(storyId, 'in_progress');
974
+ connectOrchestratorStream(storyId);
975
+ })
976
+ .catch(function(err) {
977
+ appendCardLog(storyId, '✗ Orchestrator unreachable — ' + err.message);
978
+ appendCardLog(storyId, ' Start with: node server/dashboard.js');
979
+ setTabStatus(storyId, 'error');
980
+ });
981
+ }
982
+
983
+ function stopStory(storyId) {
984
+ appendCardLog(storyId, '■ Stopping…');
985
+ fetch(ORCH + '/api/stop', {
986
+ method: 'POST',
987
+ headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + (window.__ORCH_TOKEN__ || '') },
988
+ body: JSON.stringify({ storyId }),
989
+ }).catch(function() {});
990
+ }
991
+
992
+ // ── SSE stream ───────────────────────────────────────────────────
993
+
994
+ function connectOrchestratorStream(storyId) {
995
+ if (_orchStreams[storyId]) _orchStreams[storyId].close();
996
+ var es = new EventSource(ORCH + '/api/stream/' + encodeURIComponent(storyId));
997
+ _orchStreams[storyId] = es;
998
+
999
+ es.onmessage = function(e) {
1000
+ try {
1001
+ var d = JSON.parse(e.data);
1002
+ if (d.chunk) appendCardChunk(storyId, d.chunk);
1003
+ if (d.line) appendCardLog(storyId, d.line);
1004
+ if (d.fileOp) appendCardFileOp(storyId, d.fileOp);
1005
+ if (d.status) {
1006
+ var st = d.status;
1007
+ setTabStatus(storyId, st);
1008
+ if (st === 'done') { appendCardLog(storyId, '✅ Done'); moveKanbanCard(storyId, 'done'); }
1009
+ if (st === 'error') moveKanbanCard(storyId, 'blocked');
1010
+ if (st === 'stopped') appendCardLog(storyId, '■ Stopped');
1011
+ if (st !== 'running') {
1012
+ es.close(); delete _orchStreams[storyId];
1013
+ var stopBtn = document.getElementById('orch-stop-btn');
1014
+ if (stopBtn && _panelActive === storyId) stopBtn.style.display = 'none';
1015
+ _updateOrchDot();
1016
+ }
1017
+ }
1018
+ } catch {}
1019
+ };
1020
+ es.onerror = function() {
1021
+ es.close(); delete _orchStreams[storyId];
1022
+ setTabStatus(storyId, 'error');
1023
+ _updateOrchDot();
1024
+ };
1025
+ _updateOrchDot();
1026
+ }
1027
+
1028
+ function _updateOrchDot() {
1029
+ var running = Object.keys(_orchStreams).length;
1030
+ // Global orch status dot in kanban topbar
1031
+ var dot = document.getElementById('orch-dot');
1032
+ if (dot) {
1033
+ dot.className = 'orch-status-dot' + (running > 0 ? ' up' : '');
1034
+ }
1035
+ // Panel orch dot
1036
+ var pdot = document.getElementById('orch-panel-orch-dot');
1037
+ if (pdot) {
1038
+ pdot.className = 'orch-status-dot' + (running > 0 ? ' up' : '');
1039
+ }
1040
+ }
1041
+
1042
+ function refreshOrchestratorStatus() {
1043
+ fetch(ORCH + '/api/status', { headers: { 'Authorization': 'Bearer ' + (window.__ORCH_TOKEN__ || '') } })
1044
+ .then(function(r) { return r.json(); })
1045
+ .then(function(status) {
1046
+ // Mark orch as reachable
1047
+ var dot = document.getElementById('orch-dot');
1048
+ if (dot && !Object.keys(_orchStreams).length) dot.className = 'orch-status-dot';
1049
+ for (var sid in status) {
1050
+ var info = status[sid];
1051
+ if (info.status === 'running') {
1052
+ moveKanbanCard(sid, 'in_progress');
1053
+ if (!_orchStreams[sid]) {
1054
+ var card = getCard(sid);
1055
+ var title = card ? (card.querySelector('.kanban-card-title') || {}).textContent : sid;
1056
+ createPanelTab(sid, title);
1057
+ connectOrchestratorStream(sid);
1058
+ }
1059
+ } else if (info.status === 'done') {
1060
+ moveKanbanCard(sid, 'done');
1061
+ }
1062
+ // Restore last session logs from /api/status if available
1063
+ if (info.logs && info.logs.length && !_sessions[sid]) {
1064
+ _ensureSession(sid, sid);
1065
+ createPanelTab(sid, sid);
1066
+ info.logs.forEach(function(line) { appendCardLog(sid, line); });
1067
+ setTabStatus(sid, info.status);
1068
+ }
1069
+ }
1070
+ })
1071
+ .catch(function() {
1072
+ var dot = document.getElementById('orch-dot');
1073
+ if (dot) dot.className = 'orch-status-dot down';
1074
+ });
1075
+ }
1076
+
1077
+ // ── Move card between columns ────────────────────────────────────
1078
+
1079
+ function moveKanbanCard(storyId, colId) {
1080
+ var card = getCard(storyId);
1081
+ var colBody = document.querySelector('.kanban-col[data-col="' + colId + '"] .kanban-col-body');
1082
+ if (!card || !colBody) return;
1083
+ colBody.appendChild(card);
1084
+
1085
+ // Update card class
1086
+ card.className = card.className.replace(/\bs-\w+\b/g, '').replace(/\brunning\b/g, '').trim();
1087
+ card.classList.add('s-' + colId);
1088
+ if (colId === 'in_progress') card.classList.add('running');
1089
+
1090
+ // Swap action button
1091
+ var actions = card.querySelector('.kanban-card-actions');
1092
+ if (actions) {
1093
+ if (colId === 'in_progress') {
1094
+ actions.innerHTML =
1095
+ '<button class="kanban-stop-btn" data-action="stop">■ Stop</button>' +
1096
+ '<button class="kanban-view-btn" data-action="view">↗ View</button>';
1097
+ } else if (colId === 'done') {
1098
+ actions.innerHTML = '<button class="kanban-view-btn" data-action="view">↗ Logs</button>';
1099
+ } else {
1100
+ actions.innerHTML = '<button class="kanban-run-btn" data-action="run">▶ Run</button>';
1101
+ }
1102
+ wireKanbanCardButtons(card);
1103
+ }
1104
+
1105
+ // Running indicator
1106
+ var ind = card.querySelector('.card-run-indicator');
1107
+ if (colId === 'in_progress' && !ind) {
1108
+ var indEl = document.createElement('div');
1109
+ indEl.className = 'card-run-indicator';
1110
+ indEl.id = 'run-ind-' + storyId;
1111
+ indEl.innerHTML = '<span class="run-pulse"></span>running';
1112
+ card.insertBefore(indEl, actions);
1113
+ } else if (colId !== 'in_progress' && ind) {
1114
+ ind.remove();
1115
+ }
1116
+
1117
+ refreshKanbanCounts();
1118
+ }
1119
+
1120
+ function wireKanbanLogButtons() {} // compat shim
1121
+
1122
+ function wireKanbanCardButtons(card) {
1123
+ var sid = card.dataset.storyId;
1124
+ if (!sid) return;
1125
+ card.querySelectorAll('[data-action="run"]').forEach(function(btn) {
1126
+ btn.onclick = function(e) { e.stopPropagation(); runStory(sid); };
1127
+ });
1128
+ card.querySelectorAll('[data-action="stop"]').forEach(function(btn) {
1129
+ btn.onclick = function(e) { e.stopPropagation(); stopStory(sid); };
1130
+ });
1131
+ card.querySelectorAll('[data-action="view"]').forEach(function(btn) {
1132
+ btn.onclick = function(e) { e.stopPropagation(); openOrchPanel(sid); };
1133
+ });
1134
+ }
1135
+
1136
+ function wireKanbanDnd() {
1137
+ let dragged = null;
1138
+ document.querySelectorAll('.kanban-card').forEach(card => {
1139
+ wireKanbanCardButtons(card);
1140
+ card.addEventListener('dragstart', e => {
1141
+ if (e.target.tagName === 'BUTTON') { e.preventDefault(); return; }
1142
+ dragged = card; card.style.opacity = '0.5';
1143
+ });
1144
+ card.addEventListener('dragend', () => {
1145
+ dragged = null; if (card) card.style.opacity = '';
1146
+ });
1147
+ });
1148
+ document.querySelectorAll('.kanban-col-body').forEach(body => {
1149
+ body.addEventListener('dragover', e => { e.preventDefault(); body.classList.add('drag-target'); });
1150
+ body.addEventListener('dragleave', () => body.classList.remove('drag-target'));
1151
+ body.addEventListener('drop', e => {
1152
+ e.preventDefault();
1153
+ body.classList.remove('drag-target');
1154
+ if (!dragged) return;
1155
+ body.appendChild(dragged);
1156
+ dragged.style.opacity = '';
1157
+ refreshKanbanCounts();
1158
+ showToast('Moved (visual only — not persisted)');
1159
+ });
1160
+ });
1161
+ }
1162
+
1163
+ function refreshKanbanCounts() {
1164
+ document.querySelectorAll('.kanban-col').forEach(col => {
1165
+ const n = col.querySelectorAll('.kanban-card').length;
1166
+ const badge = col.querySelector('.kanban-count');
1167
+ if (badge) badge.textContent = n;
1168
+ });
1169
+ }
1170
+
625
1171
  // #283: view plan file
626
1172
  async function viewPlanFile(phaseId) {
627
1173
  // Try to find the plan file via the file tree
@@ -688,12 +1234,16 @@ function route() {
688
1234
  // #310: scroll to top on view switch
689
1235
  document.querySelector('.content-area')?.scrollTo(0, 0);
690
1236
 
1237
+ // Close orchestrator panel when leaving kanban — it's fixed-position and overlaps other views
1238
+ if (view !== 'kanban') closeOrchPanel();
1239
+
691
1240
  if (view === 'overview') renderOverview();
692
1241
  else if (view === 'roadmap') renderRoadmap();
693
1242
  else if (view === 'milestones') renderMilestones(subId);
694
1243
  else if (view === 'phases') renderPhases(subId);
695
1244
  else if (view === 'sprints') renderSprints(subId);
696
1245
  else if (view === 'tasks') renderTasks();
1246
+ else if (view === 'kanban') renderKanban();
697
1247
  else if (view === 'decisions') renderDecisions();
698
1248
  else if (view === 'memory') renderMemory();
699
1249
  }
@@ -720,7 +1270,7 @@ function renderMemory() {
720
1270
  h += '<div class="filter-bar"><span style="color:var(--text-muted);font-size:var(--text-sm);">Last scanned: ' + esc(m.lastScanned) + '</span></div>';
721
1271
  h += '<div id="memory-sections">';
722
1272
  for (const [section, files] of Object.entries(sections)) {
723
- h += '<div style="font-size:var(--text-sm);font-weight:600;color:var(--text-muted);margin:var(--space-4) 0 var(--space-2);">' + esc(section) + '</div>';
1273
+ h += '<div class="memory-group-header">' + esc(section) + '</div>';
724
1274
  h += '<div class="decision-list">';
725
1275
  for (const f of files) {
726
1276
  const status = f.exists ? (f.populated ? '✓' : '○') : '✗';
@@ -734,7 +1284,7 @@ function renderMemory() {
734
1284
  }
735
1285
  function listGroup(label, items) {
736
1286
  if (!items || !items.length) return '';
737
- let g = '<div style="font-size:var(--text-sm);font-weight:600;color:var(--text-muted);margin:var(--space-4) 0 var(--space-2);">' + esc(label) + ' (' + items.length + ')</div>';
1287
+ let g = '<div class="memory-group-header">' + esc(label) + ' (' + items.length + ')</div>';
738
1288
  g += '<div class="decision-list">';
739
1289
  for (const f of items) {
740
1290
  g += '<div class="item">' +
@@ -780,7 +1330,7 @@ function renderDecisions() {
780
1330
  '<div class="filter-bar"><input class="filter-input" type="text" placeholder="Filter…" oninput="filterItems(this,\\'decisions-inner\\')"></div>' +
781
1331
  '<div id="decisions-inner">';
782
1332
  for (const [phase, decs] of Object.entries(grouped)) {
783
- h += '<div style="font-size:var(--text-sm);font-weight:600;color:var(--text-muted);margin:var(--space-4) 0 var(--space-2);">' + esc(phase) + '</div>';
1333
+ h += '<div class="memory-group-header">' + esc(phase) + '</div>';
784
1334
  h += '<div class="decision-list">';
785
1335
  for (const d of decs) {
786
1336
  const title = typeof d === 'string' ? d : (d.title || d.summary || d.decision || JSON.stringify(d).slice(0, 80));
@@ -814,7 +1364,8 @@ function filterItems(input, listId) {
814
1364
  const q = input.value.toLowerCase().trim();
815
1365
  const el = document.getElementById(listId);
816
1366
  if (!el) return;
817
- el.querySelectorAll('.item').forEach(item => {
1367
+ // Target both list items and agent cards
1368
+ el.querySelectorAll('.item, .agent-card').forEach(item => {
818
1369
  item.style.display = !q || item.textContent.toLowerCase().includes(q) ? '' : 'none';
819
1370
  });
820
1371
  }
@@ -838,12 +1389,12 @@ const _filesPromise = fetch('/api/files').then(function(r) { return r.json(); })
838
1389
 
839
1390
  groups.forEach(function(g) {
840
1391
  h += '<div class="inline-file-group" style="margin-bottom:var(--space-3);">';
841
- h += '<div style="font-size:var(--text-xs);font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.07em;padding:var(--space-1) 0;">' + esc(g.group) + '</div>';
1392
+ h += '<div style="font-size:var(--text-xs);font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.07em;padding:var(--space-1) var(--space-3);">' + esc(g.group) + '</div>';
842
1393
  if (g.subGroups) {
843
1394
  // Render expandable sub-groups (e.g. per-phase)
844
1395
  g.subGroups.forEach(function(sg) {
845
1396
  h += '<details class="inline-subgroup" open style="margin-left:var(--space-2);margin-bottom:var(--space-1);">';
846
- h += '<summary style="font-size:var(--text-xs);font-weight:500;color:var(--text-secondary);cursor:pointer;padding:var(--space-1) 0;user-select:none;">' + esc(sg.subGroup) + ' <span style="color:var(--text-muted);font-weight:400;">(' + sg.files.length + ')</span></summary>';
1397
+ h += '<summary style="font-size:var(--text-xs);font-weight:500;color:var(--text-secondary);cursor:pointer;padding:var(--space-1) var(--space-3);user-select:none;">' + esc(sg.subGroup) + ' <span style="color:var(--text-muted);font-weight:400;">(' + sg.files.length + ')</span></summary>';
847
1398
  sg.files.forEach(function(f) {
848
1399
  h += renderFileItem(f, sg.subGroup);
849
1400
  });
@@ -1066,6 +1617,167 @@ function closeSidebar() {
1066
1617
  // ---- Boot ----
1067
1618
  route();
1068
1619
  updateTitle();
1620
+
1621
+ // ── xterm Terminal Panel ─────────────────────────────────────────────────────
1622
+ var _term = null;
1623
+ var _termFit = null;
1624
+ var _termEvt = null;
1625
+ var _termStoryId = null;
1626
+
1627
+ function _orchToken() { return window.__ORCH_TOKEN__ || ''; }
1628
+
1629
+ function openTermPanel(storyId, title) {
1630
+ _termStoryId = storyId;
1631
+ document.getElementById('term-title').textContent = title || storyId;
1632
+ document.getElementById('term-panel').classList.add('open');
1633
+ document.getElementById('term-backdrop').classList.add('open');
1634
+ setTermDot('connecting');
1635
+
1636
+ if (!_term && typeof Terminal !== 'undefined') {
1637
+ _term = new Terminal({
1638
+ theme: {
1639
+ background: '#0c0c0e', foreground: '#c9d1d9',
1640
+ cursor: '#58a6ff', selectionBackground: 'rgba(94,106,210,0.25)',
1641
+ black: '#0c0c0e', red: '#ff4444', green: '#3fb950',
1642
+ yellow: '#d29922', blue: '#58a6ff', magenta: '#bc8cff',
1643
+ cyan: '#39c5cf', white: '#b1bac4', brightBlack: '#6e7681',
1644
+ },
1645
+ fontFamily: '"JetBrains Mono","SF Mono",Consolas,monospace',
1646
+ fontSize: 12, lineHeight: 1.4, convertEol: true,
1647
+ scrollback: 5000, cursorStyle: 'bar',
1648
+ });
1649
+ if (typeof FitAddon !== 'undefined') {
1650
+ _termFit = new FitAddon.FitAddon();
1651
+ _term.loadAddon(_termFit);
1652
+ }
1653
+ _term.open(document.getElementById('term-container'));
1654
+ if (_termFit) _termFit.fit();
1655
+ _term.onData(function(data) {
1656
+ var tok = _orchToken();
1657
+ if (!_termStoryId || !tok) return;
1658
+ fetch('http://localhost:7718/api/message', {
1659
+ method: 'POST',
1660
+ headers: { 'Authorization': 'Bearer ' + tok, 'Content-Type': 'application/json' },
1661
+ body: JSON.stringify({ storyId: _termStoryId, data: data })
1662
+ }).catch(function() {});
1663
+ });
1664
+ window.addEventListener('resize', function() { if (_termFit) _termFit.fit(); });
1665
+ } else if (_term) {
1666
+ _term.clear();
1667
+ if (_termFit) _termFit.fit();
1668
+ }
1669
+
1670
+ if (_termEvt) { _termEvt.close(); _termEvt = null; }
1671
+ var tok = _orchToken();
1672
+ if (!tok) {
1673
+ if (_term) _term.writeln('\\r\\x1b[31m✗ No orchestrator token — restart the dashboard\\x1b[0m');
1674
+ return;
1675
+ }
1676
+
1677
+ if (_term) _term.writeln('\\x1b[90m── connecting to stream: ' + storyId + ' ──\\x1b[0m\\r\\n');
1678
+
1679
+ var url = 'http://localhost:7718/api/stream/' + encodeURIComponent(storyId) + '?token=' + tok;
1680
+ _termEvt = new EventSource(url);
1681
+ _termEvt.onmessage = function(e) {
1682
+ try {
1683
+ var d = JSON.parse(e.data);
1684
+ if (d.line) { if (_term) _term.writeln('\\r' + d.line); }
1685
+ if (d.chunk) { if (_term) _term.write(d.chunk); }
1686
+ if (d.fileOp) { if (_term) _term.writeln('\\r\\x1b[36m[' + d.fileOp.type + '] ' + d.fileOp.path + '\\x1b[0m'); }
1687
+ if (d.status) {
1688
+ setTermDot(d.status);
1689
+ if (d.status === 'done' || d.status === 'error' || d.status === 'stopped') {
1690
+ if (_term) _term.writeln('\\r\\n\\x1b[90m── session ' + d.status + ' ──\\x1b[0m');
1691
+ if (_termEvt) { _termEvt.close(); _termEvt = null; }
1692
+ } else {
1693
+ setTermDot(d.status);
1694
+ }
1695
+ }
1696
+ if (d.error) { if (_term) _term.writeln('\\r\\x1b[31m✗ ' + d.error + '\\x1b[0m'); }
1697
+ } catch(ex) {}
1698
+ };
1699
+ _termEvt.onerror = function() {
1700
+ if (_term) _term.writeln('\\r\\x1b[31m✗ stream disconnected\\x1b[0m');
1701
+ setTermDot('error');
1702
+ };
1703
+ }
1704
+
1705
+ function setTermDot(status) {
1706
+ var dot = document.getElementById('term-status-dot');
1707
+ if (dot) dot.className = 'term-status-dot ' + (status || '');
1708
+ }
1709
+
1710
+ function closeTermPanel() {
1711
+ document.getElementById('term-panel').classList.remove('open');
1712
+ document.getElementById('term-backdrop').classList.remove('open');
1713
+ if (_termEvt) { _termEvt.close(); _termEvt = null; }
1714
+ }
1715
+
1716
+ function termStop() {
1717
+ var tok = _orchToken();
1718
+ if (!_termStoryId || !tok) return;
1719
+ fetch('http://localhost:7718/api/stop', {
1720
+ method: 'POST',
1721
+ headers: { 'Authorization': 'Bearer ' + tok, 'Content-Type': 'application/json' },
1722
+ body: JSON.stringify({ storyId: _termStoryId })
1723
+ }).catch(function() {});
1724
+ }
1725
+
1726
+ function termSend() {
1727
+ var inp = document.getElementById('term-input');
1728
+ if (!inp) return;
1729
+ var msg = (inp.value || '').trim();
1730
+ if (!msg) return;
1731
+ inp.value = '';
1732
+ var tok = _orchToken();
1733
+ if (!_termStoryId || !tok) return;
1734
+ if (_term) _term.writeln('\\r\\x1b[90m[you] ' + msg + '\\x1b[0m');
1735
+ fetch('http://localhost:7718/api/message', {
1736
+ method: 'POST',
1737
+ headers: { 'Authorization': 'Bearer ' + tok, 'Content-Type': 'application/json' },
1738
+ body: JSON.stringify({ storyId: _termStoryId, data: msg + '\\n' })
1739
+ }).catch(function() {});
1740
+ }
1741
+
1742
+ document.addEventListener('keydown', function(e) {
1743
+ if (e.key === 'Escape') {
1744
+ var panel = document.getElementById('term-panel');
1745
+ if (panel && panel.classList.contains('open')) closeTermPanel();
1746
+ }
1747
+ });
1748
+
1749
+ var _termInputEl = document.getElementById('term-input');
1750
+ if (_termInputEl) {
1751
+ _termInputEl.addEventListener('keydown', function(e) {
1752
+ if (e.key === 'Enter') termSend();
1753
+ });
1754
+ }
1755
+
1756
+ function runAndOpenTerm(storyId, cmd, title) {
1757
+ var tok = _orchToken();
1758
+ openTermPanel(storyId, title || storyId);
1759
+ if (!tok) return;
1760
+ fetch('http://localhost:7718/api/run', {
1761
+ method: 'POST',
1762
+ headers: { 'Authorization': 'Bearer ' + tok, 'Content-Type': 'application/json' },
1763
+ body: JSON.stringify({ storyId: storyId, cmd: cmd })
1764
+ }).then(function(r) { return r.json(); })
1765
+ .then(function(data) {
1766
+ if (data.error && data.error !== 'already running') {
1767
+ if (_term) _term.writeln('\\r\\x1b[31m✗ ' + data.error + '\\x1b[0m');
1768
+ } else if (data.error === 'already running') {
1769
+ if (_term) _term.writeln('\\r\\x1b[33m⚠ Already running (pid ' + data.pid + ') — showing live output\\x1b[0m');
1770
+ setTermDot('running');
1771
+ }
1772
+ })
1773
+ .catch(function(err) {
1774
+ if (_term) _term.writeln('\\r\\x1b[31m✗ Orchestrator unreachable: ' + err.message + '\\x1b[0m');
1775
+ });
1776
+ }
1777
+
1778
+ // Also fix existing kanban run/stop to use auth token
1779
+ var _origRunStory = window.runStory;
1780
+
1069
1781
  </script>`;
1070
1782
  }
1071
1783