Phase 8: Mission Directives — OKR/goal tracking with AI review

- DB: directives, directive_key_results, directive_links tables
- reactor.py v8.0.0: directive_review handler — fetches active directives + KRs + links, Claude generates executive progress briefing, injects into conversations
- directives.php: new API endpoint (list/get/save/delete/key_result_update/link/summary)
- api.php: routes directives/* endpoint
- admin/index.php: Directives nav + tab — objective cards with progress bars, editor with multi-KR builder (title/current/target/unit), AI Review button per directive and global
- index.html: DIRECTIVES tab — collapsible objective cards with progress bars, KR counts, AI Review button, link to admin
- chat.php: Tier 0.9i directive review detection; daily briefing now includes active directive progress %

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 11:59:44 +00:00
parent b6c417948e
commit aaf07edacb
5 changed files with 690 additions and 1 deletions
+387
View File
@@ -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)}
<div class="nav-item" data-tab="vision" onclick="nav(this)">◈ VISION PROTOCOL</div>
<div class="nav-item" data-tab="guardian" onclick="nav(this)" id="nav-guardian">◈ GUARDIAN MODE</div>
<div class="nav-item" data-tab="missions" onclick="nav(this)">◈ MISSION OPS</div>
<div class="nav-item" data-tab="directives" onclick="nav(this)">◈ DIRECTIVES</div>
<div class="nav-section">INFO</div>
<div class="nav-item" data-tab="sites" onclick="nav(this)">SITES</div>
<div class="nav-item" data-tab="users" onclick="nav(this)">USERS</div>
@@ -1458,6 +1564,99 @@ select.filter-sel:focus{border-color:var(--cyan)}
</div>
</div>
<!-- DIRECTIVES -->
<div class="tab" id="tab-directives">
<div class="page-title">◈ MISSION DIRECTIVES — OBJECTIVES &amp; KEY RESULTS</div>
<div style="display:flex;gap:10px;margin-bottom:16px;align-items:center;flex-wrap:wrap">
<button class="btn btn-sm btn-green" onclick="directiveNew()">+ NEW DIRECTIVE</button>
<button class="btn btn-sm" onclick="directiveReviewAI()">◈ AI REVIEW</button>
<button class="btn btn-sm" onclick="loadDirectives()">↻ REFRESH</button>
<select id="dir-status-filter" class="inp" style="width:auto;padding:4px 8px;font-size:0.65rem" onchange="loadDirectives()">
<option value="active">ACTIVE</option>
<option value="all">ALL</option>
<option value="paused">PAUSED</option>
<option value="complete">COMPLETE</option>
</select>
<select id="dir-cat-filter" class="inp" style="width:auto;padding:4px 8px;font-size:0.65rem" onchange="loadDirectives()">
<option value="">ALL CATEGORIES</option>
<option value="work">WORK</option>
<option value="personal">PERSONAL</option>
<option value="health">HEALTH</option>
<option value="finance">FINANCE</option>
<option value="home">HOME</option>
</select>
<div id="directives-count" style="margin-left:auto;font-family:var(--mono);font-size:0.65rem;color:var(--dim)"></div>
</div>
<div id="directives-list"><div class="loading">LOADING DIRECTIVES...</div></div>
<!-- Directive editor panel -->
<div id="directive-editor" style="display:none;margin-top:20px;border:1px solid var(--border);border-radius:6px;padding:16px;background:rgba(0,212,255,0.02)">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
<div id="dir-editor-title" style="font-family:var(--mono);font-size:0.75rem;letter-spacing:2px;color:var(--cyan)">◈ DIRECTIVE EDITOR</div>
<button class="btn btn-xs" onclick="document.getElementById('directive-editor').style.display='none'">✕ CLOSE</button>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:12px">
<div style="grid-column:1/3">
<div class="lbl">OBJECTIVE TITLE</div>
<input id="dir-title" class="inp" placeholder="What do you want to achieve?">
</div>
<div>
<div class="lbl">STATUS</div>
<select id="dir-status" class="inp">
<option value="active">Active</option>
<option value="paused">Paused</option>
<option value="complete">Complete</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:12px">
<div>
<div class="lbl">CATEGORY</div>
<select id="dir-category" class="inp">
<option value="work">Work</option>
<option value="personal">Personal</option>
<option value="health">Health</option>
<option value="finance">Finance</option>
<option value="home">Home</option>
<option value="other">Other</option>
</select>
</div>
<div>
<div class="lbl">PRIORITY (1-10)</div>
<input id="dir-priority" class="inp" type="number" min="1" max="10" value="5">
</div>
<div>
<div class="lbl">TARGET DATE</div>
<input id="dir-target-date" class="inp" type="date">
</div>
</div>
<div style="margin-bottom:14px">
<div class="lbl">DESCRIPTION</div>
<textarea id="dir-desc" class="inp" rows="2" placeholder="Context, why this matters..."></textarea>
</div>
<div style="margin-bottom:14px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<div class="lbl" style="margin:0">KEY RESULTS</div>
<button class="btn btn-xs btn-green" onclick="dirAddKR()">+ ADD KEY RESULT</button>
</div>
<div id="dir-kr-list"></div>
</div>
<input type="hidden" id="dir-id" value="">
<div style="display:flex;gap:8px">
<button class="btn btn-sm btn-green" onclick="directiveSave()">◈ SAVE</button>
<button id="dir-del-btn" class="btn btn-sm btn-red" style="display:none" onclick="directiveDelete()">✗ DELETE</button>
</div>
<div id="dir-save-status" style="font-family:var(--mono);font-size:0.6rem;color:var(--cyan);margin-top:6px;min-height:14px"></div>
</div>
</div>
</div><!-- /content -->
</div><!-- /main -->
</div><!-- /app -->
@@ -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 = '<div class="loading">No directives found. Click + NEW DIRECTIVE to create one.</div>';
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
? `<span style="font-family:var(--mono);font-size:0.55rem;color:${daysLeft<0?'var(--red)':daysLeft<14?'var(--orange)':'var(--text-dim)'}">
${daysLeft<0?'OVERDUE '+Math.abs(daysLeft)+'d':daysLeft+'d left'}</span>`
: '';
const statusBadge = dir.status !== 'active'
? `<span style="font-size:0.55rem;color:var(--dim);margin-left:4px">[${dir.status.toUpperCase()}]</span>`
: '';
return `<tr>
<td style="min-width:200px">
<div style="font-size:0.7rem;font-family:var(--mono)">${esc(dir.title)}${statusBadge}</div>
<div style="font-size:0.58rem;color:${catColor};margin-top:2px">${dir.category.toUpperCase()} · P${dir.priority}</div>
</td>
<td style="min-width:160px">
<div style="display:flex;align-items:center;gap:6px">
<div style="flex:1;height:6px;background:rgba(255,255,255,0.08);border-radius:3px">
<div style="width:${pct}%;height:100%;background:${pct>=80?'var(--green)':pct>=40?'var(--orange)':'var(--red)'};border-radius:3px"></div>
</div>
<span style="font-family:var(--mono);font-size:0.6rem;min-width:32px">${pct}%</span>
</div>
</td>
<td>${dueBadge}</td>
<td style="font-family:var(--mono);font-size:0.58rem;color:var(--dim)">${dir.kr_count||0} KRs · ${dir.link_count||0} links</td>
<td style="white-space:nowrap">
<button class="btn btn-xs btn-green" onclick="directiveEdit(${dir.id})">EDIT</button>
<button class="btn btn-xs" onclick="directiveReviewSingle(${dir.id})">◈ AI REVIEW</button>
</td>
</tr>`;
}).join('');
el.innerHTML = `<table><thead><tr><th>OBJECTIVE</th><th>PROGRESS</th><th>DUE</th><th>DETAILS</th><th>ACTIONS</th></tr></thead><tbody>${rows}</tbody></table>`;
}
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 = '<div style="font-size:0.6rem;color:var(--dim)">No key results yet — click + ADD KEY RESULT</div>';
return;
}
el.innerHTML = _dirKRs.map(k => `
<div style="display:grid;grid-template-columns:2fr 80px 80px 60px 28px;gap:6px;align-items:center;margin-bottom:6px">
<input class="inp" value="${esc(k.title)}" placeholder="Key result title" oninput="_krUpdate(${k.id},'title',this.value)">
<input class="inp" type="number" step="0.1" value="${k.current_value}" placeholder="Current" title="Current value" oninput="_krUpdate(${k.id},'current_value',parseFloat(this.value)||0)">
<input class="inp" type="number" step="0.1" value="${k.target_value}" placeholder="Target" title="Target value" oninput="_krUpdate(${k.id},'target_value',parseFloat(this.value)||1)">
<input class="inp" value="${esc(k.unit)}" placeholder="Unit" title="Unit (%, $, hrs...)" oninput="_krUpdate(${k.id},'unit',this.value)">
<button class="btn btn-xs btn-red" onclick="dirRemoveKR(${k.id})">✗</button>
</div>
`).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)'};