@crewhaus/hangar-ui 0.5.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/README.md +309 -0
- package/assets/hangar.css +1726 -0
- package/assets/index.html +21 -0
- package/assets/js/api.js +368 -0
- package/assets/js/app.js +734 -0
- package/assets/js/dom.js +228 -0
- package/assets/js/keys.js +159 -0
- package/assets/js/markdown.js +131 -0
- package/assets/js/notify.js +152 -0
- package/assets/js/omnibox.js +307 -0
- package/assets/js/router.js +280 -0
- package/assets/js/routes.js +793 -0
- package/assets/js/shapes.js +36 -0
- package/assets/js/supervision.js +1334 -0
- package/assets/js/util.js +426 -0
- package/assets/js/views/activity.js +140 -0
- package/assets/js/views/approvals.js +155 -0
- package/assets/js/views/channels.js +455 -0
- package/assets/js/views/control.js +253 -0
- package/assets/js/views/costs.js +103 -0
- package/assets/js/views/creds.js +610 -0
- package/assets/js/views/data.js +456 -0
- package/assets/js/views/deploy.js +106 -0
- package/assets/js/views/evals-lab.js +1029 -0
- package/assets/js/views/evals.js +342 -0
- package/assets/js/views/feedback.js +555 -0
- package/assets/js/views/health.js +176 -0
- package/assets/js/views/inbox.js +103 -0
- package/assets/js/views/inspect.js +751 -0
- package/assets/js/views/jobs.js +110 -0
- package/assets/js/views/library.js +605 -0
- package/assets/js/views/memory-fabric.js +2026 -0
- package/assets/js/views/memory.js +228 -0
- package/assets/js/views/onboarding.js +253 -0
- package/assets/js/views/overview.js +196 -0
- package/assets/js/views/panes.js +132 -0
- package/assets/js/views/proc.js +280 -0
- package/assets/js/views/review.js +164 -0
- package/assets/js/views/runs.js +452 -0
- package/assets/js/views/runtime.js +450 -0
- package/assets/js/views/schedulers.js +133 -0
- package/assets/js/views/security.js +925 -0
- package/assets/js/views/sessions.js +265 -0
- package/assets/js/views/settings.js +489 -0
- package/assets/js/views/spec-edit.js +1289 -0
- package/assets/js/views/spec.js +66 -0
- package/assets/js/views/thredz.js +1853 -0
- package/assets/js/views/token.js +49 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +145 -0
- package/package.json +36 -0
|
@@ -0,0 +1,2026 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory — the M3 half of the Memory tab: the facts browser with provenance,
|
|
3
|
+
* the recall playground, the continuity panel and its trash, the wiki
|
|
4
|
+
* browser/editor with versions and the link graph, the dream control panel,
|
|
5
|
+
* watchme analytics and the synthesize review, the learning panel, and
|
|
6
|
+
* knowledge sync.
|
|
7
|
+
*
|
|
8
|
+
* The M1 reads (`views/memory.js`: facts/wiki/state/dream/watchme cards)
|
|
9
|
+
* stay where they are and are rendered above this.
|
|
10
|
+
*
|
|
11
|
+
* THE INVARIANT THIS SCREEN EXISTS TO MAKE VISIBLE: nothing here deletes.
|
|
12
|
+
* Forgetting a fact writes a supersede tombstone with the operator's reason,
|
|
13
|
+
* clearing continuity moves it to a restorable trash snapshot, and a wiki
|
|
14
|
+
* article is archived rather than removed. Every one of those is reversible,
|
|
15
|
+
* and every button says so AT THE POINT OF THE CLICK — a promise made only
|
|
16
|
+
* in a docblock is a promise the operator never reads.
|
|
17
|
+
*
|
|
18
|
+
* Two more house rules this file leans on:
|
|
19
|
+
* - a refusal is a STATE, not a toast. A stale wiki write comes back 200
|
|
20
|
+
* with the version that moved and the diff; the editor renders it inline
|
|
21
|
+
* with a retry button rather than throwing the operator's draft away.
|
|
22
|
+
* - judge verdicts and human feedback are drawn in separate blocks, with
|
|
23
|
+
* their sources named, because they are separate stores by design.
|
|
24
|
+
*
|
|
25
|
+
* Panels load lazily: seven areas' worth of routes on one paint would make
|
|
26
|
+
* the tab slow for an operator who came to look at one of them.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { api } from "../api.js";
|
|
30
|
+
import {
|
|
31
|
+
asOf,
|
|
32
|
+
clear,
|
|
33
|
+
collapsible,
|
|
34
|
+
copyBtn,
|
|
35
|
+
dot,
|
|
36
|
+
el,
|
|
37
|
+
emptyState,
|
|
38
|
+
jsonPre,
|
|
39
|
+
mdBlocks,
|
|
40
|
+
skeleton,
|
|
41
|
+
toast,
|
|
42
|
+
} from "../dom.js";
|
|
43
|
+
import { hrefHarness } from "../router.js";
|
|
44
|
+
import { fmtCount, fmtPct, fmtRelativeTime, fmtUsd, usdFromMicros } from "../util.js";
|
|
45
|
+
|
|
46
|
+
const PANELS = [
|
|
47
|
+
["facts", "Facts"],
|
|
48
|
+
["continuity", "Continuity"],
|
|
49
|
+
["wiki", "Wiki"],
|
|
50
|
+
["dream", "Dream"],
|
|
51
|
+
["watchme", "Watchme"],
|
|
52
|
+
["learning", "Learning"],
|
|
53
|
+
["knowledge", "Knowledge"],
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// pure helpers (unit-tested in hangar-ui/src/memory-fabric.test.ts)
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
const DAY_MS = 86_400_000;
|
|
61
|
+
|
|
62
|
+
/** What the server's value-shape masker leaves behind. Text carrying it is
|
|
63
|
+
* not safe to write back — the mask would replace what it hid. */
|
|
64
|
+
const MASK_MARK = "***";
|
|
65
|
+
|
|
66
|
+
/** Human TTL countdown for a fact. Null when the fact has no expiry. */
|
|
67
|
+
export function ttlLabel(expiresInMs) {
|
|
68
|
+
if (typeof expiresInMs !== "number") return null;
|
|
69
|
+
const days = Math.abs(expiresInMs) / DAY_MS;
|
|
70
|
+
const rounded = days >= 1 ? `${Math.round(days)}d` : `${Math.max(1, Math.round(days * 24))}h`;
|
|
71
|
+
return expiresInMs >= 0 ? `expires in ${rounded}` : `past TTL by ${rounded}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Traffic-light state for a folded fact status (never colour alone). */
|
|
75
|
+
export function factTone(status) {
|
|
76
|
+
if (status === "live") return "ok";
|
|
77
|
+
if (status === "superseded") return "off";
|
|
78
|
+
return "warn";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Split watchme's per-model spend into what is PRICED and what is not. An
|
|
83
|
+
* unpriced model's cost is UNKNOWN, and folding it into the total would turn
|
|
84
|
+
* a measurement gap into a false zero — so it gets its own line, always.
|
|
85
|
+
*/
|
|
86
|
+
export function costSummary(analytics) {
|
|
87
|
+
const a = analytics && typeof analytics === "object" ? analytics : {};
|
|
88
|
+
const unpriced = a.unpriced && typeof a.unpriced === "object" ? a.unpriced : {};
|
|
89
|
+
const models = Array.isArray(unpriced.models) ? unpriced.models : [];
|
|
90
|
+
return {
|
|
91
|
+
priced: typeof a.costUsdMicros === "number" ? a.costUsdMicros : 0,
|
|
92
|
+
unpricedModels: models.length,
|
|
93
|
+
unpricedTurns: typeof unpriced.turns === "number" ? unpriced.turns : 0,
|
|
94
|
+
note: models.length === 0 ? null : (unpriced.note ?? "some models are unpriced"),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Classify a wiki write answer. `stale_article_version` is a first-class
|
|
100
|
+
* state carrying the version to retry from — never an error toast.
|
|
101
|
+
*/
|
|
102
|
+
export function writeOutcome(body) {
|
|
103
|
+
const b = body && typeof body === "object" ? body : {};
|
|
104
|
+
if (b.ok === true) return { kind: "ok", message: String(b.note ?? "saved"), retryVersion: null };
|
|
105
|
+
if (b.code === "stale_article_version") {
|
|
106
|
+
const current = b.current && typeof b.current === "object" ? b.current : {};
|
|
107
|
+
return {
|
|
108
|
+
kind: "stale",
|
|
109
|
+
message: String(b.note ?? "the article moved under you"),
|
|
110
|
+
retryVersion: typeof b.currentVersion === "number" ? b.currentVersion : null,
|
|
111
|
+
currentBody: typeof current.body === "string" ? current.body : "",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (b.code === "missing_sources") {
|
|
115
|
+
return {
|
|
116
|
+
kind: "gate",
|
|
117
|
+
message: String(b.note ?? "a ## Sources heading is required"),
|
|
118
|
+
retryVersion: null,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
kind: "error",
|
|
123
|
+
message: String(b.note ?? b.message ?? "the write was refused"),
|
|
124
|
+
retryVersion: null,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Dream lane verdict: overdue, on schedule, or not on a timer at all. */
|
|
129
|
+
export function dreamTone(payload) {
|
|
130
|
+
const p = payload && typeof payload === "object" ? payload : {};
|
|
131
|
+
if (p.declared !== true) return { state: "off", label: "no dream cadence declared" };
|
|
132
|
+
if (p.overdue === true) return { state: "warn", label: "overdue" };
|
|
133
|
+
return { state: "ok", label: `every ${String(p.cadence ?? "?")}` };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Staleness label for a REFLECT row, or null when it is fresh. */
|
|
137
|
+
export function staleLabel(row) {
|
|
138
|
+
const r = row && typeof row === "object" ? row : {};
|
|
139
|
+
if (typeof r.staleMs !== "number") return null;
|
|
140
|
+
const days = Math.floor(r.staleMs / DAY_MS);
|
|
141
|
+
const label = days >= 1 ? `${days}d since last touch` : "touched today";
|
|
142
|
+
return r.stale === true ? `stale — ${label}` : label;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// shared rendering
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
/** Card shell used by every panel. */
|
|
150
|
+
function card(title, children, extraTitle = null) {
|
|
151
|
+
return el("section", { class: "card ov-card ov-wide" }, [
|
|
152
|
+
el("h3", { class: "card-title" }, [el("span", { text: title }), extraTitle]),
|
|
153
|
+
...(Array.isArray(children) ? children : [children]),
|
|
154
|
+
]);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** A refusal the server answered with — including "not built yet" (501). */
|
|
158
|
+
function refusalCard(res) {
|
|
159
|
+
const body = res && typeof res.body === "object" && res.body !== null ? res.body : {};
|
|
160
|
+
const message = String(body.error ?? body.message ?? `HTTP ${res?.status ?? 0}`);
|
|
161
|
+
return el("div", { class: "card error-card" }, [
|
|
162
|
+
el("h3", { class: "card-title" }, [
|
|
163
|
+
el("span", { text: "Not available" }),
|
|
164
|
+
dot(res?.status === 501 ? "off" : "bad", `HTTP ${res?.status ?? 0}`),
|
|
165
|
+
]),
|
|
166
|
+
el("p", { class: "muted", text: message }),
|
|
167
|
+
]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Load one M3 read into `host`, rendering its refusal instead on failure. */
|
|
171
|
+
async function load(host, call) {
|
|
172
|
+
const res = await call();
|
|
173
|
+
if (!res.ok) {
|
|
174
|
+
host.appendChild(refusalCard(res));
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
return res.body && typeof res.body === "object" ? res.body : {};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Run one write with a busy button. A non-2xx (or an `ok:false` envelope)
|
|
182
|
+
* never fails silently: it becomes a toast carrying the server's own note,
|
|
183
|
+
* which is the sentence that explains what to do next.
|
|
184
|
+
*/
|
|
185
|
+
async function run(btn, label, call, onDone) {
|
|
186
|
+
const original = btn.textContent;
|
|
187
|
+
btn.disabled = true;
|
|
188
|
+
btn.textContent = `${label}…`;
|
|
189
|
+
let res;
|
|
190
|
+
try {
|
|
191
|
+
res = await call();
|
|
192
|
+
} catch (err) {
|
|
193
|
+
res = { ok: false, status: 0, body: { message: String(err) } };
|
|
194
|
+
}
|
|
195
|
+
btn.textContent = original;
|
|
196
|
+
btn.disabled = false;
|
|
197
|
+
const body = res.body && typeof res.body === "object" ? res.body : {};
|
|
198
|
+
if (!res.ok) {
|
|
199
|
+
toast(`${label} failed: ${String(body.error ?? body.message ?? res.status)}`);
|
|
200
|
+
} else if (body.ok === false) {
|
|
201
|
+
toast(String(body.note ?? body.message ?? `${label} was refused`));
|
|
202
|
+
}
|
|
203
|
+
if (onDone) onDone(res);
|
|
204
|
+
return res;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** A labelled key/value row. */
|
|
208
|
+
function kv(k, v) {
|
|
209
|
+
return el("div", { class: "kv" }, [
|
|
210
|
+
el("span", { class: "kv-k", text: k }),
|
|
211
|
+
el("span", { class: "kv-v" }, typeof v === "string" ? el("span", { text: v }) : v),
|
|
212
|
+
]);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** A table from a header list and row-node builder. */
|
|
216
|
+
function table(headers, rows) {
|
|
217
|
+
const tbody = el("tbody");
|
|
218
|
+
for (const row of rows) tbody.appendChild(row);
|
|
219
|
+
return el("div", { class: "table-scroll" }, [
|
|
220
|
+
el("table", { class: "fleet" }, [
|
|
221
|
+
el(
|
|
222
|
+
"thead",
|
|
223
|
+
null,
|
|
224
|
+
el(
|
|
225
|
+
"tr",
|
|
226
|
+
null,
|
|
227
|
+
headers.map((h) => el("th", { text: h })),
|
|
228
|
+
),
|
|
229
|
+
),
|
|
230
|
+
tbody,
|
|
231
|
+
]),
|
|
232
|
+
]);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The empty state for a payload that carries its own note + verb. */
|
|
236
|
+
function empty(payload, fallback) {
|
|
237
|
+
const note = typeof payload?.note === "string" ? payload.note : fallback;
|
|
238
|
+
const verb = typeof payload?.verb === "string" ? payload.verb : null;
|
|
239
|
+
return emptyState(note ?? fallback, verb);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** A text input with a label. */
|
|
243
|
+
function field(label, attrs = {}) {
|
|
244
|
+
const input = el("input", { class: "input", type: "text", ...attrs });
|
|
245
|
+
return { node: el("label", { class: "field" }, [el("span", { text: label }), input]), input };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** A deep link into the session viewer — the join that makes memory auditable. */
|
|
249
|
+
function sessionLink(ctx, sessionId, label) {
|
|
250
|
+
return el("a", {
|
|
251
|
+
class: "mono",
|
|
252
|
+
href: hrefHarness(ctx.id, "sessions", sessionId),
|
|
253
|
+
text: label ?? sessionId,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
// the shell
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
export async function renderMemoryFabric(root, ctx) {
|
|
262
|
+
const state = { panel: "facts" };
|
|
263
|
+
clear(root);
|
|
264
|
+
const nav = el("nav", { class: "tabs", "aria-label": "memory fabric panels" });
|
|
265
|
+
const host = el("div", { class: "tab-body" });
|
|
266
|
+
const draw = () => {
|
|
267
|
+
clear(nav);
|
|
268
|
+
for (const [id, label] of PANELS) {
|
|
269
|
+
nav.appendChild(
|
|
270
|
+
el("button", {
|
|
271
|
+
class: `tab${state.panel === id ? " active" : ""}`,
|
|
272
|
+
type: "button",
|
|
273
|
+
"aria-current": state.panel === id ? "page" : null,
|
|
274
|
+
text: label,
|
|
275
|
+
onClick: () => {
|
|
276
|
+
state.panel = id;
|
|
277
|
+
draw();
|
|
278
|
+
},
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
clear(host).appendChild(skeleton(6));
|
|
283
|
+
void panelFor(state.panel)(host, ctx, () => draw());
|
|
284
|
+
};
|
|
285
|
+
root.appendChild(el("h3", { class: "sub-title", text: "Memory fabric" }));
|
|
286
|
+
root.appendChild(nav);
|
|
287
|
+
root.appendChild(host);
|
|
288
|
+
draw();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function panelFor(id) {
|
|
292
|
+
switch (id) {
|
|
293
|
+
case "continuity":
|
|
294
|
+
return continuityPanel;
|
|
295
|
+
case "wiki":
|
|
296
|
+
return wikiPanel;
|
|
297
|
+
case "dream":
|
|
298
|
+
return dreamPanel;
|
|
299
|
+
case "watchme":
|
|
300
|
+
return watchmePanel;
|
|
301
|
+
case "learning":
|
|
302
|
+
return learningPanel;
|
|
303
|
+
case "knowledge":
|
|
304
|
+
return knowledgePanel;
|
|
305
|
+
default:
|
|
306
|
+
return factsPanel;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
// facts + recall
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
async function factsPanel(host, ctx, reload, spec) {
|
|
315
|
+
const first = spec ?? String(ctx.detail?.entry?.specName ?? "");
|
|
316
|
+
clear(host).appendChild(skeleton(6));
|
|
317
|
+
const data = await load(host, () => api.memoryFacts({ id: ctx.id, spec: first || "default" }));
|
|
318
|
+
if (data === null) return;
|
|
319
|
+
clear(host);
|
|
320
|
+
|
|
321
|
+
const specs = Array.isArray(data.specs) ? data.specs.map(String) : [];
|
|
322
|
+
if (specs.length > 0) {
|
|
323
|
+
host.appendChild(
|
|
324
|
+
el("div", { class: "filter-bar" }, [
|
|
325
|
+
el("span", { class: "muted", text: "store" }),
|
|
326
|
+
...specs.map((name) =>
|
|
327
|
+
el("button", {
|
|
328
|
+
class: `btn btn-ghost${name === data.specName ? " active" : ""}`,
|
|
329
|
+
type: "button",
|
|
330
|
+
text: name,
|
|
331
|
+
onClick: () => void factsPanel(host, ctx, reload, name),
|
|
332
|
+
}),
|
|
333
|
+
),
|
|
334
|
+
]),
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const items = Array.isArray(data.items) ? data.items : [];
|
|
339
|
+
if (items.length === 0) {
|
|
340
|
+
host.appendChild(card("Facts", empty(data, "No facts yet")));
|
|
341
|
+
} else {
|
|
342
|
+
const counts = data.counts && typeof data.counts === "object" ? data.counts : {};
|
|
343
|
+
const live = items.filter((i) => i.status === "live");
|
|
344
|
+
const folded = items.filter((i) => i.status !== "live");
|
|
345
|
+
const rows = [
|
|
346
|
+
el("div", { class: "filter-bar" }, [
|
|
347
|
+
dot("ok", `${fmtCount(counts.live ?? 0)} live`),
|
|
348
|
+
dot("off", `${fmtCount(counts.superseded ?? 0)} superseded`),
|
|
349
|
+
dot("warn", `${fmtCount(counts.expired ?? 0)} expired`),
|
|
350
|
+
data.truncated === true
|
|
351
|
+
? el("span", { class: "chip chip-warn", text: "capped — counts are floors" })
|
|
352
|
+
: null,
|
|
353
|
+
]),
|
|
354
|
+
el(
|
|
355
|
+
"ul",
|
|
356
|
+
{ class: "fact-list" },
|
|
357
|
+
live.map((f) => factRow(f, ctx, host, reload, data.specName)),
|
|
358
|
+
),
|
|
359
|
+
];
|
|
360
|
+
if (folded.length > 0) {
|
|
361
|
+
rows.push(
|
|
362
|
+
collapsible(
|
|
363
|
+
[
|
|
364
|
+
el("span", {
|
|
365
|
+
class: "muted",
|
|
366
|
+
text: `${folded.length} superseded/expired — tombstoned, never deleted`,
|
|
367
|
+
}),
|
|
368
|
+
],
|
|
369
|
+
[
|
|
370
|
+
el(
|
|
371
|
+
"ul",
|
|
372
|
+
{ class: "fact-list folded" },
|
|
373
|
+
folded.map((f) => factRow(f, ctx, host, reload, data.specName)),
|
|
374
|
+
),
|
|
375
|
+
],
|
|
376
|
+
),
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
host.appendChild(card("Facts", rows));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
host.appendChild(sweepCard(ctx, String(data.specName ?? ""), host, reload));
|
|
383
|
+
host.appendChild(recallCard(ctx));
|
|
384
|
+
host.appendChild(migrateCard(ctx));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function factRow(fact, ctx, host, reload, spec) {
|
|
388
|
+
const provenance =
|
|
389
|
+
fact.provenance && typeof fact.provenance === "object" ? fact.provenance : null;
|
|
390
|
+
const meta = el("div", { class: "fact-meta" }, [
|
|
391
|
+
...(Array.isArray(fact.tags) ? fact.tags : []).map((t) =>
|
|
392
|
+
el("span", { class: "chip", text: String(t) }),
|
|
393
|
+
),
|
|
394
|
+
fact.status !== "live" ? dot(factTone(fact.status), String(fact.status)) : null,
|
|
395
|
+
ttlLabel(fact.expiresInMs) === null
|
|
396
|
+
? null
|
|
397
|
+
: el("span", {
|
|
398
|
+
class: fact.status === "expired" ? "chip chip-warn" : "chip",
|
|
399
|
+
text: ttlLabel(fact.expiresInMs),
|
|
400
|
+
}),
|
|
401
|
+
typeof fact.createdAt === "string" && fact.createdAt !== ""
|
|
402
|
+
? el("span", {
|
|
403
|
+
class: "muted",
|
|
404
|
+
title: fact.createdAt,
|
|
405
|
+
text: fmtRelativeTime(fact.createdAt, Date.now()),
|
|
406
|
+
})
|
|
407
|
+
: null,
|
|
408
|
+
]);
|
|
409
|
+
const children = [el("div", { text: String(fact.text ?? "") }), meta];
|
|
410
|
+
|
|
411
|
+
if (provenance !== null) {
|
|
412
|
+
const evidence = Array.isArray(provenance.evidence) ? provenance.evidence : [];
|
|
413
|
+
children.push(
|
|
414
|
+
el("div", { class: "fact-meta" }, [
|
|
415
|
+
el("span", { class: "muted", text: "from" }),
|
|
416
|
+
provenance.sessionId ? sessionLink(ctx, String(provenance.sessionId)) : null,
|
|
417
|
+
...evidence.map((toolUseId) =>
|
|
418
|
+
el("a", {
|
|
419
|
+
class: "chip chip-tool",
|
|
420
|
+
href: provenance.sessionId
|
|
421
|
+
? hrefHarness(ctx.id, "sessions", String(provenance.sessionId))
|
|
422
|
+
: hrefHarness(ctx.id, "sessions"),
|
|
423
|
+
title: `evidence ${String(toolUseId)} — open the session that produced it`,
|
|
424
|
+
text: String(toolUseId),
|
|
425
|
+
}),
|
|
426
|
+
),
|
|
427
|
+
]),
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (typeof fact.supersededBy === "string" || typeof fact.supersedeReason === "string") {
|
|
432
|
+
children.push(
|
|
433
|
+
el("div", { class: "muted" }, [
|
|
434
|
+
el("span", {
|
|
435
|
+
text: `superseded${fact.supersedeReason ? `: ${fact.supersedeReason}` : ""}`,
|
|
436
|
+
}),
|
|
437
|
+
fact.supersededBy ? el("span", { class: "mono", text: ` → ${fact.supersededBy}` }) : null,
|
|
438
|
+
]),
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (fact.status === "live") {
|
|
443
|
+
children.push(forgetControl(ctx, spec, String(fact.id ?? ""), host, reload));
|
|
444
|
+
}
|
|
445
|
+
return el("li", { class: "fact" }, children);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** Forget = a supersede tombstone with a REASON. The reason is required, and
|
|
449
|
+
* the control says what actually happens before the click. */
|
|
450
|
+
function forgetControl(ctx, spec, factId, host, reload) {
|
|
451
|
+
const wrap = el("div", { class: "row-editor" });
|
|
452
|
+
const open = el("button", { class: "btn btn-ghost", type: "button", text: "Forget…" });
|
|
453
|
+
open.addEventListener("click", () => {
|
|
454
|
+
clear(wrap);
|
|
455
|
+
const reason = field("why (recorded on the tombstone)", { placeholder: "superseded by …" });
|
|
456
|
+
const confirm = el("button", { class: "btn btn-danger", type: "button", text: "Forget" });
|
|
457
|
+
confirm.addEventListener("click", () => {
|
|
458
|
+
if (reason.input.value.trim() === "") {
|
|
459
|
+
toast("a reason is required — it is what the tombstone records");
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
void run(
|
|
463
|
+
confirm,
|
|
464
|
+
"Forget",
|
|
465
|
+
() =>
|
|
466
|
+
api.memoryForget(
|
|
467
|
+
{ id: ctx.id, spec },
|
|
468
|
+
{ factId, reason: reason.input.value.trim(), confirm: true },
|
|
469
|
+
),
|
|
470
|
+
(res) => {
|
|
471
|
+
if (res.ok && res.body?.ok !== false) void factsPanel(host, ctx, reload, spec);
|
|
472
|
+
},
|
|
473
|
+
);
|
|
474
|
+
});
|
|
475
|
+
wrap.appendChild(
|
|
476
|
+
el("div", { class: "editor-actions" }, [
|
|
477
|
+
reason.node,
|
|
478
|
+
confirm,
|
|
479
|
+
el("button", {
|
|
480
|
+
class: "btn btn-ghost",
|
|
481
|
+
type: "button",
|
|
482
|
+
text: "Cancel",
|
|
483
|
+
onClick: () => {
|
|
484
|
+
clear(wrap).appendChild(open);
|
|
485
|
+
},
|
|
486
|
+
}),
|
|
487
|
+
el("span", {
|
|
488
|
+
class: "muted gated-why",
|
|
489
|
+
text: "this appends a tombstone — the original line stays on disk and stays auditable",
|
|
490
|
+
}),
|
|
491
|
+
]),
|
|
492
|
+
);
|
|
493
|
+
});
|
|
494
|
+
wrap.appendChild(open);
|
|
495
|
+
return wrap;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** Sweep: dry-run first, the real pass as a second, separate gesture. */
|
|
499
|
+
function sweepCard(ctx, spec, host, reload) {
|
|
500
|
+
const body = el("div");
|
|
501
|
+
const plan = el("button", { class: "btn", type: "button", text: "Preview expiry sweep" });
|
|
502
|
+
plan.addEventListener("click", () => {
|
|
503
|
+
void run(
|
|
504
|
+
plan,
|
|
505
|
+
"Preview",
|
|
506
|
+
() => api.memorySweep({ id: ctx.id, spec }, { dryRun: true }),
|
|
507
|
+
(res) => {
|
|
508
|
+
clear(body);
|
|
509
|
+
if (!res.ok) return;
|
|
510
|
+
const rows = Array.isArray(res.body?.plan) ? res.body.plan : [];
|
|
511
|
+
body.appendChild(el("p", { class: "muted", text: String(res.body?.note ?? "") }));
|
|
512
|
+
if (rows.length === 0) return;
|
|
513
|
+
body.appendChild(
|
|
514
|
+
el(
|
|
515
|
+
"ul",
|
|
516
|
+
{ class: "fact-list" },
|
|
517
|
+
rows.map((r) =>
|
|
518
|
+
el("li", { class: "fact" }, [
|
|
519
|
+
el("div", { text: String(r.text ?? "") }),
|
|
520
|
+
el("div", { class: "fact-meta" }, [
|
|
521
|
+
el("span", { class: "mono muted", text: String(r.id ?? "") }),
|
|
522
|
+
]),
|
|
523
|
+
]),
|
|
524
|
+
),
|
|
525
|
+
),
|
|
526
|
+
);
|
|
527
|
+
const real = el("button", {
|
|
528
|
+
class: "btn btn-danger",
|
|
529
|
+
type: "button",
|
|
530
|
+
text: "Run the sweep",
|
|
531
|
+
});
|
|
532
|
+
real.addEventListener("click", () => {
|
|
533
|
+
void run(
|
|
534
|
+
real,
|
|
535
|
+
"Sweep",
|
|
536
|
+
() => api.memorySweep({ id: ctx.id, spec }, { dryRun: false }),
|
|
537
|
+
() => void factsPanel(host, ctx, reload, spec),
|
|
538
|
+
);
|
|
539
|
+
});
|
|
540
|
+
body.appendChild(
|
|
541
|
+
el("div", { class: "editor-actions" }, [
|
|
542
|
+
real,
|
|
543
|
+
el("span", {
|
|
544
|
+
class: "muted gated-why",
|
|
545
|
+
text: "the sweep appends expired tombstones — it never rewrites or compacts the file",
|
|
546
|
+
}),
|
|
547
|
+
]),
|
|
548
|
+
);
|
|
549
|
+
},
|
|
550
|
+
);
|
|
551
|
+
});
|
|
552
|
+
return card("Expiry sweep", [
|
|
553
|
+
el("p", {
|
|
554
|
+
class: "muted",
|
|
555
|
+
text: "TTL expiry is the store's own pass; the manager shows the plan first.",
|
|
556
|
+
}),
|
|
557
|
+
plan,
|
|
558
|
+
body,
|
|
559
|
+
]);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** The recall playground: exactly what the agent would recall, with scores. */
|
|
563
|
+
function recallCard(ctx) {
|
|
564
|
+
const results = el("div");
|
|
565
|
+
const query = field("query", { placeholder: "what would the agent recall for…" });
|
|
566
|
+
const go = el("button", { class: "btn btn-primary", type: "button", text: "Recall" });
|
|
567
|
+
go.addEventListener("click", () => {
|
|
568
|
+
if (query.input.value.trim() === "") return;
|
|
569
|
+
void run(
|
|
570
|
+
go,
|
|
571
|
+
"Recall",
|
|
572
|
+
() => api.memoryRecall({ id: ctx.id }, { query: query.input.value, k: 5 }),
|
|
573
|
+
(res) => {
|
|
574
|
+
clear(results);
|
|
575
|
+
if (!res.ok) return;
|
|
576
|
+
const hits = Array.isArray(res.body?.results) ? res.body.results : [];
|
|
577
|
+
results.appendChild(el("p", { class: "muted", text: String(res.body?.note ?? "") }));
|
|
578
|
+
if (hits.length === 0) {
|
|
579
|
+
results.appendChild(el("p", { class: "muted", text: "nothing would be recalled" }));
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
results.appendChild(
|
|
583
|
+
table(
|
|
584
|
+
["Score", "Fact", "Tags"],
|
|
585
|
+
hits.map((h) =>
|
|
586
|
+
el("tr", null, [
|
|
587
|
+
el("td", { class: "num", text: (Number(h.score) || 0).toFixed(3) }),
|
|
588
|
+
el("td", { text: String(h.text ?? "") }),
|
|
589
|
+
el("td", {
|
|
590
|
+
text: (Array.isArray(h.tags) ? h.tags : []).join(", "),
|
|
591
|
+
}),
|
|
592
|
+
]),
|
|
593
|
+
),
|
|
594
|
+
),
|
|
595
|
+
);
|
|
596
|
+
},
|
|
597
|
+
);
|
|
598
|
+
});
|
|
599
|
+
return card("Recall playground", [
|
|
600
|
+
el("div", { class: "editor-actions" }, [query.node, go]),
|
|
601
|
+
el("p", {
|
|
602
|
+
class: "muted",
|
|
603
|
+
text: "a read: recall touches no counter and no file",
|
|
604
|
+
}),
|
|
605
|
+
results,
|
|
606
|
+
]);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** The v1 → v2 fact-store backfill, dry-run first and typed-confirm to run. */
|
|
610
|
+
function migrateCard(ctx) {
|
|
611
|
+
const out = el("div");
|
|
612
|
+
const dry = el("button", { class: "btn", type: "button", text: "Preview migration" });
|
|
613
|
+
const name = field("type the spec name to run it", { placeholder: "spec name" });
|
|
614
|
+
const real = el("button", { class: "btn btn-danger", type: "button", text: "Migrate" });
|
|
615
|
+
dry.addEventListener("click", () => {
|
|
616
|
+
void run(
|
|
617
|
+
dry,
|
|
618
|
+
"Preview",
|
|
619
|
+
() => api.memoryMigrate({ id: ctx.id }, { dryRun: true }),
|
|
620
|
+
(res) => {
|
|
621
|
+
clear(out);
|
|
622
|
+
if (!res.ok) return;
|
|
623
|
+
out.appendChild(el("p", { class: "muted", text: String(res.body?.note ?? "") }));
|
|
624
|
+
const fleet = Array.isArray(res.body?.fleet) ? res.body.fleet : [];
|
|
625
|
+
out.appendChild(
|
|
626
|
+
table(
|
|
627
|
+
["Harness", "Memory schema", "State"],
|
|
628
|
+
fleet.map((row) =>
|
|
629
|
+
el("tr", null, [
|
|
630
|
+
el("td", { class: "mono", text: String(row.specName ?? row.id ?? "") }),
|
|
631
|
+
el("td", {
|
|
632
|
+
class: "num",
|
|
633
|
+
text: row.schemaVersion === null ? "—" : String(row.schemaVersion),
|
|
634
|
+
}),
|
|
635
|
+
el("td", null, [
|
|
636
|
+
row.behind === true ? dot("warn", "behind") : dot("ok", "current"),
|
|
637
|
+
]),
|
|
638
|
+
]),
|
|
639
|
+
),
|
|
640
|
+
),
|
|
641
|
+
);
|
|
642
|
+
},
|
|
643
|
+
);
|
|
644
|
+
});
|
|
645
|
+
real.addEventListener("click", () => {
|
|
646
|
+
void run(real, "Migrate", () =>
|
|
647
|
+
api.memoryMigrate({ id: ctx.id }, { dryRun: false, confirmName: name.input.value.trim() }),
|
|
648
|
+
);
|
|
649
|
+
});
|
|
650
|
+
return card("Schema migration", [
|
|
651
|
+
el("p", {
|
|
652
|
+
class: "muted",
|
|
653
|
+
text: "`crewhaus migrate memories` — the v1 → v2 backfill, through the job queue.",
|
|
654
|
+
}),
|
|
655
|
+
el("div", { class: "editor-actions" }, [dry, name.node, real]),
|
|
656
|
+
out,
|
|
657
|
+
]);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// ---------------------------------------------------------------------------
|
|
661
|
+
// continuity
|
|
662
|
+
// ---------------------------------------------------------------------------
|
|
663
|
+
|
|
664
|
+
async function continuityPanel(host, ctx, reload) {
|
|
665
|
+
clear(host).appendChild(skeleton(6));
|
|
666
|
+
const [data, trash] = await Promise.all([
|
|
667
|
+
load(host, () => api.continuity({ id: ctx.id })),
|
|
668
|
+
load(host, () => api.continuityTrash({ id: ctx.id })),
|
|
669
|
+
]);
|
|
670
|
+
clear(host);
|
|
671
|
+
if (data === null) return;
|
|
672
|
+
|
|
673
|
+
if (data.present !== true) {
|
|
674
|
+
host.appendChild(card("Continuity", empty(data, "No continuity state yet")));
|
|
675
|
+
} else {
|
|
676
|
+
const focus = data.focus && typeof data.focus === "object" ? data.focus : {};
|
|
677
|
+
const children = [
|
|
678
|
+
el("div", { class: "filter-bar" }, [
|
|
679
|
+
focus.managed === true
|
|
680
|
+
? dot("ok", "managed focus.md")
|
|
681
|
+
: dot("off", "unmanaged focus.md — a human's file, never overwritten"),
|
|
682
|
+
el("span", { class: "chip", text: String(data.layout ?? "") }),
|
|
683
|
+
typeof data.degraded === "string" && data.degraded !== ""
|
|
684
|
+
? el("span", { class: "chip chip-warn", title: data.degraded, text: "degraded read" })
|
|
685
|
+
: null,
|
|
686
|
+
]),
|
|
687
|
+
];
|
|
688
|
+
if (typeof focus.body === "string" && focus.body !== "") {
|
|
689
|
+
children.push(el("h4", { class: "sub-title", text: "Focus" }));
|
|
690
|
+
children.push(mdBlocks(focus.body));
|
|
691
|
+
}
|
|
692
|
+
const reqs = Array.isArray(focus.requirements) ? focus.requirements : [];
|
|
693
|
+
if (reqs.length > 0) {
|
|
694
|
+
children.push(el("h4", { class: "sub-title", text: "Requirements ledger" }));
|
|
695
|
+
children.push(
|
|
696
|
+
table(
|
|
697
|
+
["REQ", "Status", "The user's words", "Source"],
|
|
698
|
+
reqs.map((r) =>
|
|
699
|
+
el("tr", null, [
|
|
700
|
+
el("td", { class: "mono", text: String(r.id ?? "") }),
|
|
701
|
+
el("td", null, [
|
|
702
|
+
dot(
|
|
703
|
+
r.status === "confirmed" ? "ok" : r.status === "dropped" ? "off" : "warn",
|
|
704
|
+
String(r.status ?? ""),
|
|
705
|
+
),
|
|
706
|
+
]),
|
|
707
|
+
el("td", { text: String(r.text ?? "") }),
|
|
708
|
+
el("td", null, [
|
|
709
|
+
r.sessionId
|
|
710
|
+
? sessionLink(ctx, String(r.sessionId), `turn ${String(r.turn ?? "?")}`)
|
|
711
|
+
: el("span", { text: "—" }),
|
|
712
|
+
]),
|
|
713
|
+
]),
|
|
714
|
+
),
|
|
715
|
+
),
|
|
716
|
+
);
|
|
717
|
+
if (focus.ledgerTruncated === true) {
|
|
718
|
+
children.push(
|
|
719
|
+
el("p", {
|
|
720
|
+
class: "muted",
|
|
721
|
+
text: "the ledger evicted its oldest entries to stay under its cap",
|
|
722
|
+
}),
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
host.appendChild(card("Continuity", children));
|
|
727
|
+
host.appendChild(plansCard(data, ctx));
|
|
728
|
+
host.appendChild(goalsCard(data));
|
|
729
|
+
if (data.handoff && typeof data.handoff === "object") {
|
|
730
|
+
host.appendChild(card("Handoff", mdBlocks(String(data.handoff.text ?? ""))));
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
host.appendChild(trashCard(trash, ctx, host, reload));
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function plansCard(data, ctx) {
|
|
738
|
+
const plans = Array.isArray(data.plans) ? data.plans : [];
|
|
739
|
+
if (plans.length === 0) {
|
|
740
|
+
return card("Plans", emptyState("No plans yet", "crewhaus run (then /plan)"));
|
|
741
|
+
}
|
|
742
|
+
return card(
|
|
743
|
+
"Plans",
|
|
744
|
+
plans.map((plan) =>
|
|
745
|
+
collapsible(
|
|
746
|
+
[
|
|
747
|
+
el("span", { class: "mono", text: String(plan.id ?? "") }),
|
|
748
|
+
el("span", { text: ` ${String(plan.title ?? "")}` }),
|
|
749
|
+
],
|
|
750
|
+
[
|
|
751
|
+
typeof plan.text === "string"
|
|
752
|
+
? el("pre", { class: "prose-pre", text: plan.text })
|
|
753
|
+
: el(
|
|
754
|
+
"ol",
|
|
755
|
+
{ class: "check-list" },
|
|
756
|
+
(Array.isArray(plan.steps) ? plan.steps : []).map((step) =>
|
|
757
|
+
el("li", null, [
|
|
758
|
+
dot(ladderTone(step.status), String(step.status ?? "")),
|
|
759
|
+
el("span", { text: ` ${String(step.text ?? "")}` }),
|
|
760
|
+
...(Array.isArray(step.proofs) ? step.proofs : []).map((proof) =>
|
|
761
|
+
el("a", {
|
|
762
|
+
class: "chip chip-tool",
|
|
763
|
+
href: hrefHarness(ctx.id, "sessions", String(proof.sessionId ?? "")),
|
|
764
|
+
title: `${String(proof.toolName ?? "tool")} — ${String(proof.resultDigest ?? "")}`,
|
|
765
|
+
text: String(proof.toolUseId ?? ""),
|
|
766
|
+
}),
|
|
767
|
+
),
|
|
768
|
+
]),
|
|
769
|
+
),
|
|
770
|
+
),
|
|
771
|
+
],
|
|
772
|
+
true,
|
|
773
|
+
),
|
|
774
|
+
),
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function ladderTone(status) {
|
|
779
|
+
if (status === "proven") return "ok";
|
|
780
|
+
if (status === "claimed") return "warn";
|
|
781
|
+
if (status === "in_progress") return "unknown";
|
|
782
|
+
return "off";
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function goalsCard(data) {
|
|
786
|
+
const goals = Array.isArray(data.goals) ? data.goals : [];
|
|
787
|
+
if (goals.length === 0)
|
|
788
|
+
return card("Goals", emptyState("No goals yet", "crewhaus run (then /goal)"));
|
|
789
|
+
return card(
|
|
790
|
+
"Goals",
|
|
791
|
+
table(
|
|
792
|
+
["Goal", "Status", "Progress"],
|
|
793
|
+
goals.map((g) =>
|
|
794
|
+
el("tr", null, [
|
|
795
|
+
el("td", { text: String(g.title ?? "") }),
|
|
796
|
+
el("td", null, [dot(ladderTone(g.status), String(g.status ?? ""))]),
|
|
797
|
+
el("td", {
|
|
798
|
+
text:
|
|
799
|
+
typeof g.current === "number" && typeof g.target === "number"
|
|
800
|
+
? `${g.current} / ${g.target}${g.unit ? ` ${g.unit}` : ""}`
|
|
801
|
+
: "—",
|
|
802
|
+
}),
|
|
803
|
+
]),
|
|
804
|
+
),
|
|
805
|
+
),
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/** The trash browser — the screen that makes "clear never deletes" visible. */
|
|
810
|
+
function trashCard(trash, ctx, host, reload) {
|
|
811
|
+
const snapshots = Array.isArray(trash?.snapshots) ? trash.snapshots : [];
|
|
812
|
+
if (snapshots.length === 0) {
|
|
813
|
+
return card("Trash", empty(trash, "Nothing has been cleared yet"));
|
|
814
|
+
}
|
|
815
|
+
const rows = snapshots.map((snap) => {
|
|
816
|
+
const restore = el("button", {
|
|
817
|
+
class: "btn",
|
|
818
|
+
type: "button",
|
|
819
|
+
text: "Restore",
|
|
820
|
+
disabled: snap.restorable === false || null,
|
|
821
|
+
title:
|
|
822
|
+
snap.restorable === false
|
|
823
|
+
? `restoring would overwrite ${(snap.blockedBy ?? []).join(", ")} — move those aside first`
|
|
824
|
+
: "move every file in this snapshot back where it came from",
|
|
825
|
+
});
|
|
826
|
+
if (snap.restorable !== false) {
|
|
827
|
+
restore.addEventListener("click", () => {
|
|
828
|
+
void run(
|
|
829
|
+
restore,
|
|
830
|
+
"Restore",
|
|
831
|
+
() => api.continuityRestore({ id: ctx.id }, { stamp: String(snap.ts), confirm: true }),
|
|
832
|
+
() => void continuityPanel(host, ctx, reload),
|
|
833
|
+
);
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
return el("tr", null, [
|
|
837
|
+
el("td", { class: "mono", text: String(snap.ts ?? "") }),
|
|
838
|
+
el("td", {
|
|
839
|
+
text: typeof snap.at === "string" ? fmtRelativeTime(snap.at, Date.now()) : "—",
|
|
840
|
+
}),
|
|
841
|
+
el("td", { class: "num", text: String((snap.files ?? []).length) }),
|
|
842
|
+
el("td", null, [
|
|
843
|
+
snap.restorable === false ? dot("warn", "blocked") : dot("ok", "restorable"),
|
|
844
|
+
]),
|
|
845
|
+
el("td", null, [restore]),
|
|
846
|
+
]);
|
|
847
|
+
});
|
|
848
|
+
return card("Trash", [
|
|
849
|
+
el("p", {
|
|
850
|
+
class: "muted",
|
|
851
|
+
text: `clearing continuity moves files here — restorable by timestamp, purged after ${String(trash?.purgeAfterDays ?? 7)} days`,
|
|
852
|
+
}),
|
|
853
|
+
table(["Snapshot", "Age", "Files", "State", ""], rows),
|
|
854
|
+
]);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// ---------------------------------------------------------------------------
|
|
858
|
+
// wiki
|
|
859
|
+
// ---------------------------------------------------------------------------
|
|
860
|
+
|
|
861
|
+
async function wikiPanel(host, ctx, reload) {
|
|
862
|
+
clear(host).appendChild(skeleton(6));
|
|
863
|
+
const state = { tags: "", status: "all", q: "" };
|
|
864
|
+
await drawReflect(host, ctx, reload, state);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
async function drawReflect(host, ctx, reload, state) {
|
|
868
|
+
const query = [
|
|
869
|
+
state.tags ? `tags=${encodeURIComponent(state.tags)}` : "",
|
|
870
|
+
state.status && state.status !== "all" ? `status=${encodeURIComponent(state.status)}` : "",
|
|
871
|
+
state.q ? `q=${encodeURIComponent(state.q)}` : "",
|
|
872
|
+
]
|
|
873
|
+
.filter((p) => p !== "")
|
|
874
|
+
.join("&");
|
|
875
|
+
clear(host).appendChild(skeleton(4));
|
|
876
|
+
const data = await load(host, () =>
|
|
877
|
+
api.wikiReflect({ id: ctx.id }, undefined, query === "" ? "" : `?${query}`),
|
|
878
|
+
);
|
|
879
|
+
clear(host);
|
|
880
|
+
if (data === null) return;
|
|
881
|
+
|
|
882
|
+
const filters = data.filters && typeof data.filters === "object" ? data.filters : {};
|
|
883
|
+
const tagInput = field("tags (comma separated)", { value: state.tags });
|
|
884
|
+
const qInput = field("search", { value: state.q });
|
|
885
|
+
const statusSelect = el(
|
|
886
|
+
"select",
|
|
887
|
+
{ class: "input" },
|
|
888
|
+
["all", "draft", "review", "published", "archived"].map((s) =>
|
|
889
|
+
el("option", { value: s, text: s, selected: s === state.status ? true : null }),
|
|
890
|
+
),
|
|
891
|
+
);
|
|
892
|
+
const apply = el("button", { class: "btn", type: "button", text: "Filter" });
|
|
893
|
+
apply.addEventListener("click", () => {
|
|
894
|
+
state.tags = tagInput.input.value.trim();
|
|
895
|
+
state.q = qInput.input.value.trim();
|
|
896
|
+
state.status = statusSelect.value;
|
|
897
|
+
void drawReflect(host, ctx, reload, state);
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
host.appendChild(
|
|
901
|
+
card("REFLECT queue", [
|
|
902
|
+
el("p", {
|
|
903
|
+
class: "muted",
|
|
904
|
+
text: `stale-first over ${String(data.total ?? 0)} article(s); the threshold comes from ${String(data.thresholdSource ?? "the default")}`,
|
|
905
|
+
}),
|
|
906
|
+
el("div", { class: "filter-bar" }, [
|
|
907
|
+
tagInput.node,
|
|
908
|
+
qInput.node,
|
|
909
|
+
statusSelect,
|
|
910
|
+
apply,
|
|
911
|
+
data.sourcesGate === true
|
|
912
|
+
? el("span", {
|
|
913
|
+
class: "chip",
|
|
914
|
+
title:
|
|
915
|
+
"a LOCAL rule (memory.wiki.requireSources) — the Thredz backend does not apply it",
|
|
916
|
+
text: "## Sources required (local)",
|
|
917
|
+
})
|
|
918
|
+
: null,
|
|
919
|
+
dot(Number(data.stale ?? 0) > 0 ? "warn" : "ok", `${fmtCount(data.stale ?? 0)} stale`),
|
|
920
|
+
]),
|
|
921
|
+
(Array.isArray(data.articles) ? data.articles : []).length === 0
|
|
922
|
+
? empty(data, "No wiki articles yet")
|
|
923
|
+
: reflectTable(data, ctx, host, reload, state),
|
|
924
|
+
...(Array.isArray(filters.available) && filters.available.length > 0
|
|
925
|
+
? [
|
|
926
|
+
el("p", {
|
|
927
|
+
class: "muted",
|
|
928
|
+
text: `tags in this wiki: ${filters.available.join(", ")}`,
|
|
929
|
+
}),
|
|
930
|
+
]
|
|
931
|
+
: []),
|
|
932
|
+
]),
|
|
933
|
+
);
|
|
934
|
+
host.appendChild(editorCard(ctx, host, reload, state, null));
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function reflectTable(data, ctx, host, reload, state) {
|
|
938
|
+
const rows = (Array.isArray(data.articles) ? data.articles : []).map((row) => {
|
|
939
|
+
const actions = el("div", { class: "cell-actions" }, [
|
|
940
|
+
el("button", {
|
|
941
|
+
class: "btn btn-ghost",
|
|
942
|
+
type: "button",
|
|
943
|
+
text: "Edit",
|
|
944
|
+
onClick: () => {
|
|
945
|
+
clear(host);
|
|
946
|
+
host.appendChild(editorCard(ctx, host, reload, state, row));
|
|
947
|
+
},
|
|
948
|
+
}),
|
|
949
|
+
el("button", {
|
|
950
|
+
class: "btn btn-ghost",
|
|
951
|
+
type: "button",
|
|
952
|
+
text: "History",
|
|
953
|
+
onClick: () => void versionsPanel(host, ctx, reload, state, String(row.slug)),
|
|
954
|
+
}),
|
|
955
|
+
el("button", {
|
|
956
|
+
class: "btn btn-ghost",
|
|
957
|
+
type: "button",
|
|
958
|
+
text: "Links",
|
|
959
|
+
onClick: () => void linksPanel(host, ctx, reload, state, String(row.slug)),
|
|
960
|
+
}),
|
|
961
|
+
archiveButton(ctx, host, reload, state, row),
|
|
962
|
+
]);
|
|
963
|
+
return el("tr", null, [
|
|
964
|
+
el("td", null, [
|
|
965
|
+
el("a", {
|
|
966
|
+
class: "name-link",
|
|
967
|
+
href: hrefHarness(ctx.id, "memory", "wiki", String(row.slug)),
|
|
968
|
+
text: String(row.title ?? row.slug),
|
|
969
|
+
}),
|
|
970
|
+
row.malformed === true
|
|
971
|
+
? el("span", {
|
|
972
|
+
class: "chip chip-warn",
|
|
973
|
+
title: "no frontmatter — rendered as a document",
|
|
974
|
+
text: "no frontmatter",
|
|
975
|
+
})
|
|
976
|
+
: null,
|
|
977
|
+
]),
|
|
978
|
+
el("td", { class: "num", text: `v${String(row.version ?? 1)}` }),
|
|
979
|
+
el("td", null, [dot(row.status === "archived" ? "off" : "ok", String(row.status ?? ""))]),
|
|
980
|
+
el("td", null, [
|
|
981
|
+
row.verified === true ? dot("ok", "verified") : dot("unknown", "unverified"),
|
|
982
|
+
el("span", { class: "muted", text: ` ${fmtPct(Number(row.confidence ?? 0))}` }),
|
|
983
|
+
]),
|
|
984
|
+
el("td", null, [
|
|
985
|
+
row.hasSources === true
|
|
986
|
+
? el("span", { class: "chip", text: "## Sources" })
|
|
987
|
+
: el("span", { class: "muted", text: "—" }),
|
|
988
|
+
]),
|
|
989
|
+
el("td", {
|
|
990
|
+
class: row.stale === true ? "chip chip-warn" : "muted",
|
|
991
|
+
text: staleLabel(row) ?? "—",
|
|
992
|
+
}),
|
|
993
|
+
el("td", null, [actions]),
|
|
994
|
+
]);
|
|
995
|
+
});
|
|
996
|
+
return table(["Article", "Version", "Status", "Signals", "Sources", "Freshness", ""], rows);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function archiveButton(ctx, host, reload, state, row) {
|
|
1000
|
+
const archived = row.status === "archived";
|
|
1001
|
+
const btn = el("button", {
|
|
1002
|
+
class: "btn btn-ghost",
|
|
1003
|
+
type: "button",
|
|
1004
|
+
text: archived ? "Unarchive" : "Archive",
|
|
1005
|
+
title: archived
|
|
1006
|
+
? "restore this article to the live list"
|
|
1007
|
+
: "archiving hides it from the live list — every version stays on disk, and this is reversible",
|
|
1008
|
+
});
|
|
1009
|
+
btn.addEventListener("click", () => {
|
|
1010
|
+
void run(
|
|
1011
|
+
btn,
|
|
1012
|
+
archived ? "Unarchive" : "Archive",
|
|
1013
|
+
() =>
|
|
1014
|
+
api.wikiArchive(
|
|
1015
|
+
{ id: ctx.id, slug: String(row.slug) },
|
|
1016
|
+
{ archived: !archived, confirm: true },
|
|
1017
|
+
),
|
|
1018
|
+
() => void drawReflect(host, ctx, reload, state),
|
|
1019
|
+
);
|
|
1020
|
+
});
|
|
1021
|
+
return btn;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* The editor. Every save carries `expectedVersion`; a stale answer renders
|
|
1026
|
+
* the version that moved plus a retry that re-sends against it — the
|
|
1027
|
+
* re-read-then-reapply contract, identical for the local and Thredz
|
|
1028
|
+
* backends.
|
|
1029
|
+
*/
|
|
1030
|
+
function editorCard(ctx, host, reload, state, row) {
|
|
1031
|
+
const slugField = field("slug", {
|
|
1032
|
+
value: row ? String(row.slug) : "",
|
|
1033
|
+
placeholder: "kebab-case-slug",
|
|
1034
|
+
});
|
|
1035
|
+
const titleField = field("title", { value: row ? String(row.title ?? "") : "" });
|
|
1036
|
+
const area = el("textarea", { class: "input", rows: "12", spellcheck: "false" });
|
|
1037
|
+
const versionLabel = el("span", { class: "muted" });
|
|
1038
|
+
const guard = el("div");
|
|
1039
|
+
const status = el("div");
|
|
1040
|
+
const expected = { version: row ? Number(row.version ?? 0) : 0 };
|
|
1041
|
+
|
|
1042
|
+
const setVersion = (v) => {
|
|
1043
|
+
expected.version = typeof v === "number" ? v : 0;
|
|
1044
|
+
versionLabel.textContent =
|
|
1045
|
+
expected.version > 0 ? `saving against version ${expected.version}` : "new article";
|
|
1046
|
+
};
|
|
1047
|
+
setVersion(row ? Number(row.version ?? 0) : 0);
|
|
1048
|
+
|
|
1049
|
+
const save = el("button", { class: "btn btn-primary", type: "button", text: "Save" });
|
|
1050
|
+
|
|
1051
|
+
if (row) {
|
|
1052
|
+
// Load the live body to edit, and re-read the CURRENT version rather than
|
|
1053
|
+
// trusting the row the queue was painted from — the article may have
|
|
1054
|
+
// moved while the operator was reading the list.
|
|
1055
|
+
void (async () => {
|
|
1056
|
+
let article = null;
|
|
1057
|
+
try {
|
|
1058
|
+
article = await api.wikiArticle(ctx.id, String(row.slug));
|
|
1059
|
+
} catch {
|
|
1060
|
+
article = null;
|
|
1061
|
+
}
|
|
1062
|
+
if (article !== null && typeof article.body === "string") area.value = article.body;
|
|
1063
|
+
const versions = await api.wikiVersions({ id: ctx.id, slug: String(row.slug) });
|
|
1064
|
+
if (versions.ok && typeof versions.body?.currentVersion === "number") {
|
|
1065
|
+
setVersion(versions.body.currentVersion);
|
|
1066
|
+
}
|
|
1067
|
+
// Served text is MASKED. Saving a masked span back would persist the
|
|
1068
|
+
// mask over whatever it hid, which is silent data loss — so this one
|
|
1069
|
+
// article is read-only here and says why.
|
|
1070
|
+
if (area.value.includes(MASK_MARK)) {
|
|
1071
|
+
save.disabled = true;
|
|
1072
|
+
clear(guard).appendChild(
|
|
1073
|
+
el("div", { class: "gated" }, [
|
|
1074
|
+
dot("warn", "read-only here"),
|
|
1075
|
+
el("span", {
|
|
1076
|
+
class: "muted gated-why",
|
|
1077
|
+
text: "this body contains a masked credential-shaped span; saving from the console would persist the mask — edit it with `crewhaus wiki write`",
|
|
1078
|
+
}),
|
|
1079
|
+
]),
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
})();
|
|
1083
|
+
}
|
|
1084
|
+
save.addEventListener("click", () => {
|
|
1085
|
+
const slug = slugField.input.value.trim();
|
|
1086
|
+
if (slug === "") {
|
|
1087
|
+
toast("a slug is required");
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
void run(
|
|
1091
|
+
save,
|
|
1092
|
+
"Save",
|
|
1093
|
+
() =>
|
|
1094
|
+
api.wikiWrite(
|
|
1095
|
+
{ id: ctx.id, slug },
|
|
1096
|
+
{
|
|
1097
|
+
body: area.value,
|
|
1098
|
+
title: titleField.input.value.trim(),
|
|
1099
|
+
...(expected.version > 0 ? { expectedVersion: expected.version } : {}),
|
|
1100
|
+
},
|
|
1101
|
+
),
|
|
1102
|
+
(res) => {
|
|
1103
|
+
clear(status);
|
|
1104
|
+
if (!res.ok) return;
|
|
1105
|
+
const outcome = writeOutcome(res.body);
|
|
1106
|
+
if (outcome.kind === "ok") {
|
|
1107
|
+
status.appendChild(
|
|
1108
|
+
el("p", { class: "muted" }, [
|
|
1109
|
+
dot("ok", "saved"),
|
|
1110
|
+
el("span", { text: ` ${outcome.message}` }),
|
|
1111
|
+
]),
|
|
1112
|
+
);
|
|
1113
|
+
void drawReflect(host, ctx, reload, state);
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
status.appendChild(
|
|
1117
|
+
el("div", { class: "refusal-item" }, [
|
|
1118
|
+
el("div", { class: "refusal-head" }, [
|
|
1119
|
+
dot(
|
|
1120
|
+
outcome.kind === "stale" ? "warn" : "bad",
|
|
1121
|
+
outcome.kind === "stale" ? "version moved" : "refused",
|
|
1122
|
+
),
|
|
1123
|
+
]),
|
|
1124
|
+
el("p", { class: "muted", text: outcome.message }),
|
|
1125
|
+
]),
|
|
1126
|
+
);
|
|
1127
|
+
if (outcome.kind === "stale") {
|
|
1128
|
+
const diff = String(res.body?.diff ?? "");
|
|
1129
|
+
if (diff !== "") status.appendChild(el("pre", { class: "rawjson", text: diff }));
|
|
1130
|
+
const retry = el("button", {
|
|
1131
|
+
class: "btn",
|
|
1132
|
+
type: "button",
|
|
1133
|
+
text: `Retry against version ${String(outcome.retryVersion ?? "?")}`,
|
|
1134
|
+
});
|
|
1135
|
+
retry.addEventListener("click", () => {
|
|
1136
|
+
setVersion(outcome.retryVersion);
|
|
1137
|
+
save.click();
|
|
1138
|
+
});
|
|
1139
|
+
const takeTheirs = el("button", {
|
|
1140
|
+
class: "btn btn-ghost",
|
|
1141
|
+
type: "button",
|
|
1142
|
+
text: "Load what is on disk now",
|
|
1143
|
+
});
|
|
1144
|
+
takeTheirs.addEventListener("click", () => {
|
|
1145
|
+
area.value = outcome.currentBody ?? "";
|
|
1146
|
+
setVersion(outcome.retryVersion);
|
|
1147
|
+
});
|
|
1148
|
+
status.appendChild(el("div", { class: "editor-actions" }, [takeTheirs, retry]));
|
|
1149
|
+
}
|
|
1150
|
+
},
|
|
1151
|
+
);
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
const signals = signalsControl(ctx, row, host, reload, state);
|
|
1155
|
+
return card("Editor", [
|
|
1156
|
+
row
|
|
1157
|
+
? el("div", { class: "crumb-line" }, [
|
|
1158
|
+
el("button", {
|
|
1159
|
+
class: "btn btn-ghost",
|
|
1160
|
+
type: "button",
|
|
1161
|
+
text: "← back to the queue",
|
|
1162
|
+
onClick: () => void drawReflect(host, ctx, reload, state),
|
|
1163
|
+
}),
|
|
1164
|
+
el("span", { class: "mono muted", text: String(row.slug) }),
|
|
1165
|
+
])
|
|
1166
|
+
: null,
|
|
1167
|
+
el("div", { class: "editor-actions" }, [slugField.node, titleField.node, versionLabel]),
|
|
1168
|
+
area,
|
|
1169
|
+
guard,
|
|
1170
|
+
el("div", { class: "editor-actions" }, [
|
|
1171
|
+
save,
|
|
1172
|
+
el("span", {
|
|
1173
|
+
class: "muted gated-why",
|
|
1174
|
+
text: "every save carries expectedVersion; the outgoing version is frozen under versions/ before the new one lands",
|
|
1175
|
+
}),
|
|
1176
|
+
]),
|
|
1177
|
+
status,
|
|
1178
|
+
signals,
|
|
1179
|
+
]);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/** verified / confidence — metadata only, never a way to rewrite a body. */
|
|
1183
|
+
function signalsControl(ctx, row, host, reload, state) {
|
|
1184
|
+
if (!row) return null;
|
|
1185
|
+
const verified = el("button", {
|
|
1186
|
+
class: "btn btn-ghost",
|
|
1187
|
+
type: "button",
|
|
1188
|
+
text: row.verified === true ? "Mark unverified" : "Mark verified",
|
|
1189
|
+
});
|
|
1190
|
+
verified.addEventListener("click", () => {
|
|
1191
|
+
void run(
|
|
1192
|
+
verified,
|
|
1193
|
+
"Signals",
|
|
1194
|
+
() =>
|
|
1195
|
+
api.wikiSignals(
|
|
1196
|
+
{ id: ctx.id, slug: String(row.slug) },
|
|
1197
|
+
{ verified: row.verified !== true },
|
|
1198
|
+
),
|
|
1199
|
+
() => void drawReflect(host, ctx, reload, state),
|
|
1200
|
+
);
|
|
1201
|
+
});
|
|
1202
|
+
return el("div", { class: "editor-actions" }, [
|
|
1203
|
+
verified,
|
|
1204
|
+
el("span", {
|
|
1205
|
+
class: "muted gated-why",
|
|
1206
|
+
text: "signals are metadata: no version bump, no snapshot, the body untouched",
|
|
1207
|
+
}),
|
|
1208
|
+
]);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
async function versionsPanel(host, ctx, reload, state, slug) {
|
|
1212
|
+
clear(host).appendChild(skeleton(4));
|
|
1213
|
+
const data = await load(host, () => api.wikiVersions({ id: ctx.id, slug }));
|
|
1214
|
+
clear(host);
|
|
1215
|
+
if (data === null) return;
|
|
1216
|
+
const back = el("button", {
|
|
1217
|
+
class: "btn btn-ghost",
|
|
1218
|
+
type: "button",
|
|
1219
|
+
text: "← back to the queue",
|
|
1220
|
+
onClick: () => void drawReflect(host, ctx, reload, state),
|
|
1221
|
+
});
|
|
1222
|
+
const detail = el("div");
|
|
1223
|
+
const versions = Array.isArray(data.versions) ? data.versions : [];
|
|
1224
|
+
host.appendChild(
|
|
1225
|
+
card(`History · ${slug}`, [
|
|
1226
|
+
el("div", { class: "crumb-line" }, [
|
|
1227
|
+
back,
|
|
1228
|
+
el("span", { class: "mono muted", text: `current v${String(data.currentVersion ?? "?")}` }),
|
|
1229
|
+
]),
|
|
1230
|
+
versions.length === 0
|
|
1231
|
+
? empty(data, "This article has no frozen versions yet")
|
|
1232
|
+
: table(
|
|
1233
|
+
["Version", "Author", "When", "Bytes", ""],
|
|
1234
|
+
versions.map((v) =>
|
|
1235
|
+
el("tr", null, [
|
|
1236
|
+
el("td", { class: "num", text: `v${String(v.version)}` }),
|
|
1237
|
+
el("td", null, [
|
|
1238
|
+
v.sessionId
|
|
1239
|
+
? sessionLink(ctx, String(v.sessionId), String(v.author ?? "agent"))
|
|
1240
|
+
: el("span", { text: String(v.author ?? "operator") }),
|
|
1241
|
+
]),
|
|
1242
|
+
el("td", {
|
|
1243
|
+
text:
|
|
1244
|
+
typeof v.updatedAt === "string"
|
|
1245
|
+
? fmtRelativeTime(v.updatedAt, Date.now())
|
|
1246
|
+
: "—",
|
|
1247
|
+
}),
|
|
1248
|
+
el("td", { class: "num", text: fmtCount(Number(v.bytes ?? 0)) }),
|
|
1249
|
+
el("td", null, [
|
|
1250
|
+
el("button", {
|
|
1251
|
+
class: "btn btn-ghost",
|
|
1252
|
+
type: "button",
|
|
1253
|
+
text: "Diff vs live",
|
|
1254
|
+
onClick: () => void showVersion(detail, ctx, slug, String(v.version)),
|
|
1255
|
+
}),
|
|
1256
|
+
]),
|
|
1257
|
+
]),
|
|
1258
|
+
),
|
|
1259
|
+
),
|
|
1260
|
+
detail,
|
|
1261
|
+
]),
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
async function showVersion(host, ctx, slug, version) {
|
|
1266
|
+
clear(host).appendChild(skeleton(3));
|
|
1267
|
+
const data = await load(host, () => api.wikiVersion({ id: ctx.id, slug, version }));
|
|
1268
|
+
clear(host);
|
|
1269
|
+
if (data === null) return;
|
|
1270
|
+
if (data.present !== true) {
|
|
1271
|
+
host.appendChild(empty(data, "No such version"));
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
host.appendChild(el("h4", { class: "sub-title", text: `v${version} → live` }));
|
|
1275
|
+
host.appendChild(
|
|
1276
|
+
el("pre", { class: "rawjson", text: String(data.diff ?? "") || "(no textual difference)" }),
|
|
1277
|
+
);
|
|
1278
|
+
host.appendChild(
|
|
1279
|
+
collapsible(
|
|
1280
|
+
[el("span", { class: "muted", text: `the v${version} body` })],
|
|
1281
|
+
[mdBlocks(String(data.body ?? ""))],
|
|
1282
|
+
),
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
async function linksPanel(host, ctx, reload, state, slug) {
|
|
1287
|
+
clear(host).appendChild(skeleton(4));
|
|
1288
|
+
const data = await load(host, () => api.wikiLinks({ id: ctx.id, slug }));
|
|
1289
|
+
clear(host);
|
|
1290
|
+
if (data === null) return;
|
|
1291
|
+
const links = Array.isArray(data.links) ? data.links : [];
|
|
1292
|
+
host.appendChild(
|
|
1293
|
+
card(`Links · ${slug}`, [
|
|
1294
|
+
el("div", { class: "crumb-line" }, [
|
|
1295
|
+
el("button", {
|
|
1296
|
+
class: "btn btn-ghost",
|
|
1297
|
+
type: "button",
|
|
1298
|
+
text: "← back to the queue",
|
|
1299
|
+
onClick: () => void drawReflect(host, ctx, reload, state),
|
|
1300
|
+
}),
|
|
1301
|
+
el("span", {
|
|
1302
|
+
class: "muted",
|
|
1303
|
+
text: `${String(data.outbound ?? 0)} out · ${String(data.backlinks ?? 0)} in`,
|
|
1304
|
+
}),
|
|
1305
|
+
data.indexStale === true
|
|
1306
|
+
? el("span", {
|
|
1307
|
+
class: "chip",
|
|
1308
|
+
title:
|
|
1309
|
+
"index.json is a rebuildable cache — this graph was rebuilt from the article bodies",
|
|
1310
|
+
text: "rebuilt from disk",
|
|
1311
|
+
})
|
|
1312
|
+
: null,
|
|
1313
|
+
]),
|
|
1314
|
+
links.length === 0
|
|
1315
|
+
? empty(data, "This article neither links out nor is linked to")
|
|
1316
|
+
: table(
|
|
1317
|
+
["Direction", "Article", "State"],
|
|
1318
|
+
links.map((link) =>
|
|
1319
|
+
el("tr", null, [
|
|
1320
|
+
el("td", { text: link.direction === "in" ? "backlink" : "outbound" }),
|
|
1321
|
+
el("td", null, [
|
|
1322
|
+
el("a", {
|
|
1323
|
+
class: "name-link",
|
|
1324
|
+
href: hrefHarness(ctx.id, "memory", "wiki", String(link.slug)),
|
|
1325
|
+
text: String(link.title ?? link.slug),
|
|
1326
|
+
}),
|
|
1327
|
+
]),
|
|
1328
|
+
el("td", null, [
|
|
1329
|
+
link.exists === true ? dot("ok", "written") : dot("warn", "not written yet"),
|
|
1330
|
+
]),
|
|
1331
|
+
]),
|
|
1332
|
+
),
|
|
1333
|
+
),
|
|
1334
|
+
]),
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// ---------------------------------------------------------------------------
|
|
1339
|
+
// dream
|
|
1340
|
+
// ---------------------------------------------------------------------------
|
|
1341
|
+
|
|
1342
|
+
async function dreamPanel(host, ctx) {
|
|
1343
|
+
clear(host).appendChild(skeleton(5));
|
|
1344
|
+
const data = await load(host, () => api.dreamScaffold({ id: ctx.id }));
|
|
1345
|
+
clear(host);
|
|
1346
|
+
if (data === null) return;
|
|
1347
|
+
const tone = dreamTone(data);
|
|
1348
|
+
const specs = Array.isArray(data.specs) ? data.specs : [];
|
|
1349
|
+
|
|
1350
|
+
host.appendChild(
|
|
1351
|
+
card("Dream", [
|
|
1352
|
+
el("div", { class: "filter-bar" }, [
|
|
1353
|
+
dot(tone.state, tone.label),
|
|
1354
|
+
data.modelPhase === true
|
|
1355
|
+
? el("span", {
|
|
1356
|
+
class: "chip",
|
|
1357
|
+
text: `model phase, budget ${fmtUsd(Number(data.budgetUsd ?? 0))}`,
|
|
1358
|
+
})
|
|
1359
|
+
: el("span", { class: "chip", text: "deterministic only" }),
|
|
1360
|
+
]),
|
|
1361
|
+
specs.length === 0
|
|
1362
|
+
? empty(data, "This harness has never consolidated")
|
|
1363
|
+
: table(
|
|
1364
|
+
["Spec", "Last run", "Outcome", "Next due", ""],
|
|
1365
|
+
specs.map((s) =>
|
|
1366
|
+
el("tr", null, [
|
|
1367
|
+
el("td", { class: "mono", text: String(s.specName ?? "") }),
|
|
1368
|
+
el("td", {
|
|
1369
|
+
text:
|
|
1370
|
+
typeof s.lastRunAt === "string"
|
|
1371
|
+
? fmtRelativeTime(s.lastRunAt, Date.now())
|
|
1372
|
+
: "never",
|
|
1373
|
+
}),
|
|
1374
|
+
el("td", { text: String(s.lastOutcome ?? "—") }),
|
|
1375
|
+
el("td", {
|
|
1376
|
+
text:
|
|
1377
|
+
typeof s.nextDueAt === "string"
|
|
1378
|
+
? fmtRelativeTime(s.nextDueAt, Date.now())
|
|
1379
|
+
: "—",
|
|
1380
|
+
}),
|
|
1381
|
+
el("td", null, [
|
|
1382
|
+
s.overdue === true ? dot("warn", "overdue") : dot("ok", "on schedule"),
|
|
1383
|
+
]),
|
|
1384
|
+
]),
|
|
1385
|
+
),
|
|
1386
|
+
),
|
|
1387
|
+
el("p", {
|
|
1388
|
+
class: "muted",
|
|
1389
|
+
text: "Run now lives on the Memory tab's dream card above: it goes through the job queue, which is window-idempotent and run.lock-serialized, so it can never double-fire against the daemon's janitor.",
|
|
1390
|
+
}),
|
|
1391
|
+
]),
|
|
1392
|
+
);
|
|
1393
|
+
|
|
1394
|
+
host.appendChild(
|
|
1395
|
+
card("Nightly cron scaffold", [
|
|
1396
|
+
el("p", {
|
|
1397
|
+
class: "muted",
|
|
1398
|
+
text: `print-only — this is what \`${String(data.verb ?? "crewhaus dream init")}\` would write to ${String(data.workflowPath ?? "")}`,
|
|
1399
|
+
}),
|
|
1400
|
+
el("div", { class: "editor-actions" }, [
|
|
1401
|
+
el("span", { class: "mono", text: `cron: ${String(data.cron ?? "")}` }),
|
|
1402
|
+
copyBtn(String(data.workflow ?? ""), "copy the workflow"),
|
|
1403
|
+
]),
|
|
1404
|
+
el("pre", { class: "rawjson", text: String(data.workflow ?? "") }),
|
|
1405
|
+
]),
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// ---------------------------------------------------------------------------
|
|
1410
|
+
// watchme
|
|
1411
|
+
// ---------------------------------------------------------------------------
|
|
1412
|
+
|
|
1413
|
+
async function watchmePanel(host, ctx, reload) {
|
|
1414
|
+
clear(host).appendChild(skeleton(8));
|
|
1415
|
+
const [analytics, intents, reports, synthesized] = await Promise.all([
|
|
1416
|
+
load(host, () => api.watchmeAnalytics({ id: ctx.id })),
|
|
1417
|
+
load(host, () => api.watchmeIntents({ id: ctx.id })),
|
|
1418
|
+
load(host, () => api.watchmeReports({ id: ctx.id })),
|
|
1419
|
+
load(host, () => api.watchmeSynthesized({ id: ctx.id })),
|
|
1420
|
+
]);
|
|
1421
|
+
clear(host);
|
|
1422
|
+
if (analytics === null) return;
|
|
1423
|
+
|
|
1424
|
+
const cost = costSummary(analytics);
|
|
1425
|
+
const models = Array.isArray(analytics.models) ? analytics.models : [];
|
|
1426
|
+
const tools = Array.isArray(analytics.tools) ? analytics.tools : [];
|
|
1427
|
+
const judgments =
|
|
1428
|
+
analytics.judgments && typeof analytics.judgments === "object" ? analytics.judgments : {};
|
|
1429
|
+
const observed =
|
|
1430
|
+
analytics.observedFeedback && typeof analytics.observedFeedback === "object"
|
|
1431
|
+
? analytics.observedFeedback
|
|
1432
|
+
: {};
|
|
1433
|
+
|
|
1434
|
+
host.appendChild(
|
|
1435
|
+
card(
|
|
1436
|
+
"Watchme",
|
|
1437
|
+
[
|
|
1438
|
+
el("div", { class: "filter-bar" }, [
|
|
1439
|
+
analytics.watching === true ? dot("ok", "watching") : dot("off", "not watching"),
|
|
1440
|
+
el("span", { class: "muted", text: `${fmtCount(analytics.sessions ?? 0)} sessions` }),
|
|
1441
|
+
el("span", {
|
|
1442
|
+
class: "muted",
|
|
1443
|
+
text: `${fmtCount(analytics.turns?.total ?? 0)} turns`,
|
|
1444
|
+
}),
|
|
1445
|
+
analytics.truncated === true
|
|
1446
|
+
? el("span", { class: "chip chip-warn", text: "capped — figures are floors" })
|
|
1447
|
+
: null,
|
|
1448
|
+
toggleControl(ctx, analytics, host, reload),
|
|
1449
|
+
]),
|
|
1450
|
+
analytics.present !== true ? empty(analytics, "Watchme has never run here") : null,
|
|
1451
|
+
models.length === 0
|
|
1452
|
+
? null
|
|
1453
|
+
: table(
|
|
1454
|
+
["Model", "Provider", "Turns", "In", "Out", "Cost"],
|
|
1455
|
+
models.map((m) =>
|
|
1456
|
+
el("tr", null, [
|
|
1457
|
+
el("td", { class: "mono", text: String(m.wire ?? "") }),
|
|
1458
|
+
el("td", { text: String(m.provider ?? "") }),
|
|
1459
|
+
el("td", { class: "num", text: fmtCount(Number(m.turns ?? 0)) }),
|
|
1460
|
+
el("td", { class: "num", text: fmtCount(Number(m.tokensIn ?? 0)) }),
|
|
1461
|
+
el("td", { class: "num", text: fmtCount(Number(m.tokensOut ?? 0)) }),
|
|
1462
|
+
el("td", {
|
|
1463
|
+
class: "num",
|
|
1464
|
+
text: fmtUsd(usdFromMicros(Number(m.costUsdMicros ?? 0))),
|
|
1465
|
+
}),
|
|
1466
|
+
]),
|
|
1467
|
+
),
|
|
1468
|
+
),
|
|
1469
|
+
kv("priced spend", fmtUsd(usdFromMicros(cost.priced))),
|
|
1470
|
+
cost.unpricedModels === 0
|
|
1471
|
+
? null
|
|
1472
|
+
: el("div", { class: "kv" }, [
|
|
1473
|
+
el("span", { class: "kv-k", text: "unpriced" }),
|
|
1474
|
+
el("span", { class: "kv-v" }, [
|
|
1475
|
+
dot(
|
|
1476
|
+
"warn",
|
|
1477
|
+
`${cost.unpricedModels} model(s), ${fmtCount(cost.unpricedTurns)} turns — cost UNKNOWN`,
|
|
1478
|
+
),
|
|
1479
|
+
]),
|
|
1480
|
+
]),
|
|
1481
|
+
cost.note === null ? null : el("p", { class: "muted", text: cost.note }),
|
|
1482
|
+
],
|
|
1483
|
+
asOf(typeof analytics.asOf === "string" ? analytics.asOf : null),
|
|
1484
|
+
),
|
|
1485
|
+
);
|
|
1486
|
+
|
|
1487
|
+
if (tools.length > 0) {
|
|
1488
|
+
host.appendChild(
|
|
1489
|
+
card(
|
|
1490
|
+
"Tool error rates",
|
|
1491
|
+
table(
|
|
1492
|
+
["Tool", "Calls", "Errors", "Rate"],
|
|
1493
|
+
tools.map((t) =>
|
|
1494
|
+
el("tr", null, [
|
|
1495
|
+
el("td", { class: "mono", text: String(t.name ?? "") }),
|
|
1496
|
+
el("td", { class: "num", text: fmtCount(Number(t.calls ?? 0)) }),
|
|
1497
|
+
el("td", { class: "num", text: fmtCount(Number(t.errors ?? 0)) }),
|
|
1498
|
+
el("td", null, [
|
|
1499
|
+
typeof t.errorRate === "number"
|
|
1500
|
+
? dot(t.errorRate > 0.1 ? "warn" : "ok", fmtPct(t.errorRate))
|
|
1501
|
+
: el("span", { class: "muted", text: "—" }),
|
|
1502
|
+
]),
|
|
1503
|
+
]),
|
|
1504
|
+
),
|
|
1505
|
+
),
|
|
1506
|
+
),
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// Two surfaces, deliberately apart: the judge's verdicts are a machine
|
|
1511
|
+
// signal and the human channel is not, and one blended number would hide
|
|
1512
|
+
// which of the two an operator is actually looking at.
|
|
1513
|
+
host.appendChild(
|
|
1514
|
+
card("Judge verdicts", [
|
|
1515
|
+
el("p", { class: "muted", text: String(judgments.source ?? "watchme/judgments.jsonl") }),
|
|
1516
|
+
kv("judged turns", fmtCount(Number(judgments.count ?? 0))),
|
|
1517
|
+
kv(
|
|
1518
|
+
"mean judge score",
|
|
1519
|
+
typeof judgments.meanScore === "number" ? judgments.meanScore.toFixed(2) : "—",
|
|
1520
|
+
),
|
|
1521
|
+
kv(
|
|
1522
|
+
"judge models",
|
|
1523
|
+
(Array.isArray(judgments.judgeModels) ? judgments.judgeModels : []).join(", ") || "—",
|
|
1524
|
+
),
|
|
1525
|
+
]),
|
|
1526
|
+
);
|
|
1527
|
+
host.appendChild(
|
|
1528
|
+
card("Observed human feedback", [
|
|
1529
|
+
el("p", {
|
|
1530
|
+
class: "muted",
|
|
1531
|
+
text: `${String(observed.source ?? "watchme observations")} — a separate store from the judge above, never summed with it`,
|
|
1532
|
+
}),
|
|
1533
|
+
el("div", { class: "filter-bar" }, [
|
|
1534
|
+
dot("ok", `${fmtCount(Number(observed.up ?? 0))} up`),
|
|
1535
|
+
dot("warn", `${fmtCount(Number(observed.down ?? 0))} down`),
|
|
1536
|
+
]),
|
|
1537
|
+
]),
|
|
1538
|
+
);
|
|
1539
|
+
|
|
1540
|
+
const clusters = Array.isArray(intents?.intents) ? intents.intents : [];
|
|
1541
|
+
host.appendChild(
|
|
1542
|
+
card(
|
|
1543
|
+
"Intent clusters",
|
|
1544
|
+
clusters.length === 0
|
|
1545
|
+
? empty(intents, "No intent clusters yet")
|
|
1546
|
+
: table(
|
|
1547
|
+
["Cluster", "Count", "Last seen", "Sessions"],
|
|
1548
|
+
clusters.map((c) =>
|
|
1549
|
+
el("tr", null, [
|
|
1550
|
+
el("td", { text: String(c.cluster ?? "") }),
|
|
1551
|
+
el("td", { class: "num", text: fmtCount(Number(c.count ?? 0)) }),
|
|
1552
|
+
el("td", {
|
|
1553
|
+
text:
|
|
1554
|
+
typeof c.lastSeen === "string" ? fmtRelativeTime(c.lastSeen, Date.now()) : "—",
|
|
1555
|
+
}),
|
|
1556
|
+
el(
|
|
1557
|
+
"td",
|
|
1558
|
+
null,
|
|
1559
|
+
(Array.isArray(c.sessions) ? c.sessions : [])
|
|
1560
|
+
.slice(0, 5)
|
|
1561
|
+
.map((s) => sessionLink(ctx, String(s), String(s).slice(-6))),
|
|
1562
|
+
),
|
|
1563
|
+
]),
|
|
1564
|
+
),
|
|
1565
|
+
),
|
|
1566
|
+
),
|
|
1567
|
+
);
|
|
1568
|
+
|
|
1569
|
+
host.appendChild(reportsCard(reports, ctx));
|
|
1570
|
+
host.appendChild(synthesizedCard(synthesized, ctx, host, reload));
|
|
1571
|
+
host.appendChild(publishCard(ctx));
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
function toggleControl(ctx, analytics, host, reload) {
|
|
1575
|
+
const watching = analytics.watching === true;
|
|
1576
|
+
const btn = el("button", {
|
|
1577
|
+
class: "btn btn-ghost",
|
|
1578
|
+
type: "button",
|
|
1579
|
+
text: watching ? "Turn watching off" : "Turn watching on",
|
|
1580
|
+
title: "watchme: is a human-owned spec block — this is a spec edit, not a flag file",
|
|
1581
|
+
});
|
|
1582
|
+
const nameField = field("type the spec name to apply", { placeholder: "spec name" });
|
|
1583
|
+
const wrap = el("div", { class: "gated" }, [btn, nameField.node]);
|
|
1584
|
+
btn.addEventListener("click", () => {
|
|
1585
|
+
void run(
|
|
1586
|
+
btn,
|
|
1587
|
+
"Toggle",
|
|
1588
|
+
() =>
|
|
1589
|
+
api.watchmeToggle(
|
|
1590
|
+
{ id: ctx.id },
|
|
1591
|
+
{
|
|
1592
|
+
watching: !watching,
|
|
1593
|
+
confirm: true,
|
|
1594
|
+
...(nameField.input.value.trim() !== ""
|
|
1595
|
+
? { confirmName: nameField.input.value.trim() }
|
|
1596
|
+
: {}),
|
|
1597
|
+
},
|
|
1598
|
+
),
|
|
1599
|
+
(res) => {
|
|
1600
|
+
if (res.ok && res.body?.applied === true) void watchmePanel(host, ctx, reload);
|
|
1601
|
+
else if (res.ok && res.body?.code === "needs_typed_confirm") {
|
|
1602
|
+
toast(
|
|
1603
|
+
String(res.body.note ?? "type the spec name to apply this human-owned edit"),
|
|
1604
|
+
"info",
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
},
|
|
1608
|
+
);
|
|
1609
|
+
});
|
|
1610
|
+
return wrap;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
function reportsCard(reports, ctx) {
|
|
1614
|
+
const rows = Array.isArray(reports?.reports) ? reports.reports : [];
|
|
1615
|
+
const detail = el("div");
|
|
1616
|
+
if (rows.length === 0) return card("Reports", empty(reports, "No watchme report yet"));
|
|
1617
|
+
return card("Reports", [
|
|
1618
|
+
table(
|
|
1619
|
+
["Report", "When", "Summary", ""],
|
|
1620
|
+
rows.map((r) =>
|
|
1621
|
+
el("tr", null, [
|
|
1622
|
+
el("td", { class: "mono", text: String(r.stamp ?? "") }),
|
|
1623
|
+
el("td", { text: typeof r.at === "string" ? fmtRelativeTime(r.at, Date.now()) : "—" }),
|
|
1624
|
+
el("td", { text: String(r.summary ?? "—") }),
|
|
1625
|
+
el("td", null, [
|
|
1626
|
+
el("button", {
|
|
1627
|
+
class: "btn btn-ghost",
|
|
1628
|
+
type: "button",
|
|
1629
|
+
text: "Open",
|
|
1630
|
+
onClick: () => void showReport(detail, ctx, String(r.stamp)),
|
|
1631
|
+
}),
|
|
1632
|
+
]),
|
|
1633
|
+
]),
|
|
1634
|
+
),
|
|
1635
|
+
),
|
|
1636
|
+
detail,
|
|
1637
|
+
]);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
async function showReport(host, ctx, stamp) {
|
|
1641
|
+
clear(host).appendChild(skeleton(4));
|
|
1642
|
+
const data = await load(host, () => api.watchmeReport({ id: ctx.id, stamp }));
|
|
1643
|
+
clear(host);
|
|
1644
|
+
if (data === null) return;
|
|
1645
|
+
if (data.present !== true) {
|
|
1646
|
+
host.appendChild(empty(data, "No such report"));
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
host.appendChild(el("h4", { class: "sub-title", text: stamp }));
|
|
1650
|
+
host.appendChild(mdBlocks(String(data.body ?? "")));
|
|
1651
|
+
if (data.truncated === true) {
|
|
1652
|
+
host.appendChild(el("p", { class: "muted", text: "long report — the first part only" }));
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
/** Synthesize review: a proposal is rendered, never applied by rendering it. */
|
|
1657
|
+
function synthesizedCard(synthesized, ctx, host, reload) {
|
|
1658
|
+
const proposals = Array.isArray(synthesized?.proposals) ? synthesized.proposals : [];
|
|
1659
|
+
if (proposals.length === 0) {
|
|
1660
|
+
return card("Synthesized proposals", empty(synthesized, "Nothing has been synthesized yet"));
|
|
1661
|
+
}
|
|
1662
|
+
return card("Synthesized proposals", [
|
|
1663
|
+
el("p", {
|
|
1664
|
+
class: "muted",
|
|
1665
|
+
text: String(synthesized.note ?? "advisory only — reading a proposal is not applying it"),
|
|
1666
|
+
}),
|
|
1667
|
+
...proposals.map((p) => proposalBlock(p, ctx, host, reload)),
|
|
1668
|
+
]);
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
function proposalBlock(proposal, ctx, host, reload) {
|
|
1672
|
+
const edits = Array.isArray(proposal.edits) ? proposal.edits : [];
|
|
1673
|
+
const picked = new Set();
|
|
1674
|
+
const nameField = field("type the spec name for human-owned edits", { placeholder: "spec name" });
|
|
1675
|
+
const apply = el("button", { class: "btn btn-primary", type: "button", text: "Apply selected" });
|
|
1676
|
+
apply.addEventListener("click", () => {
|
|
1677
|
+
void run(
|
|
1678
|
+
apply,
|
|
1679
|
+
"Apply",
|
|
1680
|
+
() =>
|
|
1681
|
+
api.watchmeApply(
|
|
1682
|
+
{ id: ctx.id, stamp: String(proposal.stamp) },
|
|
1683
|
+
{
|
|
1684
|
+
edits: [...picked],
|
|
1685
|
+
confirm: true,
|
|
1686
|
+
...(nameField.input.value.trim() !== ""
|
|
1687
|
+
? { confirmName: nameField.input.value.trim() }
|
|
1688
|
+
: {}),
|
|
1689
|
+
},
|
|
1690
|
+
),
|
|
1691
|
+
(res) => {
|
|
1692
|
+
if (res.ok && res.body?.applied === true) void watchmePanel(host, ctx, reload);
|
|
1693
|
+
},
|
|
1694
|
+
);
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1697
|
+
const rows = edits.map((edit) => {
|
|
1698
|
+
const box = el("input", { type: "checkbox" });
|
|
1699
|
+
box.addEventListener("change", () => {
|
|
1700
|
+
if (box.checked) picked.add(String(edit.path));
|
|
1701
|
+
else picked.delete(String(edit.path));
|
|
1702
|
+
});
|
|
1703
|
+
return el("tr", null, [
|
|
1704
|
+
el("td", null, [box]),
|
|
1705
|
+
el("td", { class: "mono", text: String(edit.path ?? "") }),
|
|
1706
|
+
el("td", null, [
|
|
1707
|
+
edit.tier === "auto-tunable"
|
|
1708
|
+
? dot("ok", "auto-tunable")
|
|
1709
|
+
: dot("warn", "human-owned — needs the typed confirmation"),
|
|
1710
|
+
]),
|
|
1711
|
+
el("td", { class: "mono", text: String(edit.before ?? "—") }),
|
|
1712
|
+
el("td", { class: "mono", text: String(edit.after ?? "—") }),
|
|
1713
|
+
el("td", { class: "muted", text: String(edit.rationale ?? "") }),
|
|
1714
|
+
]);
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
return collapsible(
|
|
1718
|
+
[
|
|
1719
|
+
el("span", { class: "mono", text: String(proposal.stamp ?? "") }),
|
|
1720
|
+
el("span", {
|
|
1721
|
+
class: "muted",
|
|
1722
|
+
text: ` ${String(proposal.autoTunable ?? 0)} auto-tunable · ${String(proposal.humanOwned ?? 0)} human-owned`,
|
|
1723
|
+
}),
|
|
1724
|
+
],
|
|
1725
|
+
[
|
|
1726
|
+
typeof proposal.error === "string" && proposal.error !== ""
|
|
1727
|
+
? el("p", { class: "muted", text: proposal.error })
|
|
1728
|
+
: null,
|
|
1729
|
+
edits.length === 0
|
|
1730
|
+
? el("p", { class: "muted", text: "this proposal changes nothing in the live spec" })
|
|
1731
|
+
: table(["", "Path", "Tier", "Now", "Proposed", "Why"], rows),
|
|
1732
|
+
el("div", { class: "editor-actions" }, [nameField.node, apply]),
|
|
1733
|
+
collapsible(
|
|
1734
|
+
[el("span", { class: "muted", text: "the proposed spec" })],
|
|
1735
|
+
[el("pre", { class: "rawjson", text: String(proposal.yaml ?? "") })],
|
|
1736
|
+
),
|
|
1737
|
+
],
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
function publishCard(ctx) {
|
|
1742
|
+
const out = el("div");
|
|
1743
|
+
const dry = el("button", { class: "btn", type: "button", text: "Preview publish" });
|
|
1744
|
+
const nameField = field("type the spec name to publish", { placeholder: "spec name" });
|
|
1745
|
+
const real = el("button", { class: "btn btn-danger", type: "button", text: "Publish" });
|
|
1746
|
+
dry.addEventListener("click", () => {
|
|
1747
|
+
void run(
|
|
1748
|
+
dry,
|
|
1749
|
+
"Preview",
|
|
1750
|
+
() => api.watchmePublish({ id: ctx.id }, { dryRun: true }),
|
|
1751
|
+
(res) => {
|
|
1752
|
+
clear(out);
|
|
1753
|
+
if (!res.ok) return;
|
|
1754
|
+
const targets = Array.isArray(res.body?.targets) ? res.body.targets : [];
|
|
1755
|
+
out.appendChild(
|
|
1756
|
+
table(
|
|
1757
|
+
["Article", "Would"],
|
|
1758
|
+
targets.map((t) =>
|
|
1759
|
+
el("tr", null, [
|
|
1760
|
+
el("td", { class: "mono", text: String(t.slug ?? "") }),
|
|
1761
|
+
el("td", null, [
|
|
1762
|
+
dot(t.action === "create" ? "ok" : "warn", String(t.action ?? "")),
|
|
1763
|
+
]),
|
|
1764
|
+
]),
|
|
1765
|
+
),
|
|
1766
|
+
),
|
|
1767
|
+
);
|
|
1768
|
+
out.appendChild(el("p", { class: "muted", text: String(res.body?.note ?? "") }));
|
|
1769
|
+
},
|
|
1770
|
+
);
|
|
1771
|
+
});
|
|
1772
|
+
real.addEventListener("click", () => {
|
|
1773
|
+
void run(real, "Publish", () =>
|
|
1774
|
+
api.watchmePublish(
|
|
1775
|
+
{ id: ctx.id },
|
|
1776
|
+
{ dryRun: false, confirmName: nameField.input.value.trim() },
|
|
1777
|
+
),
|
|
1778
|
+
);
|
|
1779
|
+
});
|
|
1780
|
+
return card("Publish findings to the wiki", [
|
|
1781
|
+
el("p", {
|
|
1782
|
+
class: "muted",
|
|
1783
|
+
text: "publishing writes articles into the memory fabric — the dry run is what makes it a review instead of a surprise",
|
|
1784
|
+
}),
|
|
1785
|
+
el("div", { class: "editor-actions" }, [dry, nameField.node, real]),
|
|
1786
|
+
out,
|
|
1787
|
+
]);
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
// ---------------------------------------------------------------------------
|
|
1791
|
+
// learning + knowledge
|
|
1792
|
+
// ---------------------------------------------------------------------------
|
|
1793
|
+
|
|
1794
|
+
async function learningPanel(host, ctx) {
|
|
1795
|
+
clear(host).appendChild(skeleton(5));
|
|
1796
|
+
const data = await load(host, () => api.learning({ id: ctx.id }));
|
|
1797
|
+
clear(host);
|
|
1798
|
+
if (data === null) return;
|
|
1799
|
+
if (data.declared !== true) {
|
|
1800
|
+
host.appendChild(card("Learning", empty(data, "This spec declares no learning: block")));
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
const curriculum =
|
|
1804
|
+
data.curriculum && typeof data.curriculum === "object" ? data.curriculum : null;
|
|
1805
|
+
const exam = data.exam && typeof data.exam === "object" ? data.exam : null;
|
|
1806
|
+
const study = data.study && typeof data.study === "object" ? data.study : {};
|
|
1807
|
+
const gaps = data.gaps && typeof data.gaps === "object" ? data.gaps : {};
|
|
1808
|
+
|
|
1809
|
+
host.appendChild(
|
|
1810
|
+
card("Learning", [
|
|
1811
|
+
el("div", { class: "filter-bar" }, [
|
|
1812
|
+
data.enabled === true ? dot("ok", "enabled") : dot("off", "declared but off"),
|
|
1813
|
+
study.onHeartbeat === true
|
|
1814
|
+
? dot("ok", "studies on heartbeat")
|
|
1815
|
+
: dot("off", "no heartbeat study"),
|
|
1816
|
+
study.onDream === true ? dot("ok", "studies on dream") : dot("off", "no dream study"),
|
|
1817
|
+
]),
|
|
1818
|
+
kv("domain", String(data.domain ?? "—")),
|
|
1819
|
+
(Array.isArray(data.sources) ? data.sources : []).length === 0
|
|
1820
|
+
? null
|
|
1821
|
+
: el("div", { class: "kv" }, [
|
|
1822
|
+
el("span", { class: "kv-k", text: "source allowlist" }),
|
|
1823
|
+
el(
|
|
1824
|
+
"span",
|
|
1825
|
+
{ class: "kv-v" },
|
|
1826
|
+
data.sources.map((s) => el("span", { class: "chip", text: String(s) })),
|
|
1827
|
+
),
|
|
1828
|
+
]),
|
|
1829
|
+
]),
|
|
1830
|
+
);
|
|
1831
|
+
|
|
1832
|
+
if (curriculum !== null) {
|
|
1833
|
+
const rungs = Array.isArray(curriculum.rungs) ? curriculum.rungs : [];
|
|
1834
|
+
host.appendChild(
|
|
1835
|
+
card("Curriculum", [
|
|
1836
|
+
el("p", {
|
|
1837
|
+
class: "muted",
|
|
1838
|
+
text: `${String(curriculum.done ?? 0)} / ${String(curriculum.total ?? 0)} rungs — ${String(curriculum.path ?? "")}`,
|
|
1839
|
+
}),
|
|
1840
|
+
curriculum.present !== true
|
|
1841
|
+
? el("p", { class: "muted", text: String(curriculum.note ?? "") })
|
|
1842
|
+
: el(
|
|
1843
|
+
"ul",
|
|
1844
|
+
{ class: "check-list" },
|
|
1845
|
+
rungs.map((r) =>
|
|
1846
|
+
el("li", null, [
|
|
1847
|
+
r.done === true ? dot("ok", "done") : dot("off", "open"),
|
|
1848
|
+
el("span", { text: ` ${String(r.text ?? "")}` }),
|
|
1849
|
+
]),
|
|
1850
|
+
),
|
|
1851
|
+
),
|
|
1852
|
+
]),
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
if (exam !== null) {
|
|
1857
|
+
const last = exam.lastRun && typeof exam.lastRun === "object" ? exam.lastRun : null;
|
|
1858
|
+
host.appendChild(
|
|
1859
|
+
card("Exam", [
|
|
1860
|
+
kv("dataset", String(exam.dataset ?? "—")),
|
|
1861
|
+
kv("graders", String(exam.graders ?? "—")),
|
|
1862
|
+
last === null
|
|
1863
|
+
? emptyState(
|
|
1864
|
+
"The exam has never run",
|
|
1865
|
+
`crewhaus eval crewhaus.yaml --dataset ${String(exam.dataset ?? "")}`,
|
|
1866
|
+
)
|
|
1867
|
+
: el("div", null, [
|
|
1868
|
+
kv("pass rate", fmtPct(Number(last.passRate ?? 0))),
|
|
1869
|
+
kv("samples", fmtCount(Number(last.sampleCount ?? 0))),
|
|
1870
|
+
kv("when", typeof last.ts === "string" ? fmtRelativeTime(last.ts, Date.now()) : "—"),
|
|
1871
|
+
]),
|
|
1872
|
+
el("p", { class: "muted", text: String(exam.note ?? "") }),
|
|
1873
|
+
]),
|
|
1874
|
+
);
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
const goals = Array.isArray(gaps.goals) ? gaps.goals : [];
|
|
1878
|
+
const articles = Array.isArray(gaps.articles) ? gaps.articles : [];
|
|
1879
|
+
host.appendChild(
|
|
1880
|
+
card("Knowledge gaps (the study queue)", [
|
|
1881
|
+
goals.length + articles.length === 0
|
|
1882
|
+
? emptyState("No open gaps", "crewhaus run (the agent logs gaps as it studies)")
|
|
1883
|
+
: el("ul", { class: "check-list" }, [
|
|
1884
|
+
...goals.map((g) =>
|
|
1885
|
+
el("li", null, [
|
|
1886
|
+
dot("warn", "goal"),
|
|
1887
|
+
el("span", { text: ` ${String(g.title ?? "")}` }),
|
|
1888
|
+
]),
|
|
1889
|
+
),
|
|
1890
|
+
...articles.map((a) =>
|
|
1891
|
+
el("li", null, [
|
|
1892
|
+
dot("warn", "article"),
|
|
1893
|
+
el("a", {
|
|
1894
|
+
class: "name-link",
|
|
1895
|
+
href: hrefHarness(ctx.id, "memory", "wiki", String(a.slug)),
|
|
1896
|
+
text: ` ${String(a.title ?? a.slug)}`,
|
|
1897
|
+
}),
|
|
1898
|
+
]),
|
|
1899
|
+
),
|
|
1900
|
+
]),
|
|
1901
|
+
el("p", {
|
|
1902
|
+
class: "muted",
|
|
1903
|
+
text: `hosted twin: Thredz tasks tagged \`${String(data.thredzTag ?? "knowledge-gap")}\` — the same queue, on the Thredz tab`,
|
|
1904
|
+
}),
|
|
1905
|
+
]),
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
async function knowledgePanel(host, ctx, reload) {
|
|
1910
|
+
clear(host).appendChild(skeleton(5));
|
|
1911
|
+
const data = await load(host, () => api.knowledge({ id: ctx.id }));
|
|
1912
|
+
clear(host);
|
|
1913
|
+
if (data === null) return;
|
|
1914
|
+
const counts = data.counts && typeof data.counts === "object" ? data.counts : {};
|
|
1915
|
+
const manifest = Array.isArray(data.manifest) ? data.manifest : [];
|
|
1916
|
+
const out = el("div");
|
|
1917
|
+
|
|
1918
|
+
const pull = el("button", { class: "btn", type: "button", text: "Preview pull" });
|
|
1919
|
+
pull.addEventListener("click", () => {
|
|
1920
|
+
void run(
|
|
1921
|
+
pull,
|
|
1922
|
+
"Pull",
|
|
1923
|
+
() => api.knowledgeSync({ id: ctx.id }, { direction: "pull", dryRun: true }),
|
|
1924
|
+
(res) => {
|
|
1925
|
+
clear(out);
|
|
1926
|
+
if (res.ok)
|
|
1927
|
+
out.appendChild(el("p", { class: "muted", text: String(res.body?.note ?? "") }));
|
|
1928
|
+
},
|
|
1929
|
+
);
|
|
1930
|
+
});
|
|
1931
|
+
const pushPreview = el("button", { class: "btn", type: "button", text: "Preview push" });
|
|
1932
|
+
const nameField = field("type the spec name to push", { placeholder: "spec name" });
|
|
1933
|
+
const push = el("button", { class: "btn btn-danger", type: "button", text: "Push" });
|
|
1934
|
+
pushPreview.addEventListener("click", () => {
|
|
1935
|
+
void run(
|
|
1936
|
+
pushPreview,
|
|
1937
|
+
"Preview push",
|
|
1938
|
+
() => api.knowledgeSync({ id: ctx.id }, { direction: "push", dryRun: true }),
|
|
1939
|
+
(res) => {
|
|
1940
|
+
clear(out);
|
|
1941
|
+
if (!res.ok) return;
|
|
1942
|
+
const preview =
|
|
1943
|
+
res.body?.preview && typeof res.body.preview === "object" ? res.body.preview : {};
|
|
1944
|
+
const redacted = Array.isArray(preview.wouldRedact) ? preview.wouldRedact : [];
|
|
1945
|
+
out.appendChild(
|
|
1946
|
+
el("p", {
|
|
1947
|
+
class: "muted",
|
|
1948
|
+
text: `${String(preview.candidates ?? 0)} fact(s) would be considered`,
|
|
1949
|
+
}),
|
|
1950
|
+
);
|
|
1951
|
+
out.appendChild(
|
|
1952
|
+
redacted.length === 0
|
|
1953
|
+
? el("p", { class: "muted" }, [dot("ok", "nothing looks credential-shaped")])
|
|
1954
|
+
: el("div", null, [
|
|
1955
|
+
el("p", { class: "muted" }, [
|
|
1956
|
+
dot("warn", `${redacted.length} fact(s) carry credential-shaped text`),
|
|
1957
|
+
]),
|
|
1958
|
+
el(
|
|
1959
|
+
"ul",
|
|
1960
|
+
{ class: "fact-list" },
|
|
1961
|
+
redacted.map((r) =>
|
|
1962
|
+
el("li", { class: "fact" }, [
|
|
1963
|
+
el("div", { text: String(r.text ?? "") }),
|
|
1964
|
+
el("div", { class: "fact-meta" }, [
|
|
1965
|
+
el("span", { class: "mono muted", text: String(r.id ?? "") }),
|
|
1966
|
+
]),
|
|
1967
|
+
]),
|
|
1968
|
+
),
|
|
1969
|
+
),
|
|
1970
|
+
]),
|
|
1971
|
+
);
|
|
1972
|
+
out.appendChild(el("p", { class: "muted", text: String(res.body?.note ?? "") }));
|
|
1973
|
+
},
|
|
1974
|
+
);
|
|
1975
|
+
});
|
|
1976
|
+
push.addEventListener("click", () => {
|
|
1977
|
+
void run(
|
|
1978
|
+
push,
|
|
1979
|
+
"Push",
|
|
1980
|
+
() =>
|
|
1981
|
+
api.knowledgeSync(
|
|
1982
|
+
{ id: ctx.id },
|
|
1983
|
+
{ direction: "push", dryRun: false, confirmName: nameField.input.value.trim() },
|
|
1984
|
+
),
|
|
1985
|
+
() => void knowledgePanel(host, ctx, reload),
|
|
1986
|
+
);
|
|
1987
|
+
});
|
|
1988
|
+
|
|
1989
|
+
host.appendChild(
|
|
1990
|
+
card("Knowledge sync", [
|
|
1991
|
+
el("div", { class: "filter-bar" }, [
|
|
1992
|
+
data.share === true
|
|
1993
|
+
? dot("ok", "opted in to sharing")
|
|
1994
|
+
: dot("off", "not opted in — a fleet sync skips this harness"),
|
|
1995
|
+
data.present === true
|
|
1996
|
+
? dot("ok", "shared store present")
|
|
1997
|
+
: dot("unknown", "no shared store yet"),
|
|
1998
|
+
]),
|
|
1999
|
+
kv("shared store", el("span", { class: "mono", text: String(data.sharedDir ?? "—") })),
|
|
2000
|
+
kv("resolved from", String(data.sharedDirSource ?? "—")),
|
|
2001
|
+
kv(
|
|
2002
|
+
"shared artifacts",
|
|
2003
|
+
`${fmtCount(Number(counts.memories ?? 0))} memories · ${fmtCount(Number(counts.graders ?? 0))} graders · ${fmtCount(Number(counts.prompts ?? 0))} prompts`,
|
|
2004
|
+
),
|
|
2005
|
+
kv("pushed by this harness", fmtCount(Number(data.pushedByThisHarness ?? 0))),
|
|
2006
|
+
el("div", { class: "editor-actions" }, [pull, pushPreview, nameField.node, push]),
|
|
2007
|
+
el("p", {
|
|
2008
|
+
class: "muted",
|
|
2009
|
+
text: "push is the one direction that can leak: it redacts on the way out and drops anything still credential-shaped",
|
|
2010
|
+
}),
|
|
2011
|
+
out,
|
|
2012
|
+
]),
|
|
2013
|
+
);
|
|
2014
|
+
|
|
2015
|
+
if (manifest.length > 0) {
|
|
2016
|
+
host.appendChild(
|
|
2017
|
+
card(
|
|
2018
|
+
"Provenance manifest",
|
|
2019
|
+
collapsible(
|
|
2020
|
+
[el("span", { class: "muted", text: `${manifest.length} shared artifact record(s)` })],
|
|
2021
|
+
[jsonPre(manifest)],
|
|
2022
|
+
),
|
|
2023
|
+
),
|
|
2024
|
+
);
|
|
2025
|
+
}
|
|
2026
|
+
}
|