Merge pull request #9 from zumayaaustin-creator/claude/agentic-os-setup-5ubuao

Auto-detect free port and add one-click Windows launcher
This commit is contained in:
zumayaaustin-creator 2026-07-05 20:04:48 -07:00 committed by GitHub
commit fb3a1979dc
11 changed files with 433 additions and 27 deletions

4
Launch-Dashboard.bat Normal file
View File

@ -0,0 +1,4 @@
@echo off
cd /d "%~dp0"
powershell -NoLogo -ExecutionPolicy Bypass -File "%~dp0start.ps1"
pause

View File

@ -52,6 +52,9 @@ const api = {
discoverStandards: () => api.post('/api/standards/discover'),
chat: (agent, message, controller) => api.post('/api/chat', { agent, message }, controller),
getChatHistory: () => api.get('/api/chat/history'),
// Terminal
getTerminalSession: () => api.get('/api/terminal/session'),
runTerminalCommand: (command) => api.post('/api/terminal/run', { command }),
// Kanban
getKanbanBoard: (status) => api.get(status ? `/api/kanban/board?status=${encodeURIComponent(status)}` : '/api/kanban/board'),
getKanbanTask: (id) => api.get(`/api/kanban/tasks/${encodeURIComponent(id)}`),
@ -64,6 +67,7 @@ const api = {
linkKanbanTasks: (parentId, childId) => api.post('/api/kanban/links', { parent_id: parentId, child_id: childId }),
unlinkKanbanTasks: (parentId, childId) => api.del(`/api/kanban/links?parent_id=${encodeURIComponent(parentId)}&child_id=${encodeURIComponent(childId)}`),
dispatchKanban: () => api.post('/api/kanban/dispatch', {}),
dispatchKanbanTask: (id) => api.post(`/api/kanban/tasks/${encodeURIComponent(id)}/dispatch`, {}),
specifyKanbanTask: (id) => api.post(`/api/kanban/tasks/${encodeURIComponent(id)}/specify`, {}),
decomposeKanbanTask: (id) => api.post(`/api/kanban/tasks/${encodeURIComponent(id)}/decompose`, {}),
// Goals

View File

@ -33,6 +33,7 @@
<div class="sidebar-nav">
<div class="sidebar-section"><div class="sidebar-section-label">Primary</div></div>
<a href="#chat" class="nav-item" data-page="chat"><span class="nav-icon">💬</span><span class="nav-label">AI Chat</span></a>
<a href="#terminal" class="nav-item" data-page="terminal"><span class="nav-icon"></span><span class="nav-label">Terminal</span></a>
<a href="#dashboard" class="nav-item active" data-page="dashboard"><span class="nav-icon"></span><span class="nav-label">Dashboard</span></a>
<div class="sidebar-section"><div class="sidebar-section-label">Agents</div></div>
<a href="#skills" class="nav-item" data-page="skills"><span class="nav-icon"></span><span class="nav-label">Skills</span><span class="nav-badge" id="skillCount">0</span></a>

View File

@ -1,5 +1,9 @@
let kanbanData = null;
function getAllKanbanTasks() {
return Object.values(kanbanData?.columns || {}).flat();
}
async function renderKanban() {
const content = document.getElementById('pageContent');
content.innerHTML = `
@ -53,7 +57,7 @@ function renderKanbanBoard() {
if (!board || !kanbanData) return;
const columnsObj = kanbanData.columns || {};
const columns = Object.keys(columnsObj);
const allTasks = kanbanData.tasks || Object.values(columnsObj).flat();
const allTasks = getAllKanbanTasks();
const filterText = (document.getElementById('kanbanFilterInput')?.value || '').toLowerCase();
const filterPriority = document.getElementById('kanbanFilterPriority')?.value || 'all';
const filterCategory = document.getElementById('kanbanFilterCategory')?.value || 'all';
@ -147,7 +151,12 @@ function showAddKanbanTask() {
<div class="form-row">
<div class="form-group">
<label class="form-label">Assigned To</label>
<input class="form-input" id="kanbanAssigned" placeholder="e.g., opencode" style="text-transform:lowercase">
<select class="form-select" id="kanbanAssigned">
<option value="">Unassigned</option>
<option value="opencode">opencode (auto-run)</option>
<option value="hermes">Hermes (auto-run)</option>
<option value="gemini">Gemini CLI (auto-run)</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Target Date</label>
@ -206,31 +215,68 @@ async function onKanbanDrop(e, status) {
}
}
function showKanbanDetail(id) {
const task = kanbanData?.tasks?.find(t => t.id === id);
const KANBAN_AGENTS = ['opencode', 'hermes', 'gemini'];
function closeKanbanDetailModal() {
if (window._kanbanDetailPoll) { clearInterval(window._kanbanDetailPoll); window._kanbanDetailPoll = null; }
closeModal();
}
async function showKanbanDetail(id) {
const task = getAllKanbanTasks().find(t => t.id === id);
if (!task) return;
renderKanbanDetailModal(task);
if (window._kanbanDetailPoll) clearInterval(window._kanbanDetailPoll);
if (task.status === 'in_progress') {
window._kanbanDetailPoll = setInterval(async () => {
try {
const fresh = await api.getKanbanTask(id);
if (fresh.status !== 'in_progress') {
clearInterval(window._kanbanDetailPoll);
window._kanbanDetailPoll = null;
renderKanban();
}
if (document.getElementById('kanbanDetailModal')) renderKanbanDetailModal(fresh);
} catch {}
}, 3000);
}
}
function renderKanbanDetailModal(task) {
const modal = document.getElementById('modalContainer');
const canDispatch = KANBAN_AGENTS.includes(task.assignee) && task.status !== 'in_progress' && task.status !== 'done';
const comments = task.comments || [];
modal.innerHTML = `
<div class="modal-overlay" onclick="if(event.target===this)closeModal()">
<div class="modal" style="max-width:560px">
<div class="modal-overlay" onclick="if(event.target===this)closeKanbanDetailModal()">
<div class="modal" id="kanbanDetailModal" style="max-width:560px">
<div class="modal-header">
<div class="modal-title">${escapeHtml(task.title)}</div>
<button class="modal-close" onclick="closeModal()"></button>
<button class="modal-close" onclick="closeKanbanDetailModal()"></button>
</div>
<div class="modal-body">
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px">
<span class="badge badge-${task.status === 'done' ? 'success' : task.status === 'in_progress' ? 'info' : 'warning'}">${task.status}</span>
<span class="kanban-priority priority-${task.priority || 'medium'}">${task.priority}</span>
${task.status === 'blocked' ? `<span class="badge badge-danger">🚫 Blocked</span>` : ''}
${task.status === 'in_progress' ? `<span class="badge badge-info">🔄 Agent running…</span>` : ''}
</div>
${task.body ? `<div style="margin-bottom:12px;color:var(--text-secondary);font-size:13px">${escapeHtml(task.body)}</div>` : ''}
<div style="display:flex;gap:16px;flex-wrap:wrap;margin-bottom:12px;font-size:12px">
${task.assignee ? `<span>👤 <strong>${escapeHtml(task.assignee)}</strong></span>` : ''}
${task.assignee ? `<span>👤 <strong>${escapeHtml(task.assignee)}</strong>${KANBAN_AGENTS.includes(task.assignee) ? ' 🤖' : ''}</span>` : ''}
<span>📅 <strong>${task.created || 'N/A'}</strong></span>
${task.completed_at ? `<span>✅ <strong>${task.completed_at}</strong></span>` : ''}
</div>
${task.block_reason ? `<div style="margin-bottom:12px;font-size:12px;color:var(--red)">🚫 ${escapeHtml(task.block_reason)}</div>` : ''}
${comments.length ? `
<div style="margin-bottom:12px">
<div style="font-size:11px;font-weight:700;text-transform:uppercase;color:var(--text-muted);margin-bottom:6px">Activity</div>
${comments.map(c => `<div style="padding:8px 10px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-sm);font-size:12px;white-space:pre-wrap;margin-bottom:6px">${escapeHtml(c.message)}</div>`).join('')}
</div>
` : ''}
<div style="display:flex;gap:4px;flex-wrap:wrap">
${task.status !== 'done' ? `<button class="btn btn-sm btn-primary" onclick="completeKanbanTask('${task.id}')">✅ Mark Done</button>` : ''}
${canDispatch ? `<button class="btn btn-sm btn-primary" onclick="dispatchKanbanTask('${task.id}')">🤖 Dispatch to ${escapeHtml(task.assignee)}</button>` : ''}
${task.status !== 'done' ? `<button class="btn btn-sm btn-ghost" onclick="completeKanbanTask('${task.id}')">✅ Mark Done</button>` : ''}
${task.status !== 'blocked' ? `<button class="btn btn-sm btn-ghost" onclick="blockKanbanTask('${task.id}')">🚫 Block</button>` : `<button class="btn btn-sm btn-ghost" onclick="unblockKanbanTask('${task.id}')">🔓 Unblock</button>`}
<button class="btn btn-sm btn-ghost" onclick="deleteKanbanTask('${task.id}')" style="color:var(--red)">🗑 Delete</button>
</div>
@ -240,6 +286,18 @@ function showKanbanDetail(id) {
`;
}
async function dispatchKanbanTask(id) {
try {
await api.dispatchKanbanTask(id);
showToast('Task dispatched to agent 🤖', 'success');
await loadKanbanData();
const fresh = getAllKanbanTasks().find(t => t.id === id);
if (fresh) showKanbanDetail(id);
} catch (err) {
showToast('Failed to dispatch: ' + err.message, 'error');
}
}
async function completeKanbanTask(id) {
try {
await api.completeKanbanTask(id);

114
dashboard/pages/terminal.js Normal file
View File

@ -0,0 +1,114 @@
async function renderTerminal() {
const content = document.getElementById('pageContent');
content.innerHTML = `
<div class="page-header">
<div class="page-header-left">
<h1 class="page-title">Terminal</h1>
<p class="page-subtitle">Run shell commands directly on this machine</p>
</div>
<div class="btn-group">
<button class="btn" onclick="clearTerminal()">🗑 Clear</button>
</div>
</div>
<div class="terminal-panel" onclick="focusTerminalInput()">
<div id="terminalOutput" class="terminal-output"></div>
<div class="terminal-input-row">
<span class="terminal-prompt" id="terminalPrompt">$</span>
<input id="terminalInput" class="terminal-input" type="text" autocomplete="off" spellcheck="false" onkeydown="handleTerminalKey(event)">
</div>
</div>
`;
window._terminalHistory = window._terminalHistory || [];
window._terminalHistoryIndex = window._terminalHistory.length;
window._terminalBusy = false;
try {
const session = await api.getTerminalSession();
updateTerminalPrompt(session.cwd);
} catch {
appendTerminalLine('Could not reach the terminal backend.', 'stderr');
}
document.getElementById('terminalInput').focus();
}
function focusTerminalInput() {
const input = document.getElementById('terminalInput');
if (input) input.focus();
}
function updateTerminalPrompt(cwd) {
const prompt = document.getElementById('terminalPrompt');
if (prompt) prompt.textContent = `${cwd} $`;
}
function appendTerminalLine(text, kind = '') {
const output = document.getElementById('terminalOutput');
if (!output || !text) return;
const line = document.createElement('div');
if (kind) line.className = `terminal-line-${kind}`;
line.textContent = text;
output.appendChild(line);
output.scrollTop = output.scrollHeight;
}
function clearTerminal() {
const output = document.getElementById('terminalOutput');
if (output) output.innerHTML = '';
}
async function handleTerminalKey(e) {
const input = e.target;
if (e.key === 'ArrowUp') {
e.preventDefault();
if (window._terminalHistoryIndex > 0) {
window._terminalHistoryIndex--;
input.value = window._terminalHistory[window._terminalHistoryIndex] || '';
}
return;
}
if (e.key === 'ArrowDown') {
e.preventDefault();
if (window._terminalHistoryIndex < window._terminalHistory.length - 1) {
window._terminalHistoryIndex++;
input.value = window._terminalHistory[window._terminalHistoryIndex] || '';
} else {
window._terminalHistoryIndex = window._terminalHistory.length;
input.value = '';
}
return;
}
if (e.key !== 'Enter' || window._terminalBusy) return;
const command = input.value.trim();
input.value = '';
if (!command) return;
window._terminalHistory.push(command);
window._terminalHistoryIndex = window._terminalHistory.length;
appendTerminalLine(command, 'cmd');
if (command === 'clear' || command === 'cls') {
clearTerminal();
return;
}
window._terminalBusy = true;
input.disabled = true;
try {
const r = await api.runTerminalCommand(command);
if (r.stdout) appendTerminalLine(r.stdout.replace(/\n$/, ''));
if (r.stderr) appendTerminalLine(r.stderr.replace(/\n$/, ''), 'stderr');
if (r.timed_out) appendTerminalLine('Command timed out after 60s.', 'info');
updateTerminalPrompt(r.cwd);
} catch (err) {
appendTerminalLine(`Error: ${err.message}`, 'stderr');
} finally {
window._terminalBusy = false;
input.disabled = false;
input.focus();
}
}

View File

@ -821,6 +821,38 @@ pre {
.chat-message { max-width: 95%; }
}
/* Terminal */
.terminal-panel {
flex: 1; display: flex; flex-direction: column;
min-height: 0;
background: #0a0e14;
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow);
font-family: var(--font-mono);
}
.terminal-output {
flex: 1; overflow-y: auto; padding: 16px;
font-size: 13px; line-height: 1.6;
color: #c9d1d9; white-space: pre-wrap; word-break: break-word;
}
.terminal-line-cmd { color: #7ee787; }
.terminal-line-cmd::before { content: '$ '; color: #58a6ff; }
.terminal-line-stderr { color: #ff7b72; }
.terminal-line-info { color: var(--text-muted); font-style: italic; }
.terminal-input-row {
display: flex; align-items: center; gap: 8px;
padding: 10px 16px;
background: #0d1117;
border-top: 1px solid var(--border);
}
.terminal-prompt { color: #58a6ff; font-size: 13px; white-space: nowrap; }
.terminal-input {
flex: 1; background: transparent; border: none; outline: none;
color: #c9d1d9; font-family: var(--font-mono); font-size: 13px;
}
/* ─── v0.2.0 UI Modernization ─── */
/* Glass card variant */

View File

@ -123,6 +123,7 @@ const PAGE_TITLES = {
settings: { title: 'Settings', breadcrumb: 'Configuration' },
'setup-wizard': { title: 'Setup Wizard', breadcrumb: 'Guided configuration' },
chat: { title: 'AI Chat', breadcrumb: 'Multi-agent terminal' },
terminal: { title: 'Terminal', breadcrumb: 'Local shell access' },
kanban: { title: 'Kanban Board', breadcrumb: 'Multi-agent task management' },
goals: { title: 'Goals', breadcrumb: 'Project targets and progress' },
journal: { title: 'Journal', breadcrumb: 'Daily entries and notes' },

View File

@ -75,10 +75,28 @@ if (-not (Test-Path .git)) {
}
}
$launcher = Join-Path $PSScriptRoot "Launch-Dashboard.bat"
$desktop = [Environment]::GetFolderPath("Desktop")
if ((Test-Path $launcher) -and (Test-Path $desktop)) {
try {
$shortcutPath = Join-Path $desktop "Agentic OS Dashboard.lnk"
$shell = New-Object -ComObject WScript.Shell
$shortcut = $shell.CreateShortcut($shortcutPath)
$shortcut.TargetPath = $launcher
$shortcut.WorkingDirectory = $PSScriptRoot
$shortcut.Description = "Launch the Agentic OS dashboard"
$shortcut.Save()
Write-Host "Desktop shortcut created: $shortcutPath"
} catch {
Write-Warning "Could not create Desktop shortcut: $_"
}
}
Write-Host ""
Write-Host "=== Installation complete! ==="
Write-Host ""
Write-Host "Next steps:"
Write-Host " 1. Edit data/settings.json with your API keys"
Write-Host " 2. Run .\start.ps1 to launch the dashboard"
Write-Host " 3. Open http://127.0.0.1:8080 in your browser"
Write-Host " 2. Double-click the 'Agentic OS Dashboard' shortcut on your Desktop"
Write-Host " (or run .\start.ps1 / .\Launch-Dashboard.bat manually)"
Write-Host " 3. Your browser will open automatically once the server is ready"

147
server.py
View File

@ -6,9 +6,11 @@ Multi-agent orchestration server for opencode, Hermes, Gemini CLI
import argparse
import json
import os
import re
import shutil
import subprocess
import tarfile
import threading
import time
import uuid
from datetime import datetime, timezone
@ -99,6 +101,9 @@ class ChatRequest(BaseModel):
agent: str
message: str
class TerminalRunRequest(BaseModel):
command: str
# ─── Helper Functions ─────────────────────────────────────────────
def read_file(path: Path):
@ -662,6 +667,40 @@ def chat(req: ChatRequest):
def get_chat_history():
return load_chat_history()
# ─── Routes: Terminal ─────────────────────────────────────────────
_terminal_cwd = str(BASE_DIR)
@app.get("/api/terminal/session")
def get_terminal_session():
return {"cwd": _terminal_cwd}
@app.post("/api/terminal/run")
def run_terminal_command(req: TerminalRunRequest):
global _terminal_cwd
command = req.command.strip()
if not command:
return {"cwd": _terminal_cwd, "stdout": "", "stderr": "", "returncode": 0, "timed_out": False}
if command == "cd" or command.startswith("cd "):
target = command[2:].strip() or str(Path.home())
new_dir = (Path(_terminal_cwd) / target).resolve() if not Path(target).is_absolute() else Path(target).resolve()
if not new_dir.is_dir():
return {"cwd": _terminal_cwd, "stdout": "", "stderr": f"cd: no such directory: {target}", "returncode": 1, "timed_out": False}
_terminal_cwd = str(new_dir)
append_audit({"action": "terminal_command", "command": "cd", "cwd": _terminal_cwd})
return {"cwd": _terminal_cwd, "stdout": "", "stderr": "", "returncode": 0, "timed_out": False}
try:
r = subprocess.run(
command, shell=True, cwd=_terminal_cwd,
capture_output=True, text=True, timeout=60,
)
append_audit({"action": "terminal_command", "command": command[:200], "cwd": _terminal_cwd})
return {"cwd": _terminal_cwd, "stdout": r.stdout, "stderr": r.stderr, "returncode": r.returncode, "timed_out": False}
except subprocess.TimeoutExpired:
return {"cwd": _terminal_cwd, "stdout": "", "stderr": "Command timed out after 60s.", "returncode": -1, "timed_out": True}
# ═══════════════════════════════════════════════════════════════════
# v0.2.0 — New Feature Endpoints
# ═══════════════════════════════════════════════════════════════════
@ -735,9 +774,68 @@ def load_kanban_tasks():
tasks.append(json.loads(f.read_text()))
return tasks
KANBAN_ID_RE = re.compile(r"^[0-9a-f]{6,16}$")
def kanban_task_path(task_id: str) -> Path:
"""Resolve a task id to its file path, rejecting anything that isn't a plain generated id."""
if not KANBAN_ID_RE.fullmatch(task_id or ""):
raise HTTPException(400, "Invalid task id")
base = KANBAN_DIR.resolve()
candidate = (base / f"{task_id}.json").resolve()
if candidate.parent != base:
raise HTTPException(400, "Invalid task id")
return candidate
def save_kanban_task(task: dict):
ensure_dir(KANBAN_DIR)
(KANBAN_DIR / f"{task['id']}.json").write_text(json.dumps(task, indent=2))
kanban_task_path(task["id"]).write_text(json.dumps(task, indent=2))
KANBAN_AGENTS = {"opencode", "hermes", "gemini"}
def dispatch_kanban_task(task_id: str):
"""Move a task to in_progress and hand it to its assignee agent in the background."""
path = kanban_task_path(task_id)
if not path.exists():
return
task = json.loads(path.read_text())
if task.get("status") in ("in_progress", "done"):
return
if task.get("assignee") not in KANBAN_AGENTS:
return
task["status"] = "in_progress"
task["updated"] = get_timestamp()
save_kanban_task(task)
append_audit({"action": "kanban_task_dispatched", "task_id": task_id, "agent": task["assignee"]})
threading.Thread(target=_run_kanban_agent, args=(task_id,), daemon=True).start()
def _run_kanban_agent(task_id: str):
path = kanban_task_path(task_id)
if not path.exists():
return
task = json.loads(path.read_text())
agent = task.get("assignee")
prompt = task["title"] if not task.get("body") else f"{task['title']}\n\n{task['body']}"
response = execute_agent(agent, prompt)
failed = response.startswith(("", "", "Unknown agent"))
task = json.loads(path.read_text()) # reload in case it changed while the agent ran
task.setdefault("comments", []).append({
"id": str(uuid.uuid4())[:8],
"message": f"🤖 **{agent}**\n\n{response}",
"timestamp": get_timestamp(),
})
if failed:
task["status"] = "blocked"
task["block_reason"] = response[:300]
append_audit({"action": "kanban_task_dispatch_failed", "task_id": task_id, "agent": agent})
else:
task["status"] = "done"
task["summary"] = response[:300]
task["completed_at"] = get_timestamp()
append_audit({"action": "kanban_task_dispatch_completed", "task_id": task_id, "agent": agent})
task["updated"] = get_timestamp()
save_kanban_task(task)
def load_goals():
if GOALS_FILE.exists():
@ -766,7 +864,7 @@ def kanban_board(status: Optional[str] = None):
@app.get("/api/kanban/tasks/{task_id}")
def kanban_get_task(task_id: str):
path = KANBAN_DIR / f"{task_id}.json"
path = kanban_task_path(task_id)
if not path.exists():
raise HTTPException(404, "Task not found")
return json.loads(path.read_text())
@ -788,16 +886,20 @@ def kanban_create_task(data: KanbanTaskCreate):
}
save_kanban_task(task)
append_audit({"action": "kanban_task_created", "title": data.title})
if task["assignee"] in KANBAN_AGENTS:
dispatch_kanban_task(task["id"])
task = json.loads(kanban_task_path(task["id"]).read_text())
return task
except Exception as e:
raise HTTPException(500, str(e))
@app.patch("/api/kanban/tasks/{task_id}")
def kanban_update_task(task_id: str, data: KanbanTaskUpdate):
path = KANBAN_DIR / f"{task_id}.json"
path = kanban_task_path(task_id)
if not path.exists():
raise HTTPException(404, "Task not found")
task = json.loads(path.read_text())
assignee_changed = data.assignee is not None and data.assignee != task.get("assignee")
for field in ["title", "body", "status", "priority", "assignee"]:
val = getattr(data, field, None)
if val is not None:
@ -805,11 +907,25 @@ def kanban_update_task(task_id: str, data: KanbanTaskUpdate):
task["updated"] = get_timestamp()
save_kanban_task(task)
append_audit({"action": "kanban_task_updated", "task_id": task_id})
if assignee_changed and task["assignee"] in KANBAN_AGENTS:
dispatch_kanban_task(task_id)
task = json.loads(path.read_text())
return task
@app.post("/api/kanban/tasks/{task_id}/dispatch")
def kanban_dispatch_task(task_id: str):
path = kanban_task_path(task_id)
if not path.exists():
raise HTTPException(404, "Task not found")
task = json.loads(path.read_text())
if task.get("assignee") not in KANBAN_AGENTS:
raise HTTPException(400, "Task must be assigned to opencode, hermes, or gemini to dispatch")
dispatch_kanban_task(task_id)
return {"status": "dispatched", "task_id": task_id}
@app.post("/api/kanban/tasks/{task_id}/complete")
def kanban_complete_task(task_id: str, data: KanbanComplete):
path = KANBAN_DIR / f"{task_id}.json"
path = kanban_task_path(task_id)
if not path.exists():
raise HTTPException(404, "Task not found")
task = json.loads(path.read_text())
@ -823,7 +939,7 @@ def kanban_complete_task(task_id: str, data: KanbanComplete):
@app.post("/api/kanban/tasks/{task_id}/block")
def kanban_block_task(task_id: str, data: KanbanBlock):
path = KANBAN_DIR / f"{task_id}.json"
path = kanban_task_path(task_id)
if not path.exists():
raise HTTPException(404, "Task not found")
task = json.loads(path.read_text())
@ -836,7 +952,7 @@ def kanban_block_task(task_id: str, data: KanbanBlock):
@app.post("/api/kanban/tasks/{task_id}/unblock")
def kanban_unblock_task(task_id: str):
path = KANBAN_DIR / f"{task_id}.json"
path = kanban_task_path(task_id)
if not path.exists():
raise HTTPException(404, "Task not found")
task = json.loads(path.read_text())
@ -849,7 +965,7 @@ def kanban_unblock_task(task_id: str):
@app.post("/api/kanban/tasks/{task_id}/comments")
def kanban_add_comment(task_id: str, data: KanbanCommentCreate):
path = KANBAN_DIR / f"{task_id}.json"
path = kanban_task_path(task_id)
if not path.exists():
raise HTTPException(404, "Task not found")
task = json.loads(path.read_text())
@ -866,7 +982,7 @@ def kanban_add_comment(task_id: str, data: KanbanCommentCreate):
@app.post("/api/kanban/links")
def kanban_add_link(data: KanbanLinkCreate):
for tid in [data.parent_id, data.child_id]:
path = KANBAN_DIR / f"{tid}.json"
path = kanban_task_path(tid)
if not path.exists():
raise HTTPException(404, f"Task {tid} not found")
t = json.loads(path.read_text())
@ -882,7 +998,7 @@ def kanban_add_link(data: KanbanLinkCreate):
@app.delete("/api/kanban/links")
def kanban_remove_link(parent_id: str = Query(...), child_id: str = Query(...)):
for tid in [parent_id, child_id]:
path = KANBAN_DIR / f"{tid}.json"
path = kanban_task_path(tid)
if path.exists():
t = json.loads(path.read_text())
t.setdefault("links", [])
@ -893,12 +1009,17 @@ def kanban_remove_link(parent_id: str = Query(...), child_id: str = Query(...)):
@app.post("/api/kanban/dispatch")
def kanban_dispatch():
append_audit({"action": "kanban_dispatch_triggered"})
return {"status": "dispatch_triggered", "message": "Dispatcher notified"}
dispatched = []
for task in load_kanban_tasks():
if task.get("status") in ("todo", "ready") and task.get("assignee") in KANBAN_AGENTS:
dispatch_kanban_task(task["id"])
dispatched.append(task["id"])
append_audit({"action": "kanban_dispatch_triggered", "task_ids": dispatched})
return {"status": "dispatch_triggered", "dispatched": dispatched, "message": f"Dispatched {len(dispatched)} task(s)"}
@app.post("/api/kanban/tasks/{task_id}/specify")
def kanban_specify_task(task_id: str):
path = KANBAN_DIR / f"{task_id}.json"
path = kanban_task_path(task_id)
if not path.exists():
raise HTTPException(404, "Task not found")
task = json.loads(path.read_text())
@ -910,7 +1031,7 @@ def kanban_specify_task(task_id: str):
@app.post("/api/kanban/tasks/{task_id}/decompose")
def kanban_decompose_task(task_id: str):
path = KANBAN_DIR / f"{task_id}.json"
path = kanban_task_path(task_id)
if not path.exists():
raise HTTPException(404, "Task not found")
task = json.loads(path.read_text())

View File

@ -36,11 +36,36 @@ $pythonArgs = @($python.Args)
& $pythonCommand @pythonArgs -m pip install -r requirements.txt --quiet
$port = & $pythonCommand @pythonArgs -c "import json; f=open('data/settings.json'); d=json.load(f); print(d.get('dashboard',{}).get('port',8080)); f.close()" 2>$null
if (-not $port) { $port = "8080" }
$configuredPort = & $pythonCommand @pythonArgs -c "import json; f=open('data/settings.json'); d=json.load(f); print(int(d.get('dashboard',{}).get('port',8080))); f.close()" 2>$null
if (-not $configuredPort) { $configuredPort = "8080" }
$configuredPort = [int]$configuredPort
function Find-FreePort {
param([int]$StartPort, [int]$MaxAttempts = 20)
for ($p = $StartPort; $p -lt $StartPort + $MaxAttempts; $p++) {
$listener = $null
try {
$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Parse("127.0.0.1"), $p)
$listener.Start()
return $p
} catch {
continue
} finally {
if ($listener) { $listener.Stop() }
}
}
throw "No free port found between $StartPort and $($StartPort + $MaxAttempts - 1). Close some other application or free a port and retry."
}
$port = Find-FreePort -StartPort $configuredPort
if ($port -ne $configuredPort) {
Write-Warning "Port $configuredPort is already in use (another app, or a leftover Agentic OS process) - using $port instead."
}
Write-Host "Dashboard: http://127.0.0.1:$port"
Write-Host "Press Ctrl+C to stop"
Write-Host ""
Start-Job -ScriptBlock { Start-Sleep -Seconds 2; Start-Process "http://127.0.0.1:$using:port" } | Out-Null
& $pythonCommand @pythonArgs server.py --port $port

View File

@ -24,11 +24,39 @@ fi
$PYTHON -m pip install -r requirements.txt --quiet 2>/dev/null
# Get port from settings or default
PORT=$($PYTHON -c "import json; f=open('data/settings.json'); d=json.load(f); print(d.get('dashboard',{}).get('port',8080)); f.close()" 2>/dev/null || echo "8080")
CONFIGURED_PORT=$($PYTHON -c "import json; f=open('data/settings.json'); d=json.load(f); print(d.get('dashboard',{}).get('port',8080)); f.close()" 2>/dev/null || echo "8080")
# Find a free port starting at CONFIGURED_PORT (in case it's already taken by another app)
PORT="$CONFIGURED_PORT"
FOUND=0
for _ in $(seq 1 20); do
if ! (exec 3<>"/dev/tcp/127.0.0.1/${PORT}") 2>/dev/null; then
FOUND=1
break
fi
exec 3>&- 2>/dev/null || true
PORT=$((PORT + 1))
done
if [ "$FOUND" -eq 0 ]; then
echo "ERROR: Could not find a free port after 20 attempts starting at ${CONFIGURED_PORT}."
exit 1
fi
if [ "$PORT" != "$CONFIGURED_PORT" ]; then
echo "WARNING: Port ${CONFIGURED_PORT} is already in use - using ${PORT} instead."
fi
echo "Dashboard: http://127.0.0.1:${PORT}"
echo "Press Ctrl+C to stop"
echo ""
# Best-effort auto-open browser once the server is up
( sleep 2
if command -v xdg-open &>/dev/null; then xdg-open "http://127.0.0.1:${PORT}" &>/dev/null
elif command -v open &>/dev/null; then open "http://127.0.0.1:${PORT}" &>/dev/null
fi
) &
# Start server
$PYTHON server.py --port "${PORT}"