diff --git a/api/endpoints/chat.php b/api/endpoints/chat.php
index 28bdae9..eaf2f9d 100644
--- a/api/endpoints/chat.php
+++ b/api/endpoints/chat.php
@@ -760,6 +760,21 @@ if (!$reply) {
if ($ov > 0) $parts[] = $ov . ' overdue task' . ($ov > 1 ? 's' : '') . ' need attention';
$ai = (int)($email_actions['cnt'] ?? 0);
if ($ai > 0) $parts[] = $ai . ' email' . ($ai > 1 ? 's' : '') . ' require action';
+ // Include active directive progress summary
+ $active_dirs = JarvisDB::query(
+ "SELECT d.title,
+ COALESCE(SUM(kr.current_value),0) AS cur,
+ COALESCE(SUM(kr.target_value),1) AS tgt
+ FROM directives d
+ LEFT JOIN directive_key_results kr ON kr.directive_id=d.id
+ WHERE d.status='active'
+ GROUP BY d.id
+ ORDER BY d.priority DESC LIMIT 3"
+ ) ?? [];
+ if ($active_dirs) {
+ $dp = array_map(fn($d) => $d['title'] . ' (' . round($d['cur'] / max($d['tgt'],1) * 100) . '%)', $active_dirs);
+ $parts[] = count($active_dirs) . ' active directive' . (count($active_dirs) > 1 ? 's' : '') . ': ' . implode(', ', $dp);
+ }
$reply = $parts
? "Good morning, {$userAddr}. " . implode('. ', $parts) . '.'
: "Good morning, {$userAddr}. Your schedule is clear — no tasks, appointments, or email actions pending today.";
@@ -1355,6 +1370,31 @@ if (!$reply) {
}
}
+// ── Tier 0.9i: Directives — review objectives, progress check ─────────────────
+if (!$reply) {
+ $dirReviewPatterns = [
+ '/^(?:jarvis[,\s]+)?(?:review\s+(?:my\s+)?(?:directives?|objectives?|goals?|OKRs?))/i',
+ '/^(?:jarvis[,\s]+)?(?:how\s+am\s+i\s+doing\s+on\s+(?:my\s+)?(?:directives?|objectives?|goals?))/i',
+ '/^(?:jarvis[,\s]+)?(?:directives?\s+(?:review|status|update|progress|briefing))/i',
+ '/^(?:jarvis[,\s]+)?(?:OKR\s+(?:review|update|status))/i',
+ '/^(?:jarvis[,\s]+)?(?:what(?:\'s|\s+is)\s+(?:my\s+)?(?:progress|status)\s+on\s+(?:my\s+)?(?:directives?|goals?|objectives?))/i',
+ ];
+ foreach ($dirReviewPatterns as $pat) {
+ if (preg_match($pat, $message)) {
+ $arcRes = arcSubmitJob('directive_review', ['provider' => 'claude'], $sessionId);
+ if (isset($arcRes['job_id'])) {
+ $arcJobId = $arcRes['job_id'];
+ $reply = "◈ DIRECTIVE REVIEW INITIATED (Job #{$arcJobId}). I'm analyzing your active objectives and key results now, {$userAddr}. Stand by for your progress briefing.";
+ $source = 'arc:directive_review';
+ } else {
+ $reply = "Directive review is offline, {$userAddr}. Arc Reactor may be unavailable.";
+ $source = 'arc:offline';
+ }
+ break;
+ }
+ }
+}
+
// ── Tier 1: Intent Engine (instant, no LLM) ───────────────────────────────
if (!$reply) {
$matched = KBEngine::match($message);
diff --git a/api/endpoints/directives.php b/api/endpoints/directives.php
new file mode 100644
index 0000000..9a75f05
--- /dev/null
+++ b/api/endpoints/directives.php
@@ -0,0 +1,171 @@
+ 0)
+ ? (float)round($r['kr_current_sum'] / $r['kr_target_sum'] * 100, 1)
+ : 0;
+ }
+ unset($r);
+ echo json_encode(['directives' => $rows]);
+ break;
+
+ // GET directives/get?id=X — full directive with key results and links
+ case 'get':
+ $id = (int)($_GET['id'] ?? 0);
+ if (!$id) { echo json_encode(['error' => 'Missing id']); break; }
+ $d = JarvisDB::single("SELECT * FROM directives WHERE id=?", [$id]);
+ if (!$d) { echo json_encode(['error' => 'Not found']); break; }
+ $krs = JarvisDB::query("SELECT * FROM directive_key_results WHERE directive_id=? ORDER BY id ASC", [$id]) ?: [];
+ $links = JarvisDB::query(
+ "SELECT dl.*, t.title AS task_title, a.title AS appt_title
+ FROM directive_links dl
+ LEFT JOIN tasks t ON dl.link_type='task' AND t.id=dl.link_id
+ LEFT JOIN appointments a ON dl.link_type='appointment' AND a.id=dl.link_id
+ WHERE dl.directive_id=?
+ ORDER BY dl.created_at DESC",
+ [$id]
+ ) ?: [];
+ $sum_cur = array_sum(array_column($krs, 'current_value'));
+ $sum_tgt = array_sum(array_column($krs, 'target_value'));
+ $d['progress'] = ($sum_tgt > 0) ? round($sum_cur / $sum_tgt * 100, 1) : 0;
+ $d['key_results'] = $krs;
+ $d['links'] = $links;
+ echo json_encode($d);
+ break;
+
+ // POST directives/save — create or update directive + key results
+ case 'save':
+ $id = (int)($data['id'] ?? 0);
+ $title = trim($data['title'] ?? '');
+ $description = trim($data['description'] ?? '');
+ $category = $data['category'] ?? 'work';
+ $status = $data['status'] ?? 'active';
+ $priority = (int)($data['priority'] ?? 5);
+ $target_date = !empty($data['target_date']) ? $data['target_date'] : null;
+ if (!$title) { echo json_encode(['error' => 'Title required']); break; }
+ if ($id) {
+ JarvisDB::execute(
+ "UPDATE directives SET title=?,description=?,category=?,status=?,priority=?,target_date=?,updated_at=NOW() WHERE id=?",
+ [$title,$description,$category,$status,$priority,$target_date,$id]
+ );
+ } else {
+ $id = JarvisDB::insert(
+ "INSERT INTO directives (title,description,category,status,priority,target_date) VALUES (?,?,?,?,?,?)",
+ [$title,$description,$category,$status,$priority,$target_date]
+ );
+ }
+ // Replace key results if provided
+ if (isset($data['key_results']) && is_array($data['key_results'])) {
+ JarvisDB::execute("DELETE FROM directive_key_results WHERE directive_id=?", [$id]);
+ foreach ($data['key_results'] as $kr) {
+ $krtitle = trim($kr['title'] ?? ''); if (!$krtitle) continue;
+ JarvisDB::execute(
+ "INSERT INTO directive_key_results (directive_id,title,current_value,target_value,unit) VALUES (?,?,?,?,?)",
+ [$id, $krtitle, (float)($kr['current_value']??0), (float)($kr['target_value']??100), $kr['unit']??'%']
+ );
+ }
+ }
+ echo json_encode(['ok' => true, 'id' => $id]);
+ break;
+
+ // POST directives/delete?id=X
+ case 'delete':
+ $id = (int)($_GET['id'] ?? $data['id'] ?? 0);
+ if (!$id) { echo json_encode(['error' => 'Missing id']); break; }
+ JarvisDB::execute("DELETE FROM directive_key_results WHERE directive_id=?", [$id]);
+ JarvisDB::execute("DELETE FROM directive_links WHERE directive_id=?", [$id]);
+ JarvisDB::execute("DELETE FROM directives WHERE id=?", [$id]);
+ echo json_encode(['ok' => true]);
+ break;
+
+ // POST directives/key_result_update — update a single KR's current value
+ case 'key_result_update':
+ $krid = (int)($data['id'] ?? 0);
+ $value = (float)($data['current_value'] ?? 0);
+ if (!$krid) { echo json_encode(['error' => 'Missing kr id']); break; }
+ JarvisDB::execute(
+ "UPDATE directive_key_results SET current_value=?, updated_at=NOW() WHERE id=?",
+ [$value, $krid]
+ );
+ echo json_encode(['ok' => true]);
+ break;
+
+ // POST directives/link — link a task or appointment to a directive
+ case 'link':
+ $did = (int)($data['directive_id'] ?? 0);
+ $link_type = $data['link_type'] ?? 'task';
+ $link_id = (int)($data['link_id'] ?? 0);
+ $note = trim($data['note'] ?? '');
+ if (!$did) { echo json_encode(['error' => 'Missing directive_id']); break; }
+ JarvisDB::execute(
+ "INSERT INTO directive_links (directive_id,link_type,link_id,note) VALUES (?,?,?,?)",
+ [$did, $link_type, $link_id ?: null, $note]
+ );
+ echo json_encode(['ok' => true]);
+ break;
+
+ // POST directives/unlink?id=X — remove a link
+ case 'unlink':
+ $lid = (int)($_GET['id'] ?? $data['id'] ?? 0);
+ if (!$lid) { echo json_encode(['error' => 'Missing id']); break; }
+ JarvisDB::execute("DELETE FROM directive_links WHERE id=?", [$lid]);
+ echo json_encode(['ok' => true]);
+ break;
+
+ // GET directives/summary — compact progress snapshot for chat/briefing injection
+ case 'summary':
+ $rows = JarvisDB::query(
+ "SELECT d.id, d.title, d.category, d.target_date,
+ COALESCE(SUM(kr.current_value),0) AS kr_cur,
+ COALESCE(SUM(kr.target_value),1) AS kr_tgt
+ FROM directives d
+ LEFT JOIN directive_key_results kr ON kr.directive_id=d.id
+ WHERE d.status='active'
+ GROUP BY d.id
+ ORDER BY d.priority DESC, d.target_date ASC
+ LIMIT 10"
+ ) ?: [];
+ $summary = [];
+ foreach ($rows as $r) {
+ $pct = round($r['kr_cur'] / max($r['kr_tgt'], 1) * 100, 0);
+ $summary[] = [
+ 'id' => $r['id'],
+ 'title' => $r['title'],
+ 'category' => $r['category'],
+ 'target_date' => $r['target_date'],
+ 'progress' => $pct,
+ ];
+ }
+ echo json_encode(['directives' => $summary]);
+ break;
+
+ default:
+ http_response_code(404);
+ echo json_encode(['error' => "Unknown directives action: {$action}"]);
+}
diff --git a/public_html/admin/index.php b/public_html/admin/index.php
index f0fe42b..8f042c2 100644
--- a/public_html/admin/index.php
+++ b/public_html/admin/index.php
@@ -593,6 +593,111 @@ if ($action) {
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: ['error' => 'Arc Reactor unreachable']);
+ // ── DIRECTIVES ───────────────────────────────────────────────────────
+ case 'directive_list':
+ $status = $_GET['status'] ?? 'active';
+ $category = $_GET['category'] ?? '';
+ $where = '1=1'; $params = [];
+ if ($status && $status !== 'all') { $where .= ' AND d.status=?'; $params[] = $status; }
+ if ($category) { $where .= ' AND d.category=?'; $params[] = $category; }
+ $rows = JarvisDB::query(
+ "SELECT d.*,
+ COUNT(kr.id) AS kr_count,
+ COALESCE(SUM(kr.current_value),0) AS kr_current_sum,
+ COALESCE(SUM(kr.target_value),0) AS kr_target_sum,
+ (SELECT COUNT(*) FROM directive_links dl WHERE dl.directive_id=d.id) AS link_count
+ FROM directives d
+ LEFT JOIN directive_key_results kr ON kr.directive_id=d.id
+ WHERE {$where}
+ GROUP BY d.id
+ ORDER BY d.priority DESC, d.target_date ASC, d.created_at DESC",
+ $params
+ ) ?: [];
+ foreach ($rows as &$r) {
+ $r['progress'] = ($r['kr_target_sum'] > 0)
+ ? (float)round($r['kr_current_sum'] / $r['kr_target_sum'] * 100, 1)
+ : 0;
+ }
+ j(['directives' => $rows]);
+
+ case 'directive_get':
+ $id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id');
+ $d = JarvisDB::single("SELECT * FROM directives WHERE id=?", [$id]);
+ if (!$d) bad('Not found', 404);
+ $krs = JarvisDB::query("SELECT * FROM directive_key_results WHERE directive_id=? ORDER BY id", [$id]) ?: [];
+ $links = JarvisDB::query(
+ "SELECT dl.*, COALESCE(t.title,a.title) AS linked_title
+ FROM directive_links dl
+ LEFT JOIN tasks t ON dl.link_type='task' AND t.id=dl.link_id
+ LEFT JOIN appointments a ON dl.link_type='appointment' AND a.id=dl.link_id
+ WHERE dl.directive_id=? ORDER BY dl.created_at DESC",
+ [$id]
+ ) ?: [];
+ $cur = array_sum(array_column($krs,'current_value'));
+ $tgt = array_sum(array_column($krs,'target_value'));
+ $d['progress'] = $tgt > 0 ? round($cur/$tgt*100,1) : 0;
+ $d['key_results'] = $krs;
+ $d['links'] = $links;
+ j($d);
+
+ case 'directive_save':
+ $id = (int)($_GET['id'] ?? 0);
+ $body = file_get_contents('php://input');
+ $data_in = json_decode($body, true) ?: [];
+ $title = trim($data_in['title'] ?? '');
+ $description = trim($data_in['description'] ?? '');
+ $category = $data_in['category'] ?? 'work';
+ $status = $data_in['status'] ?? 'active';
+ $priority = (int)($data_in['priority'] ?? 5);
+ $target_date = $data_in['target_date'] ?? null;
+ $krs = $data_in['key_results'] ?? [];
+ if (!$title) bad('Title required');
+ if ($id) {
+ JarvisDB::execute(
+ "UPDATE directives SET title=?,description=?,category=?,status=?,priority=?,target_date=?,updated_at=NOW() WHERE id=?",
+ [$title,$description,$category,$status,$priority,$target_date?:null,$id]
+ );
+ } else {
+ $id = JarvisDB::insert(
+ "INSERT INTO directives (title,description,category,status,priority,target_date) VALUES (?,?,?,?,?,?)",
+ [$title,$description,$category,$status,$priority,$target_date?:null]
+ );
+ }
+ if (is_array($krs)) {
+ JarvisDB::execute("DELETE FROM directive_key_results WHERE directive_id=?", [$id]);
+ foreach ($krs as $kr) {
+ $krt = trim($kr['title'] ?? ''); if (!$krt) continue;
+ JarvisDB::execute(
+ "INSERT INTO directive_key_results (directive_id,title,current_value,target_value,unit) VALUES (?,?,?,?,?)",
+ [$id,$krt,(float)($kr['current_value']??0),(float)($kr['target_value']??100),$kr['unit']??'%']
+ );
+ }
+ }
+ j(['ok' => true, 'id' => $id]);
+
+ case 'directive_delete':
+ $id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id');
+ JarvisDB::execute("DELETE FROM directive_key_results WHERE directive_id=?", [$id]);
+ JarvisDB::execute("DELETE FROM directive_links WHERE directive_id=?", [$id]);
+ JarvisDB::execute("DELETE FROM directives WHERE id=?", [$id]);
+ j(['ok' => true]);
+
+ case 'arc_action':
+ $body = file_get_contents('php://input');
+ $d = json_decode($body, true) ?: [];
+ $type = $d['action'] === 'job_create' ? ($d['type'] ?? '') : '';
+ $payload = $d['payload'] ?? [];
+ $pri = (int)($d['priority'] ?? 5);
+ if (!$type) bad('Missing type');
+ $ch = curl_init('http://127.0.0.1:7474/job');
+ curl_setopt_array($ch, [
+ CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>10, CURLOPT_POST=>true,
+ CURLOPT_POSTFIELDS => json_encode(['type'=>$type,'payload'=>$payload,'priority'=>$pri,'created_by'=>'admin']),
+ CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
+ ]);
+ $raw = curl_exec($ch); curl_close($ch);
+ j(json_decode($raw, true) ?: ['error'=>'Arc Reactor unreachable']);
+
// ── MISSION OPS ──────────────────────────────────────────────────────
case 'mission_list':
$ch = curl_init('http://127.0.0.1:7474/missions');
@@ -994,6 +1099,7 @@ select.filter-sel:focus{border-color:var(--cyan)}
◈ VISION PROTOCOL
◈ GUARDIAN MODE
◈ MISSION OPS
+ ◈ DIRECTIVES
INFO
SITES
USERS
@@ -1458,6 +1564,99 @@ select.filter-sel:focus{border-color:var(--cyan)}
+
+
+
◈ MISSION DIRECTIVES — OBJECTIVES & KEY RESULTS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
◈ DIRECTIVE EDITOR
+
+
+
+
+
+
+
STATUS
+
+
+
+
+
+
+
CATEGORY
+
+
+
+
+
+
+
+
+
+
+
KEY RESULTS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1572,6 +1771,7 @@ function loadTab(tab) {
triage: loadTriage,
outbox: loadOutbox,
missions: loadMissions,
+ directives: loadDirectives,
vision: loadVision,
guardian: loadGuardian,
tasks: loadTasks,
@@ -3244,6 +3444,193 @@ async function missionToggle(id, enabled) {
else toast('Toggle failed', 'err');
}
+// ── DIRECTIVES ───────────────────────────────────────────────────────────────
+
+let _dirKRs = [];
+let _dirKRIdx = 0;
+
+const CAT_COLORS = {work:'var(--cyan)',personal:'#a78bfa',health:'#00ff88',finance:'#ffd700',home:'var(--orange)',other:'var(--text-dim)'};
+
+async function loadDirectives() {
+ const el = document.getElementById('directives-list');
+ if (!el) return;
+ const status = document.getElementById('dir-status-filter')?.value || 'active';
+ const category = document.getElementById('dir-cat-filter')?.value || '';
+ const params = {status};
+ if (category) params.category = category;
+ const d = await api('directive_list', params);
+ const list = d.directives || [];
+ document.getElementById('directives-count').textContent = list.length + ' DIRECTIVES';
+ if (!list.length) {
+ el.innerHTML = 'No directives found. Click + NEW DIRECTIVE to create one.
';
+ return;
+ }
+ const rows = list.map(dir => {
+ const pct = Math.min(100, Math.round(dir.progress || 0));
+ const catColor = CAT_COLORS[dir.category] || 'var(--text-dim)';
+ const daysLeft = dir.target_date
+ ? Math.ceil((new Date(dir.target_date) - new Date()) / 86400000)
+ : null;
+ const dueBadge = daysLeft !== null
+ ? `
+ ${daysLeft<0?'OVERDUE '+Math.abs(daysLeft)+'d':daysLeft+'d left'}`
+ : '';
+ const statusBadge = dir.status !== 'active'
+ ? `[${dir.status.toUpperCase()}]`
+ : '';
+ return `
+ |
+ ${esc(dir.title)}${statusBadge}
+ ${dir.category.toUpperCase()} · P${dir.priority}
+ |
+
+
+ |
+ ${dueBadge} |
+ ${dir.kr_count||0} KRs · ${dir.link_count||0} links |
+
+
+
+ |
+
`;
+ }).join('');
+ el.innerHTML = `| OBJECTIVE | PROGRESS | DUE | DETAILS | ACTIONS |
${rows}
`;
+}
+
+function directiveNew() {
+ _dirKRs = []; _dirKRIdx = 0;
+ document.getElementById('dir-id').value = '';
+ document.getElementById('dir-title').value = '';
+ document.getElementById('dir-desc').value = '';
+ document.getElementById('dir-category').value = 'work';
+ document.getElementById('dir-status').value = 'active';
+ document.getElementById('dir-priority').value = 5;
+ document.getElementById('dir-target-date').value = '';
+ document.getElementById('dir-editor-title').textContent = '◈ NEW DIRECTIVE';
+ document.getElementById('dir-del-btn').style.display = 'none';
+ document.getElementById('dir-save-status').textContent = '';
+ _renderDirKRs();
+ document.getElementById('directive-editor').style.display = 'block';
+ document.getElementById('directive-editor').scrollIntoView({behavior:'smooth'});
+}
+
+async function directiveEdit(id) {
+ const d = await api('directive_get', {id});
+ if (d.error) { toast('Load failed: ' + d.error, 'err'); return; }
+ document.getElementById('dir-id').value = d.id;
+ document.getElementById('dir-title').value = d.title || '';
+ document.getElementById('dir-desc').value = d.description || '';
+ document.getElementById('dir-category').value = d.category || 'work';
+ document.getElementById('dir-status').value = d.status || 'active';
+ document.getElementById('dir-priority').value = d.priority || 5;
+ document.getElementById('dir-target-date').value = d.target_date || '';
+ document.getElementById('dir-editor-title').textContent = '◈ EDIT — ' + esc(d.title);
+ document.getElementById('dir-del-btn').style.display = '';
+ document.getElementById('dir-save-status').textContent = '';
+ _dirKRs = (d.key_results || []).map(kr => ({
+ id: ++_dirKRIdx, dbid: kr.id,
+ title: kr.title, current_value: kr.current_value,
+ target_value: kr.target_value, unit: kr.unit || '%',
+ }));
+ _renderDirKRs();
+ document.getElementById('directive-editor').style.display = 'block';
+ document.getElementById('directive-editor').scrollIntoView({behavior:'smooth'});
+}
+
+function dirAddKR() {
+ _dirKRIdx++;
+ _dirKRs.push({id: _dirKRIdx, dbid: null, title:'', current_value:0, target_value:100, unit:'%'});
+ _renderDirKRs();
+}
+
+function dirRemoveKR(sid) {
+ _dirKRs = _dirKRs.filter(k => k.id !== sid);
+ _renderDirKRs();
+}
+
+function _krUpdate(sid, field, val) {
+ const k = _dirKRs.find(x => x.id === sid);
+ if (k) k[field] = val;
+}
+
+function _renderDirKRs() {
+ const el = document.getElementById('dir-kr-list');
+ if (!el) return;
+ if (!_dirKRs.length) {
+ el.innerHTML = 'No key results yet — click + ADD KEY RESULT
';
+ return;
+ }
+ el.innerHTML = _dirKRs.map(k => `
+
+
+
+
+
+
+
+ `).join('');
+}
+
+async function directiveSave() {
+ const id = parseInt(document.getElementById('dir-id')?.value || 0) || null;
+ const title = document.getElementById('dir-title')?.value.trim();
+ const desc = document.getElementById('dir-desc')?.value.trim();
+ const category = document.getElementById('dir-category')?.value;
+ const status = document.getElementById('dir-status')?.value;
+ const priority = parseInt(document.getElementById('dir-priority')?.value || 5);
+ const target_date = document.getElementById('dir-target-date')?.value || '';
+ const stat = document.getElementById('dir-save-status');
+ if (!title) { if (stat) stat.textContent = '✗ Title required'; return; }
+ const key_results = _dirKRs.map(k => ({
+ title: k.title, current_value: parseFloat(k.current_value)||0,
+ target_value: parseFloat(k.target_value)||1, unit: k.unit||'%',
+ })).filter(k => k.title.trim());
+ if (stat) stat.textContent = '◈ SAVING…';
+ const d = await fetch(`admin?action=directive_save${id?'&id='+id:''}`, {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({title, description:desc, category, status, priority, target_date, key_results}),
+ }).then(r => r.json()).catch(() => ({error: 'request failed'}));
+ if (d.ok) {
+ if (stat) stat.textContent = '◈ SAVED ✓';
+ toast('Directive saved', 'ok');
+ loadDirectives();
+ } else {
+ if (stat) stat.textContent = '✗ ' + (d.error || 'Save failed');
+ toast('Save failed', 'err');
+ }
+}
+
+async function directiveDelete() {
+ const id = parseInt(document.getElementById('dir-id')?.value || 0);
+ if (!id || !confirm('Delete this directive and all its key results?')) return;
+ const d = await api('directive_delete', {id});
+ if (d.ok) {
+ toast('Directive deleted', 'ok');
+ document.getElementById('directive-editor').style.display = 'none';
+ loadDirectives();
+ } else toast('Delete failed', 'err');
+}
+
+async function directiveReviewAI(id) {
+ toast('◈ Dispatching AI directive review…', 'ok');
+ const payload = id ? {directive_id: id, provider: 'claude'} : {provider: 'claude'};
+ const res = await fetch('admin?action=arc_action', {
+ method: 'POST',
+ headers: {'Content-Type':'application/json'},
+ body: JSON.stringify({action:'job_create', type:'directive_review', payload, priority: 6}),
+ }).then(r => r.json()).catch(() => ({}));
+ if (res.job_id) toast('Review job #' + res.job_id + ' started — results will appear in JARVIS chat', 'ok');
+ else toast('Failed: ' + (res.error||'Arc offline'), 'err');
+}
+
+async function directiveReviewSingle(id) { return directiveReviewAI(id); }
+
// ── PLANNER ─────────────────────────────────────────────────────────────────
const _PRI_COLOR = {urgent:'var(--red)',high:'var(--orange)',normal:'var(--text)',low:'var(--border2)'};
diff --git a/public_html/api.php b/public_html/api.php
index 54ddff2..5557fc8 100644
--- a/public_html/api.php
+++ b/public_html/api.php
@@ -102,6 +102,9 @@ switch ($endpoint) {
case "arc":
require __DIR__ . "/../api/endpoints/arc.php";
break;
+ case "directives":
+ require __DIR__ . "/../api/endpoints/directives.php";
+ break;
case "calendar":
require __DIR__ . '/../api/endpoints/calendar_sync.php';
break;
diff --git a/public_html/index.html b/public_html/index.html
index 5db7d9b..37ede2c 100644
--- a/public_html/index.html
+++ b/public_html/index.html
@@ -948,6 +948,20 @@ body::after{
.comms-compose-field{width:100%;background:rgba(0,212,255,0.04);border:1px solid var(--panel-border);border-radius:3px;padding:6px 8px;color:var(--text);font-family:var(--font-mono);font-size:0.6rem;box-sizing:border-box;margin-bottom:7px}
.comms-compose-field:focus{outline:none;border-color:var(--cyan)}
.comms-compose-actions{display:flex;gap:6px;margin-top:8px}
+/* ── DIRECTIVES HUD ──────────────────────────────────────────────── */
+.dir-card{background:rgba(0,212,255,0.03);border:1px solid var(--panel-border);border-radius:var(--r);margin-bottom:7px;overflow:hidden}
+.dir-card-head{display:flex;align-items:center;gap:8px;padding:8px 10px;cursor:pointer;user-select:none}
+.dir-card-head:hover{background:rgba(0,212,255,0.06)}
+.dir-card-title{font-family:var(--font-display);font-size:0.6rem;letter-spacing:1px;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.dir-card-body{display:none;padding:0 10px 10px;border-top:1px solid var(--panel-border)}
+.dir-card.open .dir-card-body{display:block}
+.dir-progress-bar{height:5px;background:rgba(255,255,255,0.08);border-radius:3px;margin:6px 0}
+.dir-progress-fill{height:100%;border-radius:3px;transition:width 0.4s ease}
+.dir-kr-row{display:flex;align-items:center;gap:6px;margin:4px 0;font-family:var(--font-mono);font-size:0.52rem;color:var(--text-dim)}
+.dir-kr-bar{flex:1;height:3px;background:rgba(255,255,255,0.06);border-radius:2px}
+.dir-kr-fill{height:100%;border-radius:2px;background:rgba(0,212,255,0.5)}
+.dir-admin-btn{width:100%;background:rgba(0,212,255,0.06);border:1px solid rgba(0,212,255,0.3);border-radius:4px;padding:5px;color:var(--cyan);font-family:var(--font-display);font-size:0.52rem;letter-spacing:2px;cursor:pointer;margin-bottom:7px}
+.dir-admin-btn:hover{background:rgba(0,212,255,0.12)}
/* ── MISSION OPS HUD ─────────────────────────────────────────────── */
.mission-card{background:rgba(0,212,255,0.03);border:1px solid var(--panel-border);border-radius:var(--r);margin-bottom:7px;overflow:hidden}
.mission-card-head{display:flex;align-items:center;gap:8px;padding:8px 10px;cursor:pointer;user-select:none}
@@ -1193,6 +1207,7 @@ body::after{
COMMS
GUARDIAN
MISSIONS
+ DIRECTIVES
@@ -1223,6 +1238,9 @@ body::after{
+
@@ -3143,7 +3161,8 @@ function switchTab(name) {
if (name === 'intel') loadIntel();
if (name === 'comms') { loadComms(); loadCommsOutbox(); }
if (name === 'guardian') loadGuardian();
- if (name === 'missions') loadMissionsHud();
+ if (name === 'missions') loadMissionsHud();
+ if (name === 'directives') loadDirectivesHud();
if (name === 'alerts') loadAlerts();
}
@@ -4272,6 +4291,75 @@ async function hudRunMission(id) {
}
}
+// ── DIRECTIVES HUD ────────────────────────────────────────────────────────────
+let _dirOpenCards = new Set();
+
+async function loadDirectivesHud() {
+ const el = document.getElementById('directives-hud');
+ if (!el) return;
+ try {
+ const d = await api('directives/list?status=active');
+ const list = (d.directives || []);
+
+ let html = '';
+
+ if (!list.length) {
+ html += '◈ NO ACTIVE DIRECTIVES
Create objectives in Admin → Directives
';
+ el.innerHTML = html;
+ return;
+ }
+
+ const catColors = {work:'var(--cyan)',personal:'#a78bfa',health:'#00ff88',finance:'#ffd700',home:'var(--panel-border)',other:'var(--text-dim)'};
+ for (const dir of list) {
+ const pct = Math.min(100, Math.round(dir.progress || 0));
+ const isOpen = _dirOpenCards.has(dir.id);
+ const color = catColors[dir.category] || 'var(--cyan)';
+ const fillColor = pct >= 80 ? '#00ff88' : pct >= 40 ? '#ffd700' : '#ff6644';
+ const daysLeft = dir.target_date
+ ? Math.ceil((new Date(dir.target_date) - new Date()) / 86400000) : null;
+ const dueTxt = daysLeft !== null
+ ? (daysLeft < 0 ? `OVERDUE ${Math.abs(daysLeft)}d` : `${daysLeft}d left`)
+ : '';
+ const dueColor = daysLeft !== null && daysLeft < 0 ? '#ff2244' : daysLeft < 14 ? '#ffd700' : 'var(--text-dim)';
+
+ html += `
+
+ ${dir.category.toUpperCase()}
+ ${escHtml(dir.title)}
+ ${pct}%
+ ${dueTxt ? `${dueTxt}` : ''}
+
+
+
+
${dir.kr_count||0} KEY RESULTS · ${dir.link_count||0} LINKED ITEMS
+
+
+
`;
+ }
+ el.innerHTML = html;
+ } catch(e) {
+ if (el) el.innerHTML = 'DIRECTIVES OFFLINE
';
+ }
+}
+
+function toggleDirCard(id) {
+ const card = document.getElementById('dir-card-' + id);
+ if (!card) return;
+ if (_dirOpenCards.has(id)) _dirOpenCards.delete(id);
+ else _dirOpenCards.add(id);
+ card.classList.toggle('open');
+}
+
+async function hudDirectiveReview(id) {
+ const res = await api('arc?action=job_create', 'POST', {
+ type: 'directive_review', payload: {directive_id: id, provider: 'claude'}, priority: 6,
+ });
+ if (res.job_id) {
+ addMessage('jarvis', `◈ DIRECTIVE REVIEW initiated (Job #${res.job_id}). Analyzing objectives and key results now. Results will appear here shortly.`);
+ speak(`Directive review underway. I'll brief you on your progress in a moment.`);
+ }
+}
+
async function loadAgents() {
const [listData, metricsData] = await Promise.all([
api('agent/list'),