@staix/agent-hub 0.15.0 → 0.16.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.
- package/CHANGELOG.md +25 -0
- package/README.md +12 -8
- package/docs/agent-notes/README.md +4 -0
- package/docs/agent-notes/adapters.md +6 -3
- package/docs/agent-notes/budget.md +4 -2
- package/docs/agent-notes/daemon.md +9 -3
- package/docs/agent-notes/local-worker.md +2 -2
- package/docs/agent-notes/tasks.md +13 -1
- package/docs/agent-notes/tests.md +3 -0
- package/docs/dogfood.md +13 -4
- package/docs/events.md +1 -1
- package/docs/memory.md +78 -10
- package/docs/operations.md +116 -30
- package/docs/quickstart.md +8 -4
- package/docs/security.md +80 -18
- package/docs/smoke.md +7 -3
- package/docs/specs/2026-09-19-agent-hub-design.md +84 -8
- package/docs/verification/2026-10-24-always-ask-adapters.md +52 -0
- package/docs/verification/2026-10-24-amem-runtime.md +69 -0
- package/docs/verification/2026-10-24-codex-0164-reviewer-isolation.md +55 -0
- package/docs/verification/2026-10-24-delivery-holds-ac8.md +264 -0
- package/docs/verification/2026-10-24-hardcoded-values.md +2108 -0
- package/docs/verification/2026-10-25-0.16.0.md +82 -0
- package/docs/verified.json +26 -17
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +2085 -1702
- package/src/adapters/acp.ts +5 -2
- package/src/adapters/codex-appserver.ts +69 -1
- package/src/adapters/compat.ts +2 -2
- package/src/adapters/definitions/builtin-metadata.ts +3 -3
- package/src/adapters/local-worker.ts +5 -4
- package/src/adapters/registry-types.ts +1 -1
- package/src/cli/console-state.ts +20 -2
- package/src/cli/console-symbols.ts +3 -2
- package/src/cli/console-usage.ts +5 -2
- package/src/cli/console.ts +5 -1
- package/src/cli/help.ts +7 -5
- package/src/cli/identity.ts +3 -1
- package/src/cli/main.ts +44 -10
- package/src/cli/memory.ts +119 -7
- package/src/cli/output.ts +12 -4
- package/src/hub/action-class.ts +211 -17
- package/src/hub/action-enforce.ts +13 -5
- package/src/hub/action-policy.ts +19 -8
- package/src/hub/ask.ts +16 -4
- package/src/hub/board.ts +86 -31
- package/src/hub/budget-notes.ts +65 -0
- package/src/hub/budget.ts +42 -10
- package/src/hub/bus.ts +1 -1
- package/src/hub/code-facts.ts +56 -16
- package/src/hub/conductor.ts +5 -2
- package/src/hub/config-apply.ts +7 -0
- package/src/hub/control-client.ts +1 -1
- package/src/hub/daemon.ts +250 -49
- package/src/hub/envelope.ts +1 -1
- package/src/hub/events.ts +2 -2
- package/src/hub/hub-tools.ts +3 -3
- package/src/hub/kimi-usage.ts +12 -0
- package/src/hub/lifecycle.ts +10 -5
- package/src/hub/memory-code-head.ts +44 -0
- package/src/hub/memory-code-overlay.ts +119 -0
- package/src/hub/memory-plan-guard.ts +10 -1
- package/src/hub/memory-stale-anchors.ts +159 -0
- package/src/hub/memory-symbols.ts +74 -0
- package/src/hub/planning.ts +10 -4
- package/src/hub/read-tools.ts +21 -3
- package/src/hub/sdlc.ts +34 -0
- package/src/hub/settings.ts +17 -2
- package/src/hub/settled-roles.ts +15 -3
- package/src/hub/task-sweep.ts +25 -3
- package/src/hub/tasks.ts +353 -64
- package/src/hub/token-window.ts +15 -2
- package/src/hub/window-kind.ts +13 -0
- package/src/local/tools.ts +5 -5
- package/src/memory/brief.ts +22 -12
- package/src/memory/capture.ts +26 -40
- package/src/memory/client.ts +137 -65
- package/src/memory/recall.ts +13 -22
- package/src/ui/index.html +100 -18
package/src/ui/index.html
CHANGED
|
@@ -151,6 +151,13 @@ progress.critical::-moz-progress-bar{background:var(--red)}
|
|
|
151
151
|
/* Memory */
|
|
152
152
|
.memory-controls{display:flex;gap:var(--space-2);flex-wrap:wrap;padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--line)}
|
|
153
153
|
.memory-controls input,.memory-controls select{width:auto;flex:1 1 130px}
|
|
154
|
+
.memory-code-toggle{display:flex;align-items:center;gap:var(--space-2);flex:1 1 210px;min-height:44px}
|
|
155
|
+
.memory-controls .memory-code-toggle input{flex:0 0 auto;width:auto;accent-color:var(--accent)}
|
|
156
|
+
#memory-code-help{flex:1 1 220px;align-self:center}
|
|
157
|
+
.memory-row.code-neighbour-note .label{text-decoration:underline dotted;text-underline-offset:3px}
|
|
158
|
+
.mnode.code-neighbour-note circle{stroke:var(--focus);stroke-width:3}
|
|
159
|
+
.medge.derived{stroke:var(--ink);stroke-dasharray:6 4}
|
|
160
|
+
.memory-code-coverage{margin:0;padding:var(--space-2) var(--space-4);overflow-wrap:anywhere}
|
|
154
161
|
.memory-row{display:block;width:100%;text-align:left;border:0;border-bottom:1px solid var(--line);border-radius:0;padding:var(--space-3) var(--space-4);background:var(--panel)}
|
|
155
162
|
.memory-row[aria-pressed="true"]{background:var(--accent-soft);box-shadow:inset 3px 0 0 var(--accent)}
|
|
156
163
|
.memory-row:hover{background:var(--hover)}
|
|
@@ -440,7 +447,7 @@ main[data-stale]>:not(#notice):not(#disconnected){opacity:.6;transition:opacity
|
|
|
440
447
|
<section id="office-section" aria-labelledby="office-title" data-view="office" hidden><div class="section-head"><h2 id="office-title">Office</h2><div class="filters"><label for="office-view">Office mode</label><select id="office-view"><option value="off">Off</option><option value="flat">Flat</option><option value="isometric">Isometric</option></select></div></div><div id="office" class="content"></div></section>
|
|
441
448
|
<section aria-labelledby="stream-title" data-view="stream" hidden><div class="section-head"><h2 id="stream-title">Live stream</h2><div class="filters"><div id="stream-filters" class="chips" role="group" aria-label="Filter events by kind"></div><label><input id="follow" type="checkbox" checked> Follow latest</label></div></div><div id="stream" class="stream" tabindex="0" aria-label="Recent hub events"><p class="empty">Waiting for hub activity...</p></div><p id="stream-empty" class="empty" hidden>No events match this filter.</p></section>
|
|
442
449
|
|
|
443
|
-
<section aria-labelledby="memory-title" data-view="memory" hidden><div class="section-head"><h2 id="memory-title">Memory</h2><div class="filters"><div id="memory-tiers" class="chips" role="group" aria-label="Memory tier"></div><span id="memory-count" class="meta"></span></div></div><div class="memory-controls"><input id="memory-q" type="search" maxlength="200" placeholder="Search memory" aria-label="Search memory"><select id="memory-kind" aria-label="Filter memory by kind"><option value="">All kinds</option><option value="decision">Decisions</option><option value="contract">Contracts</option><option value="fail">Fails</option><option value="finding">Findings</option></select><select id="memory-peer" aria-label="Filter memory by peer"><option value="">All peers</option></select><select id="memory-machine" aria-label="Filter memory by machine"><option value="">All machines</option></select><select id="memory-since" aria-label="Filter memory by time"><option value="">Any time</option><option value="1">Last day</option><option value="7">Last week</option><option value="30">Last 30 days</option></select><select id="memory-hops" aria-label="Graph neighbourhood depth"><option value="1">1 hop</option><option value="2">2 hops</option></select></div><div id="memory-list" class="task-list" aria-label="Memory entries"></div><div id="memory-graph" class="memory-graph"></div><div id="memory-detail"></div></section>
|
|
450
|
+
<section aria-labelledby="memory-title" data-view="memory" hidden><div class="section-head"><h2 id="memory-title">Memory</h2><div class="filters"><div id="memory-tiers" class="chips" role="group" aria-label="Memory tier"></div><span id="memory-count" class="meta"></span></div></div><div class="memory-controls"><input id="memory-q" type="search" maxlength="200" placeholder="Search memory" aria-label="Search memory"><select id="memory-kind" aria-label="Filter memory by kind"><option value="">All kinds</option><option value="decision">Decisions</option><option value="contract">Contracts</option><option value="fail">Fails</option><option value="finding">Findings</option></select><select id="memory-peer" aria-label="Filter memory by peer"><option value="">All peers</option></select><select id="memory-machine" aria-label="Filter memory by machine"><option value="">All machines</option></select><select id="memory-since" aria-label="Filter memory by time"><option value="">Any time</option><option value="1">Last day</option><option value="7">Last week</option><option value="30">Last 30 days</option></select><select id="memory-hops" aria-label="Graph neighbourhood depth"><option value="1">1 hop</option><option value="2">2 hops</option></select><label class="memory-code-toggle"><input id="memory-code-neighbours" type="checkbox" aria-describedby="memory-code-help" disabled>Code neighbours (1 hop)</label><span id="memory-code-help" class="meta">Select a symbol to inspect derived code neighbours.</span></div><div id="memory-list" class="task-list" aria-label="Memory entries"></div><div id="memory-graph" class="memory-graph"></div><div id="memory-detail"></div></section>
|
|
444
451
|
<section aria-labelledby="compose-title" data-view="message" hidden><div class="section-head"><h2 id="compose-title">Console message</h2></div><form id="send" class="content"><div class="field"><label for="recipient">Recipient</label><select id="recipient" name="recipient"><option value="">All peers</option></select></div><div class="field"><label for="message">Message</label><textarea id="message" name="body" required maxlength="8000" placeholder="Send an instruction to the team..."></textarea></div><button class="primary" data-mutation="project" type="submit">Send message</button></form></section>
|
|
445
452
|
<section aria-labelledby="settings-title" data-view="settings" hidden><div class="section-head"><h2 id="settings-title">Settings</h2><span id="settings-session" class="meta" role="status"></span></div><div id="settings"><p class="empty">Waiting for the hub...</p></div></section>
|
|
446
453
|
</div>
|
|
@@ -834,7 +841,7 @@ function settingRow(row, raise) {
|
|
|
834
841
|
const input=el('input');input.type='number';input.id=id;input.step=row.type === 'number' ? 'any' : '1';input.min=String(row.min ?? 0);input.max=String(row.max ?? Number.MAX_SAFE_INTEGER);const current=fileValue;input.value=current===null ? '' : String(current);input.placeholder='Empty inherits';
|
|
835
842
|
read=()=>input.value==='' ? null : Number(input.value);actions.append(input);
|
|
836
843
|
} else if (row.type === 'peers') {
|
|
837
|
-
const input = el('input'); input.type = 'text'; input.id = id; input.value = Array.isArray(fileValue) ? fileValue.join(', ') : ''; input.placeholder = 'peer ids, comma-separated; empty inherits'; input.autocomplete = 'off'; input.spellcheck = false;
|
|
844
|
+
const input = el('input'); input.type = 'text'; input.id = id; input.value = Array.isArray(fileValue) ? fileValue.join(', ') : typeof fileValue === 'string' ? fileValue : ''; input.placeholder = 'peer ids, comma-separated; empty inherits'; input.autocomplete = 'off'; input.spellcheck = false;
|
|
838
845
|
read = () => { const list = input.value.split(',').map((value) => value.trim()).filter(Boolean); return list.length ? list : null; };
|
|
839
846
|
actions.append(input);
|
|
840
847
|
} else {
|
|
@@ -1246,6 +1253,24 @@ function openMemoryAt(node) {
|
|
|
1246
1253
|
memoryQuery.node = node; memoryQuery.q = ''; $('memory-q').value = '';
|
|
1247
1254
|
closeTaskDetail(); switchView('memory'); rerenderMemory();
|
|
1248
1255
|
}
|
|
1256
|
+
// #317: the task's deliverables as the board holds them (ids, hashes, sizes; a path only where the hub's PII screen allowed it). "changed since review" is shown only when the hub measured
|
|
1257
|
+
// exactly these records; the snapshot never hashes a file, so without a measurement the page says nothing about change.
|
|
1258
|
+
function deliverableLabel(d) {
|
|
1259
|
+
if (!d) return '';
|
|
1260
|
+
const n = d.files.length, base = n + (n === 1 ? ' file ' : ' files ') + d.state;
|
|
1261
|
+
if (!d.checked) return base;
|
|
1262
|
+
// Only a measurement in which every file was unchanged says so: a missing or unreadable file is shown as such, never as unchanged.
|
|
1263
|
+
const says = { changed: ', changed since review', missing: ', a file is missing', unknown: ', could not be measured', unchanged: ', unchanged when last checked' };
|
|
1264
|
+
return base + (says[d.checked.result] ?? says.unknown);
|
|
1265
|
+
}
|
|
1266
|
+
function deliverablesSection(task) {
|
|
1267
|
+
const d = task.deliverables; if (!d?.files?.length) return null;
|
|
1268
|
+
const section = el('section', undefined, 'detail-group'); section.append(el('h4', 'Deliverables', 'detail-heading'));
|
|
1269
|
+
section.append(el('p', deliverableLabel(d) + (d.checked ? '' : ' (not measured now: run ahub deliverables --check)'), d.checked && d.checked.result !== 'unchanged' ? 'meta warn' : 'meta'));
|
|
1270
|
+
const list = el('ul', undefined, 'deliverable-list');
|
|
1271
|
+
for (const file of d.files) list.append(el('li', (file.path || 'private file') + ' [' + file.format + (file.version ? ' ' + file.version : '') + '] ' + file.bytes + ' B, sha256 ' + file.sha256.slice(0, 12), 'meta'));
|
|
1272
|
+
section.append(list); return section;
|
|
1273
|
+
}
|
|
1249
1274
|
function taskDetail(task, card, stage, ids, overdue) {
|
|
1250
1275
|
const row = el('article', undefined, 'task-detail'), head = el('div', undefined, 'row-head');
|
|
1251
1276
|
row.setAttribute('aria-label', '#' + task.id + ' ' + task.title + ', ' + task.state.replaceAll('_', ' ') + ', owner ' + (task.owner || 'unassigned') + ', priority P' + card.priority);
|
|
@@ -1277,6 +1302,7 @@ function taskDetail(task, card, stage, ids, overdue) {
|
|
|
1277
1302
|
if(task.pinned)links.append(el('p','Pinned','meta'));
|
|
1278
1303
|
if (links.children.length > 1) row.append(links);
|
|
1279
1304
|
const code = codeGroupSection(task); if (code) row.append(code);
|
|
1305
|
+
const delivered = deliverablesSection(task); if (delivered) row.append(delivered);
|
|
1280
1306
|
const controls = el('div', undefined, 'actions');
|
|
1281
1307
|
if (card.pull && boardViewState?.snapshot.boardCapabilities?.backlog===true) controls.append(boardButton('Pull into To Do', {action:'backlog_pull',id:task.id}));
|
|
1282
1308
|
if (card.pull && boardViewState?.snapshot.planningCapabilities?.wipPull===true) controls.append(boardButton('Pull within WIP', {action:'planner_pull',id:task.id,revision:task.planningRevision ?? 0,reason:'person requested a WIP-constrained pull'}));
|
|
@@ -1308,6 +1334,7 @@ function taskCard(task, card, stage, ids, overdue, compact = true) {
|
|
|
1308
1334
|
if (card.starred) { const star = el('span', undefined, 'star'); star.title = 'starred'; star.append(icon('i-star', 'starred')); head.append(star); }
|
|
1309
1335
|
const meta = el('div', undefined, 'card-meta'); meta.append(cardBadge(task, card, overdue));
|
|
1310
1336
|
if (task.release != null) { const release=boardViewState?.snapshot.releases?.find(release=>release.id===task.release); meta.append(el('span', 'Release #' + task.release + (release?.name ? ' ' + release.name : ''), 'release-chip')); }
|
|
1337
|
+
if (task.deliverables?.files?.length) { const chip = el('span', 'Deliverables ' + task.deliverables.files.length + (task.deliverables.state === 'approved' ? ' approved' : '') + (task.deliverables.checked?.result === 'changed' ? ', changed' : task.deliverables.checked && task.deliverables.checked.result !== 'unchanged' ? ', ' + task.deliverables.checked.result : ''), 'release-chip deliverable-chip'); chip.title = deliverableLabel(task.deliverables); meta.append(chip); row.setAttribute('aria-label', row.getAttribute('aria-label') + ', ' + deliverableLabel(task.deliverables)); }
|
|
1311
1338
|
row.append(head, meta);
|
|
1312
1339
|
row.addEventListener('click', () => openTaskDetail(task.id));
|
|
1313
1340
|
row.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openTaskDetail(task.id); } });
|
|
@@ -1612,6 +1639,8 @@ function renderUsage(usage, tasks, budget) {
|
|
|
1612
1639
|
}
|
|
1613
1640
|
$('usage-task-count').textContent = taskCount + ' tasks';
|
|
1614
1641
|
}
|
|
1642
|
+
// Mirrors windowKind in src/hub/window-kind.ts (the page imports nothing): a window from an older hub has no kind. test/ui.test.ts keeps the two equal.
|
|
1643
|
+
function windowKindOf(w) { return w.kind || (w.id === 'tokens' ? 'count' : w.source === 'ahub budget set' ? 'manual' : 'native'); }
|
|
1615
1644
|
function render(snapshot) {
|
|
1616
1645
|
renderNeedsAttention(snapshot);
|
|
1617
1646
|
const peers = Object.fromEntries(Object.entries(snapshot.status.peers || {}).map(([id, peer]) => [id, {...peer, ...(snapshot.status.roleInfo || {})[id]}])), ids = Object.keys(peers), starts = Array.isArray(snapshot.starts) ? snapshot.starts : [];
|
|
@@ -1677,13 +1706,14 @@ function render(snapshot) {
|
|
|
1677
1706
|
if (pct !== null) { const bar = el('progress'); bar.max = 1; bar.value = context.used; bar.setAttribute('aria-label', peer + ' context usage'); row.append(bar); }
|
|
1678
1707
|
}
|
|
1679
1708
|
if (budget.paused) { const line = el('p', 'Paused: ' + budget.paused.reason + ' / resets ', 'meta'), stamp = el('span'); stamp.dataset.ts = budget.paused.resetsAt; stamp.title = time(budget.paused.resetsAt); stamp.textContent = rel(budget.paused.resetsAt); line.append(stamp); row.append(line); }
|
|
1680
|
-
if (!budget.windows.length) row.append(el('p', 'No usage windows reported.', 'meta'));
|
|
1709
|
+
if (!budget.windows.length) row.append(el('p', budget.note || 'No usage windows reported.', 'meta'));
|
|
1710
|
+
else if (budget.note) row.append(el('p', budget.note, 'meta'));
|
|
1681
1711
|
for (const window of budget.windows) {
|
|
1682
1712
|
const item = el('div', undefined, 'budget-window'), percent = Math.round(window.used * 100);
|
|
1683
|
-
item.append(el('p', window.id + ': ' + percent + '% used' + (window.stale ? ' (stale)' : ''), 'meta'));
|
|
1713
|
+
item.append(el('p', window.id + ': ' + percent + '% used' + (window.stale ? ' (stale)' : window.superseded ? ' (information only)' : ''), 'meta'));
|
|
1684
1714
|
const bar = el('progress'); bar.max = 1; bar.value = window.used; bar.className = percent >= 90 ? 'critical' : percent >= 70 ? 'warn' : '';
|
|
1685
1715
|
bar.setAttribute('aria-label', peer + ' ' + window.id + ' usage'); item.append(bar);
|
|
1686
|
-
const meta = el('p', window.source + (window.resetsAt ? ' / resets ' : ''), 'meta');
|
|
1716
|
+
const meta = el('p', windowKindOf(window) + ' / ' + window.source + (window.resetsAt ? ' / resets ' : ''), 'meta');
|
|
1687
1717
|
if (window.resetsAt) { const stamp = el('span'); stamp.dataset.ts = window.resetsAt; stamp.title = time(window.resetsAt); stamp.textContent = rel(window.resetsAt); meta.append(stamp); }
|
|
1688
1718
|
item.append(meta); row.append(item);
|
|
1689
1719
|
}
|
|
@@ -2219,21 +2249,22 @@ function renderBench(bench) {
|
|
|
2219
2249
|
}
|
|
2220
2250
|
// #258: the hub's memory, read-only. A tier at a time, filtered; selecting an entry or a node shows its neighbourhood
|
|
2221
2251
|
// (inline SVG, a small force layout computed here) with the same edges as a list, and its detail.
|
|
2222
|
-
const memoryQuery = {tier:'long', q:'', kind:'', peer:'', machine:'', since:'', node:'', hops:1};
|
|
2252
|
+
const memoryQuery = {tier:'long', q:'', kind:'', peer:'', machine:'', since:'', node:'', hops:1, codeNeighbours:false};
|
|
2223
2253
|
let memoryGeneration = -1;
|
|
2224
2254
|
function memoryInput() {
|
|
2225
2255
|
if (memoryGeneration !== viewGeneration) { // another project: its own nodes, peers and machines
|
|
2226
|
-
memoryGeneration = viewGeneration; memoryQuery.node = ''; memoryQuery.peer = ''; memoryQuery.machine = '';
|
|
2256
|
+
memoryGeneration = viewGeneration; memoryQuery.node = ''; memoryQuery.peer = ''; memoryQuery.machine = ''; memoryQuery.codeNeighbours = false;
|
|
2227
2257
|
for (const id of ['memory-peer', 'memory-machine']) $(id).value = '';
|
|
2228
2258
|
}
|
|
2229
2259
|
const out = {tier:memoryQuery.tier, hops:memoryQuery.hops};
|
|
2230
2260
|
for (const key of ['q', 'kind', 'peer', 'machine', 'node']) if (memoryQuery[key]) out[key] = memoryQuery[key];
|
|
2261
|
+
if (memoryQuery.codeNeighbours === true && memoryQuery.node.startsWith('symbol:') && currentView === 'memory' && !document.hidden) out.codeNeighbours = true;
|
|
2231
2262
|
// A whole timestamp, floored to the hour so the query stays the same between polls.
|
|
2232
2263
|
if (memoryQuery.since) out.since = new Date(Math.floor((Date.now() - Number(memoryQuery.since) * 86400000) / 3600000) * 3600000).toISOString();
|
|
2233
2264
|
return out;
|
|
2234
2265
|
}
|
|
2235
2266
|
// The next poll (at most a second away) carries the changed query; polling is never started twice.
|
|
2236
|
-
function rerenderMemory() { for (const chip of $('memory-tiers').children) chip.setAttribute('aria-pressed', String(chip.dataset.tier === memoryQuery.tier)); }
|
|
2267
|
+
function rerenderMemory() { memoryCodeToggle(); for (const chip of $('memory-tiers').children) chip.setAttribute('aria-pressed', String(chip.dataset.tier === memoryQuery.tier)); }
|
|
2237
2268
|
function memoryOptions(id, values, all, key) {
|
|
2238
2269
|
const select = $(id), keep = select.value;
|
|
2239
2270
|
const want = [''].concat(values);
|
|
@@ -2242,6 +2273,51 @@ function memoryOptions(id, values, all, key) {
|
|
|
2242
2273
|
select.value = want.includes(keep) ? keep : '';
|
|
2243
2274
|
memoryQuery[key] = select.value; // a filter whose value is gone reads as none, on the page and in the next query
|
|
2244
2275
|
}
|
|
2276
|
+
// Render-only union: stored edges remain stored; code facts are separately SHA-stamped and never persisted here.
|
|
2277
|
+
function memoryPresentation(memory) {
|
|
2278
|
+
const stored = memory.graph || {nodes:[], edges:[]}, entries = memory.entries || [];
|
|
2279
|
+
const enabled = memoryQuery.codeNeighbours === true && memoryQuery.node.startsWith('symbol:') && memory.node === memoryQuery.node;
|
|
2280
|
+
if (!enabled) return {graph:stored, entries, noteIds:[], coverage:''};
|
|
2281
|
+
const code = memory.code, head = code?.head;
|
|
2282
|
+
if (!code || !/^[a-f0-9]{40}$/.test(head || '')) return {graph:stored, entries, noteIds:[], coverage:'Code neighbours: unknown (no current code facts).'};
|
|
2283
|
+
let coverage = 'Code neighbours: ' + (code.state === 'known' ? 'known' : 'unknown') + (code.truncated ? '; truncated' : '')
|
|
2284
|
+
+ (code.countsFloor ? '; lower-bound counts' : '') + ' at ' + head + '; dashed edges are derived, never stored.';
|
|
2285
|
+
const nodes = new Map(stored.nodes.map(node => [node.id, node]));
|
|
2286
|
+
const candidateNodes = new Map((code.nodes || []).filter(node => node.derived === true && node.head === head && node.type === 'symbol' && typeof node.id === 'string' && node.id.startsWith('symbol:')).map(node => [node.id, node]));
|
|
2287
|
+
const edges = new Map(stored.edges.map(edge => [JSON.stringify([edge.from, edge.to, edge.rel]), edge]));
|
|
2288
|
+
const connected = new Set();
|
|
2289
|
+
for (const edge of code.edges || []) {
|
|
2290
|
+
if (edge.derived !== true || edge.head !== head || edge.rel !== 'calls' || !candidateNodes.has(edge.from) || !candidateNodes.has(edge.to)) continue;
|
|
2291
|
+
const key = JSON.stringify([edge.from, edge.to, edge.rel]);
|
|
2292
|
+
if (!edges.has(key)) edges.set(key, edge);
|
|
2293
|
+
connected.add(edge.from); connected.add(edge.to);
|
|
2294
|
+
}
|
|
2295
|
+
for (const [id, node] of candidateNodes) if ((connected.has(id) || id === memory.node) && !nodes.has(id)) nodes.set(id, node);
|
|
2296
|
+
const notes = (code.notes || []).filter(note => (!memoryQuery.kind || note.kind === memoryQuery.kind) && (!memoryQuery.tier || note.tier === memoryQuery.tier) && (!memoryQuery.peer || note.peer === memoryQuery.peer) && (!memoryQuery.machine || note.machine === memoryQuery.machine) && /^mem:[a-f0-9]{32}$/.test(note.id) && typeof note.line === 'string' && Array.isArray(note.about) && note.about.some(id => connected.has(id)));
|
|
2297
|
+
const noteIds = [...new Set(notes.map(note => note.id))], highlightIds = new Set(noteIds), byId = new Map(entries.map(entry => [entry.id, entry]));
|
|
2298
|
+
for (const note of notes) {
|
|
2299
|
+
const id = note.id.slice(4), existing = byId.get(id);
|
|
2300
|
+
byId.set(id, existing ? {...existing, codeNeighbour:true} : {id, title:null, text:note.line, kind:note.kind || 'finding', codeNeighbour:true, codeFallback:true, head});
|
|
2301
|
+
if (!nodes.has(note.id)) nodes.set(note.id, {id:note.id, type:'memory', label:note.line, kind:note.kind});
|
|
2302
|
+
for (const about of note.about) if (candidateNodes.has(about) && connected.has(about)) {
|
|
2303
|
+
highlightIds.add(about);
|
|
2304
|
+
const rel = note.kind === 'fail' ? 'rules_out' : 'about', key = JSON.stringify([note.id, about, rel]);
|
|
2305
|
+
if (!edges.has(key)) edges.set(key, {from:note.id, to:about, rel});
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
const ordered = [...byId.values()].sort((a, b) => Number(b.kind === 'fail') - Number(a.kind === 'fail') || Number(b.codeNeighbour === true) - Number(a.codeNeighbour === true));
|
|
2309
|
+
const rank = node => node.kind === 'fail' ? 0 : node.id === memory.node ? 1 : highlightIds.has(node.id) ? 2 : node.derived ? 3 : 4;
|
|
2310
|
+
const orderedNodes = [...nodes.values()].sort((a, b) => rank(a) - rank(b)), keptNodes = orderedNodes.slice(0, 200), keptIds = new Set(keptNodes.map(node => node.id));
|
|
2311
|
+
const nodeOmitted = orderedNodes.length - keptNodes.length, noteOmitted = ordered.length - Math.min(ordered.length, 50);
|
|
2312
|
+
if (nodeOmitted || noteOmitted) coverage += '; display truncated: ' + nodeOmitted + ' nodes and ' + noteOmitted + ' entries omitted';
|
|
2313
|
+
return {graph:{nodes:keptNodes, edges:[...edges.values()].filter(edge => keptIds.has(edge.from) && keptIds.has(edge.to))}, entries:ordered.slice(0, 50), noteIds:[...highlightIds], coverage};
|
|
2314
|
+
}
|
|
2315
|
+
function memoryCodeToggle() {
|
|
2316
|
+
const toggle = $('memory-code-neighbours'), eligible = memoryQuery.node.startsWith('symbol:');
|
|
2317
|
+
if (!eligible) memoryQuery.codeNeighbours = false;
|
|
2318
|
+
toggle.disabled = !eligible; toggle.checked = memoryQuery.codeNeighbours === true;
|
|
2319
|
+
$('memory-code-help').textContent = eligible ? 'Optional current-head caller/callee context; dashed edges are derived.' : 'Select a symbol to inspect derived code neighbours.';
|
|
2320
|
+
}
|
|
2245
2321
|
function layout(graph) {
|
|
2246
2322
|
// Deterministic: start on a circle in node order, then a fixed number of spring steps; no animation loop.
|
|
2247
2323
|
const W = 640, H = 320, n = graph.nodes.length, pos = new Map();
|
|
@@ -2264,6 +2340,7 @@ function layout(graph) {
|
|
|
2264
2340
|
}
|
|
2265
2341
|
function selectMemoryNode(node) { memoryQuery.node = memoryQuery.node === node ? '' : node; rerenderMemory(); }
|
|
2266
2342
|
function renderMemory(memory) {
|
|
2343
|
+
memoryCodeToggle();
|
|
2267
2344
|
const tiers = $('memory-tiers');
|
|
2268
2345
|
if (!tiers.children.length) for (const [tier, label] of [['short', 'Short-term'], ['long', 'Long-term'], ['goals', 'Long-term tasks']]) {
|
|
2269
2346
|
const chip = el('button', label, 'chip'); chip.type = 'button'; chip.dataset.tier = tier;
|
|
@@ -2272,6 +2349,7 @@ function renderMemory(memory) {
|
|
|
2272
2349
|
}
|
|
2273
2350
|
for (const chip of tiers.children) chip.setAttribute('aria-pressed', String(chip.dataset.tier === memoryQuery.tier));
|
|
2274
2351
|
if (!memory) { $('memory-count').textContent = ''; update('memory-list', null, (target) => empty(target, 'Hub memory is off for this project, or nothing is stored yet. Save a note with ahub remember <text>.')); update('memory-graph', null, () => {}); update('memory-detail', null, () => {}); return; }
|
|
2352
|
+
const presentation = memoryPresentation(memory);
|
|
2275
2353
|
$('memory-count').textContent = memory.counts.long + ' long-term / ' + memory.counts.short + ' short-term / ' + (memory.counts.goals || 0) + ' open tasks';
|
|
2276
2354
|
if (memory.goals) {
|
|
2277
2355
|
// Long-term tasks: state, who holds the lease and until when (a conflict when two machines claimed it), linked board tasks.
|
|
@@ -2297,15 +2375,16 @@ function renderMemory(memory) {
|
|
|
2297
2375
|
}
|
|
2298
2376
|
memoryOptions('memory-peer', memory.peers, 'All peers', 'peer');
|
|
2299
2377
|
memoryOptions('memory-machine', memory.machines, 'All machines', 'machine');
|
|
2300
|
-
if (!memory.goals) update('memory-list', {entries:
|
|
2301
|
-
if (!
|
|
2378
|
+
if (!memory.goals) update('memory-list', {entries: presentation.entries, node: memoryQuery.node}, (target) => {
|
|
2379
|
+
if (!presentation.entries.length) return empty(target, 'No ' + memory.tier + '-term entries match. Save one with ahub remember <text>.');
|
|
2302
2380
|
const keyed = keyedStart('memory');
|
|
2303
|
-
for (const entry of
|
|
2381
|
+
for (const entry of presentation.entries) {
|
|
2304
2382
|
const row = keyed('mem:' + entry.id, JSON.stringify([entry, memoryQuery.node === 'mem:' + entry.id]), () => {
|
|
2305
|
-
const row = el('button', undefined, 'memory-row'); row.type = 'button'; row.setAttribute('aria-pressed', String(memoryQuery.node === 'mem:' + entry.id));
|
|
2383
|
+
const row = el('button', undefined, 'memory-row' + (entry.codeNeighbour ? ' code-neighbour-note' : '')); row.type = 'button'; row.setAttribute('aria-pressed', String(memoryQuery.node === 'mem:' + entry.id));
|
|
2306
2384
|
const head = el('div', undefined, 'row-head');
|
|
2307
2385
|
head.append(el('span', (entry.title ? entry.title + ': ' : '') + entry.text.split('\n')[0].slice(0, 140), 'label'), badge(entry.kind, entry.kind === 'fail' ? 'err' : entry.kind === 'decision' ? 'ok' : entry.kind === 'contract' ? 'warn' : ''));
|
|
2308
|
-
|
|
2386
|
+
if (entry.codeNeighbour) head.append(badge('Code neighbour', 'info'));
|
|
2387
|
+
row.append(head, el('div', entry.codeFallback ? entry.id.slice(0, 8) + ' / via code neighbour at ' + entry.head : entry.id.slice(0, 8) + ' / ' + entry.peer + ' / ' + time(entry.at) + (entry.scope === 'shared' ? ' / shared' : '') + (entry.supersededBy ? ' / superseded' : '') + (entry.machine !== memory.machine ? ' / from machine ' + entry.machine.slice(0, 6) : ''), 'meta'));
|
|
2309
2388
|
row.addEventListener('click', () => selectMemoryNode('mem:' + entry.id));
|
|
2310
2389
|
return row;
|
|
2311
2390
|
});
|
|
@@ -2313,19 +2392,21 @@ function renderMemory(memory) {
|
|
|
2313
2392
|
}
|
|
2314
2393
|
keyed.end();
|
|
2315
2394
|
});
|
|
2316
|
-
update('memory-graph',
|
|
2317
|
-
const graph =
|
|
2395
|
+
update('memory-graph', {graph:presentation.graph, coverage:presentation.coverage, noteIds:presentation.noteIds}, (target) => {
|
|
2396
|
+
const graph = presentation.graph;
|
|
2397
|
+
if (presentation.coverage) target.append(el('p', presentation.coverage, 'memory-code-coverage meta'));
|
|
2318
2398
|
if (!graph || !graph.nodes.length) return;
|
|
2319
2399
|
const NS = 'http://www.w3.org/2000/svg', svg = document.createElementNS(NS, 'svg'), pos = layout(graph);
|
|
2320
2400
|
svg.setAttribute('viewBox', '0 0 640 320'); svg.setAttribute('role', 'img'); svg.setAttribute('aria-label', 'Neighbourhood of ' + memory.node + ': ' + graph.nodes.length + ' nodes, ' + graph.edges.length + ' edges; the list below has the same edges');
|
|
2321
2401
|
for (const e of graph.edges) {
|
|
2322
2402
|
const p = pos.get(e.from), q = pos.get(e.to); if (!p || !q) continue;
|
|
2323
|
-
const line = document.createElementNS(NS, 'line'); line.setAttribute('x1', p.x); line.setAttribute('y1', p.y); line.setAttribute('x2', q.x); line.setAttribute('y2', q.y); line.setAttribute('class', 'medge ' + e.rel);
|
|
2403
|
+
const line = document.createElementNS(NS, 'line'); line.setAttribute('x1', p.x); line.setAttribute('y1', p.y); line.setAttribute('x2', q.x); line.setAttribute('y2', q.y); line.setAttribute('class', 'medge ' + e.rel + (e.derived ? ' derived' : ''));
|
|
2404
|
+
if (e.derived) { const title = document.createElementNS(NS, 'title'); title.textContent = 'Derived ' + e.rel + ' at ' + e.head; line.append(title); }
|
|
2324
2405
|
svg.append(line);
|
|
2325
2406
|
}
|
|
2326
2407
|
for (const node of graph.nodes) {
|
|
2327
2408
|
const p = pos.get(node.id), g = document.createElementNS(NS, 'g'), c = document.createElementNS(NS, 'circle'), t = document.createElementNS(NS, 'text');
|
|
2328
|
-
g.setAttribute('class', 'mnode ' + node.type + (node.kind ? ' ' + node.kind : '') + (node.id === memory.node ? ' selected' : '')); g.setAttribute('tabindex', '0'); g.setAttribute('role', 'button'); g.setAttribute('aria-label', node.type + ' ' + node.label);
|
|
2409
|
+
g.setAttribute('class', 'mnode ' + node.type + (node.kind ? ' ' + node.kind : '') + (node.id === memory.node ? ' selected' : '') + (presentation.noteIds.includes(node.id) ? ' code-neighbour-note' : '')); g.setAttribute('tabindex', '0'); g.setAttribute('role', 'button'); g.setAttribute('aria-label', node.type + ' ' + node.label);
|
|
2329
2410
|
c.setAttribute('cx', p.x); c.setAttribute('cy', p.y); c.setAttribute('r', node.type === 'memory' ? 9 : 6);
|
|
2330
2411
|
t.setAttribute('x', p.x + 11); t.setAttribute('y', p.y + 4); t.textContent = node.label.slice(0, 28);
|
|
2331
2412
|
g.append(c, t);
|
|
@@ -2334,7 +2415,7 @@ function renderMemory(memory) {
|
|
|
2334
2415
|
svg.append(g);
|
|
2335
2416
|
}
|
|
2336
2417
|
const list = el('ul', undefined, 'memory-edges');
|
|
2337
|
-
for (const e of graph.edges) list.append(el('li', e.from + ' ' + e.rel + ' ' + e.to));
|
|
2418
|
+
for (const e of graph.edges) list.append(el('li', e.from + ' ' + e.rel + ' ' + e.to + (e.derived ? ' (derived at ' + e.head + ')' : '')));
|
|
2338
2419
|
target.append(svg, list);
|
|
2339
2420
|
});
|
|
2340
2421
|
update('memory-detail', memory.detail || null, (target) => {
|
|
@@ -2350,6 +2431,7 @@ function renderMemory(memory) {
|
|
|
2350
2431
|
});
|
|
2351
2432
|
}
|
|
2352
2433
|
for (const [id, key] of [['memory-kind', 'kind'], ['memory-peer', 'peer'], ['memory-machine', 'machine'], ['memory-since', 'since']]) $(id).addEventListener('change', () => { memoryQuery[key] = $(id).value; rerenderMemory(); });
|
|
2434
|
+
$('memory-code-neighbours').addEventListener('change', () => { memoryQuery.codeNeighbours = $('memory-code-neighbours').checked === true; rerenderMemory(); });
|
|
2353
2435
|
$('memory-hops').addEventListener('change', () => { memoryQuery.hops = Number($('memory-hops').value); rerenderMemory(); });
|
|
2354
2436
|
let memorySearch;
|
|
2355
2437
|
$('memory-q').addEventListener('input', () => { clearTimeout(memorySearch); memorySearch = setTimeout(() => { memoryQuery.q = $('memory-q').value.trim(); rerenderMemory(); }, 300); });
|