@staix/agent-hub 0.14.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 +42 -0
- package/README.md +23 -9
- package/docs/agent-notes/README.md +8 -0
- package/docs/agent-notes/adapters.md +21 -4
- package/docs/agent-notes/budget.md +6 -4
- package/docs/agent-notes/bus.md +1 -0
- package/docs/agent-notes/daemon.md +20 -9
- package/docs/agent-notes/local-worker.md +2 -2
- package/docs/agent-notes/tasks.md +21 -4
- package/docs/agent-notes/tests.md +6 -3
- package/docs/dogfood.md +19 -5
- package/docs/events.md +21 -7
- package/docs/memory.md +78 -10
- package/docs/operations.md +197 -39
- package/docs/profiles.md +1 -0
- package/docs/quickstart.md +16 -6
- package/docs/sdlc.md +17 -0
- package/docs/security.md +110 -21
- package/docs/smoke.md +7 -3
- package/docs/specs/2026-09-19-agent-hub-design.md +156 -16
- package/docs/verification/2026-10-23-0.15.0.md +80 -0
- package/docs/verification/2026-10-23-daemon-responsiveness.md +44 -0
- package/docs/verification/2026-10-23-headless-claude-permissions.md +47 -0
- package/docs/verification/2026-10-23-terminal-claude-execution-root.md +41 -0
- 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 +40 -19
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +3109 -2046
- package/src/adapters/acp.ts +46 -7
- package/src/adapters/claude-channel.ts +26 -4
- package/src/adapters/claude-implementer.ts +243 -0
- package/src/adapters/claude-print-child.ts +90 -0
- package/src/adapters/claude-task-sessions.ts +80 -0
- package/src/adapters/codex-appserver.ts +69 -1
- package/src/adapters/compat.ts +6 -2
- package/src/adapters/conformance/enrollment.ts +2 -1
- package/src/adapters/conformance/fakes/acp-server.ts +3 -1
- package/src/adapters/definitions/builtin-metadata.ts +17 -5
- package/src/adapters/definitions/native-channel-state.ts +1 -1
- package/src/adapters/definitions/native-cli-entry-claude.ts +13 -8
- package/src/adapters/definitions/native-cli-entry-types.ts +2 -0
- package/src/adapters/definitions/native-factories.ts +8 -0
- package/src/adapters/kimi-wire-usage.ts +303 -0
- package/src/adapters/local-worker.ts +5 -4
- package/src/adapters/registry-types.ts +8 -4
- package/src/adapters/registry.ts +14 -2
- package/src/adapters/reviewer-print.ts +30 -53
- package/src/cli/console-config.ts +123 -0
- package/src/cli/console-state.ts +48 -3
- package/src/cli/console-symbols.ts +3 -2
- package/src/cli/console-usage.ts +5 -2
- package/src/cli/console.ts +30 -2
- package/src/cli/facts-hook.ts +9 -5
- package/src/cli/help.ts +10 -7
- package/src/cli/identity.ts +3 -1
- package/src/cli/launch.ts +10 -4
- package/src/cli/main.ts +91 -19
- package/src/cli/memory.ts +119 -7
- package/src/cli/output.ts +40 -4
- package/src/cli/reset.ts +1 -1
- package/src/cli/settings-edit.ts +89 -0
- package/src/cli/terminal-recovery.ts +1 -6
- package/src/hub/action-class.ts +211 -17
- package/src/hub/action-enforce.ts +90 -0
- package/src/hub/action-policy.ts +40 -17
- package/src/hub/ask.ts +16 -4
- package/src/hub/board.ts +110 -31
- package/src/hub/budget-notes.ts +65 -0
- package/src/hub/budget.ts +44 -7
- package/src/hub/bus.ts +6 -6
- package/src/hub/claude-launch-proof.ts +33 -0
- package/src/hub/code-facts.ts +56 -16
- package/src/hub/conductor.ts +5 -2
- package/src/hub/config-apply.ts +12 -0
- package/src/hub/config-edit-host.ts +173 -0
- package/src/hub/config-edit.ts +282 -0
- package/src/hub/config-tier.ts +89 -0
- package/src/hub/control-client.ts +24 -3
- package/src/hub/daemon.ts +614 -104
- package/src/hub/deliverable-files.ts +131 -0
- package/src/hub/deliverables.ts +125 -0
- package/src/hub/delivery-journal.ts +18 -2
- package/src/hub/envelope.ts +1 -1
- package/src/hub/events.ts +5 -3
- package/src/hub/hub-tools.ts +22 -6
- package/src/hub/instances.ts +7 -4
- package/src/hub/kimi-usage.ts +156 -0
- package/src/hub/lifecycle.ts +10 -5
- package/src/hub/manager.ts +1 -1
- 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/peers.ts +7 -0
- package/src/hub/permission-payload.ts +15 -0
- package/src/hub/planning.ts +10 -4
- package/src/hub/read-tools.ts +21 -3
- package/src/hub/routing.ts +118 -3
- package/src/hub/sdlc.ts +34 -0
- package/src/hub/settings.ts +18 -3
- package/src/hub/settled-roles.ts +15 -3
- package/src/hub/task-sweep.ts +25 -3
- package/src/hub/tasks.ts +551 -75
- package/src/hub/token-window.ts +46 -0
- package/src/hub/usage-rollup.ts +16 -6
- package/src/hub/window-kind.ts +13 -0
- package/src/hub/workflow.ts +16 -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 +215 -19
package/src/memory/recall.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { ADAPTERS, definitionOf } from "../adapters/registry.ts";
|
|
2
1
|
import type { PeerId } from "../hub/envelope.ts";
|
|
3
2
|
import type { MemoryClient } from "./client.ts";
|
|
4
3
|
import type { View as MemoryView } from "@staix/agent-mem";
|
|
5
4
|
|
|
6
5
|
|
|
7
6
|
const HEADER =
|
|
8
|
-
"Shared project memory (
|
|
7
|
+
"Shared project memory (amem): curated recent notes by the agents in this project. Reference only, not a request. " +
|
|
9
8
|
"Fetch details by id with your memory tools if you need them.";
|
|
10
9
|
|
|
11
10
|
/** Cut at a line boundary. */
|
|
@@ -17,29 +16,21 @@ export function trimToTokens(text: string, tokens: number): string {
|
|
|
17
16
|
return `${cut >= 0 ? text.slice(0, cut) : ""}\n(trimmed)`;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
/** The
|
|
19
|
+
/** The project client supplies a screened context block; unavailable/empty views have no preface. */
|
|
21
20
|
const hasContext = (text: string | undefined): text is string => !!text && text.startsWith("# [");
|
|
22
21
|
|
|
23
|
-
/**
|
|
24
|
-
const entries = (text: string) =>
|
|
22
|
+
/** Drop only the redundant source heading. Keep coverage/truncation notices before section headings. */
|
|
23
|
+
const entries = (text: string) => {
|
|
24
|
+
const start = text.indexOf("\n");
|
|
25
|
+
return start < 0 ? "" : text.slice(start + 1).trim();
|
|
26
|
+
};
|
|
25
27
|
|
|
26
|
-
export async function recallFor(
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
[...ADAPTERS.flatMap(adapter => adapter.hooks?.recallSource ? [adapter.hooks.recallSource] : []), "agent-hub"].filter((p) => p !== own).map(async (p) => {
|
|
33
|
-
const text = await client.contextInject(chain, p);
|
|
34
|
-
return hasContext(text) ? `## from ${p} sessions\n${entries(text)}` : undefined;
|
|
35
|
-
}),
|
|
36
|
-
)
|
|
37
|
-
: [await client.contextInject(chain).then((text) => (hasContext(text) ? entries(text) : undefined))];
|
|
38
|
-
const found = blocks.filter((b): b is string => !!b);
|
|
39
|
-
if (!found.length) return undefined;
|
|
40
|
-
// The budget is split per block, or the first platform could crowd the others out of a cross-platform recall.
|
|
41
|
-
const share = Math.floor(tokens / found.length);
|
|
42
|
-
return `${HEADER}\n\n${found.map((b) => trimToTokens(b, share)).join("\n\n")}`;
|
|
28
|
+
export async function recallFor(_peer: PeerId, client: MemoryClient, chain: string[], tokens: number): Promise<string | undefined> {
|
|
29
|
+
// Curated amem notes are one project source, regardless of which native peer originally authored them.
|
|
30
|
+
const text = await client.contextInject(chain);
|
|
31
|
+
if (!hasContext(text)) return undefined;
|
|
32
|
+
const body = entries(text);
|
|
33
|
+
return body ? `${HEADER}\n\n${trimToTokens(body, tokens)}` : undefined;
|
|
43
34
|
}
|
|
44
35
|
|
|
45
36
|
/**
|
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)}
|
|
@@ -201,6 +208,15 @@ details.settings-group{border-bottom:1px solid var(--line)}
|
|
|
201
208
|
details.settings-group>summary{background:var(--panel-soft)}
|
|
202
209
|
details.settings-group .row{border-bottom:1px dashed var(--line)}
|
|
203
210
|
details.settings-group .row:last-child{border-bottom:0}
|
|
211
|
+
/* Structure (#689): instances, lifecycle and the executable keys. Tier 3 is text only: no control, a command to run in the console. */
|
|
212
|
+
.structure-group .row{padding:var(--space-3) var(--space-4)}
|
|
213
|
+
.structure-instance,.structure-sdlc,.structure-add,.structure-backups{border-bottom:1px dashed var(--line)}
|
|
214
|
+
.structure-tier3{background:var(--panel-soft);color:var(--ink);border:1px solid var(--line);border-radius:var(--radius-surface);padding:var(--space-3) var(--space-4);margin:var(--space-3) var(--space-4)}
|
|
215
|
+
.structure-tier3 code{display:block;overflow-wrap:anywhere;color:var(--muted)}
|
|
216
|
+
.structure-group button,.structure-group input,.structure-group select{min-height:44px}
|
|
217
|
+
.structure-field{display:grid;grid-template-columns:minmax(0,12rem) minmax(0,1fr);gap:var(--space-2);align-items:center;margin:var(--space-2) 0}
|
|
218
|
+
.structure-diff{white-space:pre-wrap;overflow-wrap:anywhere;background:var(--panel-soft);color:var(--ink);border:1px solid var(--line);border-radius:var(--radius-control);padding:var(--space-2) var(--space-3);margin:var(--space-2) 0}
|
|
219
|
+
.structure-error{color:var(--red);background:var(--red-soft);border:1px solid var(--red);border-radius:var(--radius-control);padding:var(--space-2) var(--space-3);margin:var(--space-2) 0}
|
|
204
220
|
/* Peer colours: the registry's hue, one variant per theme, generated by peer-palette.ts */
|
|
205
221
|
/* PEER_PALETTE_CSS */
|
|
206
222
|
|
|
@@ -233,7 +249,7 @@ details.settings-group .row:last-child{border-bottom:0}
|
|
|
233
249
|
.board-unknown{padding:var(--space-3) var(--space-4);color:var(--amber)}
|
|
234
250
|
@media(max-width:1100px){.board-columns{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
235
251
|
@media(max-width:900px){.stack{gap:var(--space-4)}.stream{height:300px}footer{padding:var(--space-4)}}
|
|
236
|
-
@media(max-width:600px){.split{grid-template-columns:minmax(0,1fr)}.board-columns{grid-template-columns:1fr;padding:var(--space-3)}.board-toolbar{align-items:stretch}.board-toolbar select{max-width:none;width:100%}.board-toolbar label{width:100%}}
|
|
252
|
+
@media(max-width:600px){.structure-field{grid-template-columns:minmax(0,1fr)}.structure-tier3{margin:var(--space-3)}.split{grid-template-columns:minmax(0,1fr)}.board-columns{grid-template-columns:1fr;padding:var(--space-3)}.board-toolbar{align-items:stretch}.board-toolbar select{max-width:none;width:100%}.board-toolbar label{width:100%}}
|
|
237
253
|
/* Reduced motion (#469 R6): one rule for the whole page. Every transition and animation is off, whatever rule declares it. */
|
|
238
254
|
@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important;scroll-behavior:auto!important}}
|
|
239
255
|
|
|
@@ -431,7 +447,7 @@ main[data-stale]>:not(#notice):not(#disconnected){opacity:.6;transition:opacity
|
|
|
431
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>
|
|
432
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>
|
|
433
449
|
|
|
434
|
-
<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>
|
|
435
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>
|
|
436
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>
|
|
437
453
|
</div>
|
|
@@ -825,7 +841,7 @@ function settingRow(row, raise) {
|
|
|
825
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';
|
|
826
842
|
read=()=>input.value==='' ? null : Number(input.value);actions.append(input);
|
|
827
843
|
} else if (row.type === 'peers') {
|
|
828
|
-
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;
|
|
829
845
|
read = () => { const list = input.value.split(',').map((value) => value.trim()).filter(Boolean); return list.length ? list : null; };
|
|
830
846
|
actions.append(input);
|
|
831
847
|
} else {
|
|
@@ -863,6 +879,105 @@ function settingRow(row, raise) {
|
|
|
863
879
|
actions.append(save); if (row.type === 'roles') { const inherit = el('button', 'Use profile/shared roles'); inherit.type = 'button'; inherit.dataset.mutation = 'project'; inherit.disabled = managerMode && !snapshotValid; inherit.setAttribute('aria-label', 'Inherit ' + row.label); inherit.addEventListener('click', async () => { out.textContent = 'Restoring inherited role guidance...'; const accepted = await action({action:'setting',key:row.key,value:null}, inherit); out.textContent = accepted ? 'Inherited selection saved. Applies at the next peer start.' : $('notice').textContent; }); actions.append(inherit); } node.append(actions, out);
|
|
864
880
|
return node;
|
|
865
881
|
}
|
|
882
|
+
/* Structure (#689): tier 2 edits inside a settings session; tier 3 keys are text only, with the console command that changes them. */
|
|
883
|
+
const STRUCTURE_FIELDS = [['kind', 'text'], ['roles', 'text'], ['start_mode', 'text'], ['review_mode', 'text'], ['worktree', 'text'], ['permission_mode', 'text'], ['readOnly', 'text']];
|
|
884
|
+
const structureState = {revision: {}, lines: [], error: '', field: '', tier: 0, preview: null};
|
|
885
|
+
const structureList = (text) => String(text).split(',').map((item) => item.trim()).filter(Boolean);
|
|
886
|
+
function structureField(form, name, label, placeholder) {
|
|
887
|
+
const row = el('div', undefined, 'structure-field'), input = el('input'), id = 'structure-' + name;
|
|
888
|
+
const title = el('label', label, 'meta'); title.htmlFor = id; input.id = id; input.type = 'text'; input.placeholder = placeholder || ''; input.setAttribute('aria-label', label);
|
|
889
|
+
row.append(title, input); form.append(row); return input;
|
|
890
|
+
}
|
|
891
|
+
function structureResult(box, out) {
|
|
892
|
+
box.replaceChildren();
|
|
893
|
+
if (structureState.error) box.append(el('p', structureState.error + (structureState.field ? ' (' + structureState.field + ')' : ''), 'structure-error'));
|
|
894
|
+
if (structureState.lines.length) box.append(el('pre', structureState.lines.join('\n'), 'structure-diff'));
|
|
895
|
+
if (out) out(box);
|
|
896
|
+
}
|
|
897
|
+
function structureForm(cls, title, build, file, edit, raise) {
|
|
898
|
+
const form = el('div', undefined, cls), head = el('p', title, 'label'), fields = {}, result = el('div');
|
|
899
|
+
form.append(head);
|
|
900
|
+
build(form, fields);
|
|
901
|
+
const preview = el('button', 'Preview change', 'primary'); preview.type = 'button'; preview.dataset.mutation = 'project';
|
|
902
|
+
const reason = structureField(form, 'reason', 'Reason (recorded with the change)', 'why this change');
|
|
903
|
+
const actions = el('div', undefined, 'actions'); actions.append(preview);
|
|
904
|
+
form.append(actions, result);
|
|
905
|
+
const body = () => { const set = fields.set(); return set ? {file, revision: edit.revisions[file], set} : null; };
|
|
906
|
+
preview.addEventListener('click', async () => {
|
|
907
|
+
structureState.error = ''; structureState.field = ''; structureState.lines = [];
|
|
908
|
+
const request = body();
|
|
909
|
+
if (!request) { if (!structureState.error) structureState.error = 'Give a mode or the required checks.'; structureResult(result); return; }
|
|
910
|
+
try {
|
|
911
|
+
const reply = await post('/action', {action:'config_edit_preview', edit: request});
|
|
912
|
+
if (reply.ok === false) { structureState.error = reply.error || 'The hub refused this change.'; structureState.field = reply.field || ''; }
|
|
913
|
+
else { structureState.lines = (reply.lines || []).map(String); structureState.tier = reply.tier || 2; }
|
|
914
|
+
} catch (error) { structureState.error = error.message; }
|
|
915
|
+
structureResult(result, (box) => {
|
|
916
|
+
if (structureState.error || structureState.tier === 3) { if (!structureState.error) box.append(el('p', 'This key runs code or locates secrets: change it in the console, never here.', 'structure-error')); return; }
|
|
917
|
+
const write = el('button', 'Write change', 'primary'); write.type = 'button'; write.dataset.mutation = 'project';
|
|
918
|
+
write.addEventListener('click', async () => {
|
|
919
|
+
if (!reason.value.trim()) { notice('Give a reason before writing.', true); return; }
|
|
920
|
+
const done = await action({action:'config_edit_apply', edit: request, reason: reason.value.trim()}, write);
|
|
921
|
+
if (done) { structureState.lines = []; structureResult(result); }
|
|
922
|
+
});
|
|
923
|
+
box.append(write);
|
|
924
|
+
});
|
|
925
|
+
});
|
|
926
|
+
return form;
|
|
927
|
+
}
|
|
928
|
+
function renderStructure(target, edit, raise) {
|
|
929
|
+
const group = el('div', undefined, 'structure-group'), head = el('div', undefined, 'row-head');
|
|
930
|
+
head.append(el('span', 'Structure', 'label'));
|
|
931
|
+
if (edit.sdlc && edit.sdlc.pending) head.append(badge('restart pending', 'warn'));
|
|
932
|
+
group.append(head);
|
|
933
|
+
if (!raise) group.append(el('p', 'Read only here. Open a settings session with ahub ui --settings to change instances, review, snapshots, approvals and the lifecycle.', 'meta'));
|
|
934
|
+
for (const instance of edit.instances || []) {
|
|
935
|
+
const row = el('div', undefined, 'structure-instance'), line = Object.entries(instance.fields).map(([key, value]) => key + ': ' + (Array.isArray(value) ? value.join(', ') : String(value))).join(' / ');
|
|
936
|
+
row.append(el('p', instance.id, 'label'), el('p', line || 'no tier 2 fields set', 'meta'));
|
|
937
|
+
if (instance.tier3.length) row.append(el('p', instance.tier3.join(', ') + ': set in the console', 'meta'));
|
|
938
|
+
group.append(row);
|
|
939
|
+
}
|
|
940
|
+
for (const block of edit.blocks || []) group.append(el('p', block.path + ': ' + settingText(block.value), 'meta'));
|
|
941
|
+
const lifecycle = el('div', undefined, 'structure-sdlc');
|
|
942
|
+
lifecycle.append(el('p', 'Lifecycle (sdlc.toml): ' + edit.sdlc.mode + '; required checks: ' + (edit.sdlc.requiredChecks.length ? edit.sdlc.requiredChecks.join(', ') : 'none'), 'meta'));
|
|
943
|
+
group.append(lifecycle);
|
|
944
|
+
if (raise) {
|
|
945
|
+
lifecycle.append(structureForm('structure-sdlc-form', 'Change the lifecycle', (form, fields) => {
|
|
946
|
+
const mode = structureField(form, 'sdlc-mode', 'sdlc mode (off, advise, enforce)', edit.sdlc.mode), checks = structureField(form, 'sdlc-checks', 'required checks, comma list', edit.sdlc.requiredChecks.join(', '));
|
|
947
|
+
fields.set = () => { const set = []; if (mode.value.trim()) set.push({path:['mode'], value: mode.value.trim()}); if (checks.value.trim()) set.push({path:['required_checks'], value: structureList(checks.value)}); return set.length ? set : null; };
|
|
948
|
+
}, 'sdlc.toml', edit, raise));
|
|
949
|
+
const add = structureForm('structure-add', 'Add an instance, or change one field of an existing one (type clear to remove a field)', (form, fields) => {
|
|
950
|
+
const id = structureField(form, 'id', 'instance id, <kind>-<name>', 'codex-review');
|
|
951
|
+
const inputs = {}; for (const [name] of STRUCTURE_FIELDS) inputs[name] = structureField(form, name, name + (name === 'roles' ? ' (comma list)' : name === 'readOnly' ? ' (true or false)' : ''), '');
|
|
952
|
+
const convert = (name, text) => name === 'roles' ? structureList(text) : name === 'readOnly' ? text === 'true' : text;
|
|
953
|
+
fields.set = () => {
|
|
954
|
+
const name = id.value.trim(), known = (edit.known || []).includes(name), typed = [];
|
|
955
|
+
for (const [field] of STRUCTURE_FIELDS) { const text = inputs[field].value.trim(); if (text) typed.push([field, text]); }
|
|
956
|
+
if (!name) { structureState.error = 'An instance needs an id and a kind.'; return null; }
|
|
957
|
+
// readOnly is exactly true or false: anything else is the person's typo, never a silent false.
|
|
958
|
+
if (typed.some(([field, text]) => field === 'readOnly' && text !== 'true' && text !== 'false' && text !== 'clear')) { structureState.error = 'readOnly is true or false.'; return null; }
|
|
959
|
+
if (known) {
|
|
960
|
+
// An existing instance changes leaf by leaf: a field left blank keeps its value, and a tier 3 field it holds is never part of the request.
|
|
961
|
+
if (!typed.length) { structureState.error = 'Type the field to change, or clear to remove it.'; return null; }
|
|
962
|
+
return typed.map(([field, text]) => ({path:['instances', name, field], value: text === 'clear' ? null : convert(field, text)}));
|
|
963
|
+
}
|
|
964
|
+
const value = {}; for (const [field, text] of typed) { if (text !== 'clear') value[field] = convert(field, text); }
|
|
965
|
+
if (!value.kind) { structureState.error = 'An instance needs an id and a kind.'; return null; }
|
|
966
|
+
return [{path:['instances', name], value}];
|
|
967
|
+
};
|
|
968
|
+
}, 'config.local.json', edit, raise);
|
|
969
|
+
group.append(add);
|
|
970
|
+
}
|
|
971
|
+
const tier3 = el('div', undefined, 'structure-tier3');
|
|
972
|
+
tier3.append(el('p', 'Executable and secret-locating keys', 'label'), el('p', "Changing one runs code with the daemon's rights or chooses where secrets are read from. It is never done from a browser: use the console command.", 'meta'));
|
|
973
|
+
for (const item of edit.tier3 || []) { tier3.append(el('p', item.key + (item.set ? ' (set)' : ' (not set)'), 'meta'), el('code', item.command)); }
|
|
974
|
+
group.append(tier3);
|
|
975
|
+
const backups = el('div', undefined, 'structure-backups');
|
|
976
|
+
backups.append(el('p', 'Backups (the last ' + (edit.backups || []).length + ')', 'label'));
|
|
977
|
+
for (const item of edit.backups || []) backups.append(el('p', new Date(item.at).toISOString() + ' ' + item.file + ' ' + item.bytes + ' bytes', 'meta'));
|
|
978
|
+
group.append(backups);
|
|
979
|
+
target.append(group);
|
|
980
|
+
}
|
|
866
981
|
const settingSlots = new Map();
|
|
867
982
|
function setText(node, text) { if (node.textContent !== text) node.textContent = text; }
|
|
868
983
|
function renderSettings(settings, roster = []) {
|
|
@@ -876,6 +991,7 @@ function renderSettings(settings, roster = []) {
|
|
|
876
991
|
settingSlots.clear();
|
|
877
992
|
const reload = el('div', undefined, 'settings-reload'); settingSlots.set('$reload', { node: reload, signature: null }); target.append(reload);
|
|
878
993
|
const undo = el('div'); settingSlots.set('', { node: undo, signature: null }); target.append(undo);
|
|
994
|
+
const structure = el('div'); settingSlots.set('$structure', { node: structure, signature: null }); target.append(structure);
|
|
879
995
|
for (const group of SETTING_GROUPS) {
|
|
880
996
|
const rows = settings.rows.filter((row) => row.group === group);
|
|
881
997
|
if (!rows.length) continue;
|
|
@@ -903,6 +1019,11 @@ function renderSettings(settings, roster = []) {
|
|
|
903
1019
|
}
|
|
904
1020
|
}
|
|
905
1021
|
}
|
|
1022
|
+
const structureSlot = settingSlots.get('$structure'), structureSignature = JSON.stringify([settings.configEdit, raise]);
|
|
1023
|
+
if (structureSlot && structureSlot.signature !== structureSignature) {
|
|
1024
|
+
structureSlot.signature = structureSignature; structureSlot.node.replaceChildren();
|
|
1025
|
+
if (settings.configEdit && Array.isArray(settings.configEdit.instances)) renderStructure(structureSlot.node, settings.configEdit, raise);
|
|
1026
|
+
}
|
|
906
1027
|
const undoSlot = settingSlots.get(''), undoKey = settings.undo || '';
|
|
907
1028
|
if (undoSlot && undoSlot.signature !== undoKey) {
|
|
908
1029
|
undoSlot.signature = undoKey; undoSlot.node.replaceChildren();
|
|
@@ -1132,6 +1253,24 @@ function openMemoryAt(node) {
|
|
|
1132
1253
|
memoryQuery.node = node; memoryQuery.q = ''; $('memory-q').value = '';
|
|
1133
1254
|
closeTaskDetail(); switchView('memory'); rerenderMemory();
|
|
1134
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
|
+
}
|
|
1135
1274
|
function taskDetail(task, card, stage, ids, overdue) {
|
|
1136
1275
|
const row = el('article', undefined, 'task-detail'), head = el('div', undefined, 'row-head');
|
|
1137
1276
|
row.setAttribute('aria-label', '#' + task.id + ' ' + task.title + ', ' + task.state.replaceAll('_', ' ') + ', owner ' + (task.owner || 'unassigned') + ', priority P' + card.priority);
|
|
@@ -1163,6 +1302,7 @@ function taskDetail(task, card, stage, ids, overdue) {
|
|
|
1163
1302
|
if(task.pinned)links.append(el('p','Pinned','meta'));
|
|
1164
1303
|
if (links.children.length > 1) row.append(links);
|
|
1165
1304
|
const code = codeGroupSection(task); if (code) row.append(code);
|
|
1305
|
+
const delivered = deliverablesSection(task); if (delivered) row.append(delivered);
|
|
1166
1306
|
const controls = el('div', undefined, 'actions');
|
|
1167
1307
|
if (card.pull && boardViewState?.snapshot.boardCapabilities?.backlog===true) controls.append(boardButton('Pull into To Do', {action:'backlog_pull',id:task.id}));
|
|
1168
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'}));
|
|
@@ -1194,6 +1334,7 @@ function taskCard(task, card, stage, ids, overdue, compact = true) {
|
|
|
1194
1334
|
if (card.starred) { const star = el('span', undefined, 'star'); star.title = 'starred'; star.append(icon('i-star', 'starred')); head.append(star); }
|
|
1195
1335
|
const meta = el('div', undefined, 'card-meta'); meta.append(cardBadge(task, card, overdue));
|
|
1196
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)); }
|
|
1197
1338
|
row.append(head, meta);
|
|
1198
1339
|
row.addEventListener('click', () => openTaskDetail(task.id));
|
|
1199
1340
|
row.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openTaskDetail(task.id); } });
|
|
@@ -1498,6 +1639,8 @@ function renderUsage(usage, tasks, budget) {
|
|
|
1498
1639
|
}
|
|
1499
1640
|
$('usage-task-count').textContent = taskCount + ' tasks';
|
|
1500
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'); }
|
|
1501
1644
|
function render(snapshot) {
|
|
1502
1645
|
renderNeedsAttention(snapshot);
|
|
1503
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 : [];
|
|
@@ -1563,13 +1706,14 @@ function render(snapshot) {
|
|
|
1563
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); }
|
|
1564
1707
|
}
|
|
1565
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); }
|
|
1566
|
-
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'));
|
|
1567
1711
|
for (const window of budget.windows) {
|
|
1568
1712
|
const item = el('div', undefined, 'budget-window'), percent = Math.round(window.used * 100);
|
|
1569
|
-
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'));
|
|
1570
1714
|
const bar = el('progress'); bar.max = 1; bar.value = window.used; bar.className = percent >= 90 ? 'critical' : percent >= 70 ? 'warn' : '';
|
|
1571
1715
|
bar.setAttribute('aria-label', peer + ' ' + window.id + ' usage'); item.append(bar);
|
|
1572
|
-
const meta = el('p', window.source + (window.resetsAt ? ' / resets ' : ''), 'meta');
|
|
1716
|
+
const meta = el('p', windowKindOf(window) + ' / ' + window.source + (window.resetsAt ? ' / resets ' : ''), 'meta');
|
|
1573
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); }
|
|
1574
1718
|
item.append(meta); row.append(item);
|
|
1575
1719
|
}
|
|
@@ -2105,21 +2249,22 @@ function renderBench(bench) {
|
|
|
2105
2249
|
}
|
|
2106
2250
|
// #258: the hub's memory, read-only. A tier at a time, filtered; selecting an entry or a node shows its neighbourhood
|
|
2107
2251
|
// (inline SVG, a small force layout computed here) with the same edges as a list, and its detail.
|
|
2108
|
-
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};
|
|
2109
2253
|
let memoryGeneration = -1;
|
|
2110
2254
|
function memoryInput() {
|
|
2111
2255
|
if (memoryGeneration !== viewGeneration) { // another project: its own nodes, peers and machines
|
|
2112
|
-
memoryGeneration = viewGeneration; memoryQuery.node = ''; memoryQuery.peer = ''; memoryQuery.machine = '';
|
|
2256
|
+
memoryGeneration = viewGeneration; memoryQuery.node = ''; memoryQuery.peer = ''; memoryQuery.machine = ''; memoryQuery.codeNeighbours = false;
|
|
2113
2257
|
for (const id of ['memory-peer', 'memory-machine']) $(id).value = '';
|
|
2114
2258
|
}
|
|
2115
2259
|
const out = {tier:memoryQuery.tier, hops:memoryQuery.hops};
|
|
2116
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;
|
|
2117
2262
|
// A whole timestamp, floored to the hour so the query stays the same between polls.
|
|
2118
2263
|
if (memoryQuery.since) out.since = new Date(Math.floor((Date.now() - Number(memoryQuery.since) * 86400000) / 3600000) * 3600000).toISOString();
|
|
2119
2264
|
return out;
|
|
2120
2265
|
}
|
|
2121
2266
|
// The next poll (at most a second away) carries the changed query; polling is never started twice.
|
|
2122
|
-
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)); }
|
|
2123
2268
|
function memoryOptions(id, values, all, key) {
|
|
2124
2269
|
const select = $(id), keep = select.value;
|
|
2125
2270
|
const want = [''].concat(values);
|
|
@@ -2128,6 +2273,51 @@ function memoryOptions(id, values, all, key) {
|
|
|
2128
2273
|
select.value = want.includes(keep) ? keep : '';
|
|
2129
2274
|
memoryQuery[key] = select.value; // a filter whose value is gone reads as none, on the page and in the next query
|
|
2130
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
|
+
}
|
|
2131
2321
|
function layout(graph) {
|
|
2132
2322
|
// Deterministic: start on a circle in node order, then a fixed number of spring steps; no animation loop.
|
|
2133
2323
|
const W = 640, H = 320, n = graph.nodes.length, pos = new Map();
|
|
@@ -2150,6 +2340,7 @@ function layout(graph) {
|
|
|
2150
2340
|
}
|
|
2151
2341
|
function selectMemoryNode(node) { memoryQuery.node = memoryQuery.node === node ? '' : node; rerenderMemory(); }
|
|
2152
2342
|
function renderMemory(memory) {
|
|
2343
|
+
memoryCodeToggle();
|
|
2153
2344
|
const tiers = $('memory-tiers');
|
|
2154
2345
|
if (!tiers.children.length) for (const [tier, label] of [['short', 'Short-term'], ['long', 'Long-term'], ['goals', 'Long-term tasks']]) {
|
|
2155
2346
|
const chip = el('button', label, 'chip'); chip.type = 'button'; chip.dataset.tier = tier;
|
|
@@ -2158,6 +2349,7 @@ function renderMemory(memory) {
|
|
|
2158
2349
|
}
|
|
2159
2350
|
for (const chip of tiers.children) chip.setAttribute('aria-pressed', String(chip.dataset.tier === memoryQuery.tier));
|
|
2160
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);
|
|
2161
2353
|
$('memory-count').textContent = memory.counts.long + ' long-term / ' + memory.counts.short + ' short-term / ' + (memory.counts.goals || 0) + ' open tasks';
|
|
2162
2354
|
if (memory.goals) {
|
|
2163
2355
|
// Long-term tasks: state, who holds the lease and until when (a conflict when two machines claimed it), linked board tasks.
|
|
@@ -2183,15 +2375,16 @@ function renderMemory(memory) {
|
|
|
2183
2375
|
}
|
|
2184
2376
|
memoryOptions('memory-peer', memory.peers, 'All peers', 'peer');
|
|
2185
2377
|
memoryOptions('memory-machine', memory.machines, 'All machines', 'machine');
|
|
2186
|
-
if (!memory.goals) update('memory-list', {entries:
|
|
2187
|
-
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>.');
|
|
2188
2380
|
const keyed = keyedStart('memory');
|
|
2189
|
-
for (const entry of
|
|
2381
|
+
for (const entry of presentation.entries) {
|
|
2190
2382
|
const row = keyed('mem:' + entry.id, JSON.stringify([entry, memoryQuery.node === 'mem:' + entry.id]), () => {
|
|
2191
|
-
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));
|
|
2192
2384
|
const head = el('div', undefined, 'row-head');
|
|
2193
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' : ''));
|
|
2194
|
-
|
|
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'));
|
|
2195
2388
|
row.addEventListener('click', () => selectMemoryNode('mem:' + entry.id));
|
|
2196
2389
|
return row;
|
|
2197
2390
|
});
|
|
@@ -2199,19 +2392,21 @@ function renderMemory(memory) {
|
|
|
2199
2392
|
}
|
|
2200
2393
|
keyed.end();
|
|
2201
2394
|
});
|
|
2202
|
-
update('memory-graph',
|
|
2203
|
-
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'));
|
|
2204
2398
|
if (!graph || !graph.nodes.length) return;
|
|
2205
2399
|
const NS = 'http://www.w3.org/2000/svg', svg = document.createElementNS(NS, 'svg'), pos = layout(graph);
|
|
2206
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');
|
|
2207
2401
|
for (const e of graph.edges) {
|
|
2208
2402
|
const p = pos.get(e.from), q = pos.get(e.to); if (!p || !q) continue;
|
|
2209
|
-
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); }
|
|
2210
2405
|
svg.append(line);
|
|
2211
2406
|
}
|
|
2212
2407
|
for (const node of graph.nodes) {
|
|
2213
2408
|
const p = pos.get(node.id), g = document.createElementNS(NS, 'g'), c = document.createElementNS(NS, 'circle'), t = document.createElementNS(NS, 'text');
|
|
2214
|
-
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);
|
|
2215
2410
|
c.setAttribute('cx', p.x); c.setAttribute('cy', p.y); c.setAttribute('r', node.type === 'memory' ? 9 : 6);
|
|
2216
2411
|
t.setAttribute('x', p.x + 11); t.setAttribute('y', p.y + 4); t.textContent = node.label.slice(0, 28);
|
|
2217
2412
|
g.append(c, t);
|
|
@@ -2220,7 +2415,7 @@ function renderMemory(memory) {
|
|
|
2220
2415
|
svg.append(g);
|
|
2221
2416
|
}
|
|
2222
2417
|
const list = el('ul', undefined, 'memory-edges');
|
|
2223
|
-
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 + ')' : '')));
|
|
2224
2419
|
target.append(svg, list);
|
|
2225
2420
|
});
|
|
2226
2421
|
update('memory-detail', memory.detail || null, (target) => {
|
|
@@ -2236,6 +2431,7 @@ function renderMemory(memory) {
|
|
|
2236
2431
|
});
|
|
2237
2432
|
}
|
|
2238
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(); });
|
|
2239
2435
|
$('memory-hops').addEventListener('change', () => { memoryQuery.hops = Number($('memory-hops').value); rerenderMemory(); });
|
|
2240
2436
|
let memorySearch;
|
|
2241
2437
|
$('memory-q').addEventListener('input', () => { clearTimeout(memorySearch); memorySearch = setTimeout(() => { memoryQuery.q = $('memory-q').value.trim(); rerenderMemory(); }, 300); });
|