@azure-id/orc 1.7.1 → 1.8.1

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +3649 -3381
  2. package/README-id.md +923 -844
  3. package/README.md +836 -788
  4. package/bin/build-agents.js +43 -27
  5. package/bin/cli.js +701 -3
  6. package/bin/graph-extract.js +927 -0
  7. package/bin/graph-notes.js +188 -0
  8. package/bin/graph-query.js +808 -0
  9. package/bin/graph-resolve.js +178 -0
  10. package/bin/graph-signals.js +277 -0
  11. package/bin/graph.js +605 -0
  12. package/bin/verify-contracts.js +4669 -4553
  13. package/bin/verify-package.js +626 -616
  14. package/bin/webui/api.js +1419 -1414
  15. package/bin/webui/fixtures/index.js +579 -576
  16. package/bin/webui/fixtures/knowledge.js +316 -291
  17. package/bin/webui/i18n/en/knowledge.json +167 -151
  18. package/bin/webui/i18n/en/overview.json +101 -100
  19. package/bin/webui/i18n/id/knowledge.json +167 -151
  20. package/bin/webui/i18n/id/overview.json +101 -100
  21. package/bin/webui/js/panels/knowledge.js +1065 -1006
  22. package/bin/webui/js/panels/overview.js +492 -488
  23. package/package.json +39 -39
  24. package/templates/agents/MODEL-MAPPING.md +163 -158
  25. package/templates/agents/orc-executor-haiku-4-5.md +133 -121
  26. package/templates/agents/orc-executor-opus-4-7-high.md +134 -122
  27. package/templates/agents/orc-executor-opus-4-7-med.md +134 -122
  28. package/templates/agents/orc-executor-opus-4-8-high.md +134 -122
  29. package/templates/agents/orc-executor-opus-5-high.md +134 -122
  30. package/templates/agents/orc-executor-opus-5-low.md +134 -122
  31. package/templates/agents/orc-executor-opus-5-med.md +134 -122
  32. package/templates/agents/orc-executor-sonnet-4-6-high.md +134 -122
  33. package/templates/agents/orc-executor-sonnet-4-6-med.md +134 -122
  34. package/templates/agents/orc-executor-sonnet-5-high.md +134 -122
  35. package/templates/agents/orc-graph-noter-sonnet-4-6-med.md +86 -0
  36. package/templates/hooks/README.md +444 -396
  37. package/templates/hooks/orc-graph-hook.js +336 -0
  38. package/templates/hooks/orc-statusline-render.js +922 -921
  39. package/templates/hooks/orc-statusline.js +1596 -1545
  40. package/templates/skills/_shared/README.md +4 -0
  41. package/templates/skills/_shared/code-graph.md +220 -0
  42. package/templates/skills/_shared/opus5-only.md +4 -0
  43. package/templates/skills/_shared/phases/execution.md +166 -147
  44. package/templates/skills/_shared/phases/planning.md +142 -135
  45. package/templates/skills/_shared/phases/preflight.md +132 -118
  46. package/templates/skills/_shared/phases/review.md +63 -53
  47. package/templates/skills/_shared/phases/ship.md +96 -88
  48. package/templates/skills/_shared/phases/trace.md +6 -0
  49. package/templates/skills/_shared/phases/wiki-consult.md +194 -189
  50. package/templates/skills/_shared/read-ladder.md +124 -102
  51. package/templates/skills/_shared/return-validation.md +259 -250
  52. package/templates/skills/orc/SKILL.md +255 -254
  53. package/templates/skills/orc-diy/references/flow-schema.md +101 -100
  54. package/templates/skills/orc-fast/SKILL.md +236 -229
  55. package/templates/skills/orc-mini/SKILL.md +267 -259
  56. package/templates/skills/orc-quick/SKILL.md +378 -361
  57. package/templates/skills/orc-quick/references/dispatch-gate.md +6 -0
  58. package/templates/skills/orc-wiki/references/staleness.md +294 -288
@@ -1,488 +1,492 @@
1
- "use strict";
2
- /* panels/overview.js — orc ui client
3
- FINDING_ROUTE (a caution routes to the panel that can CLEAR it), the
4
- attention list, the stat tiles.
5
-
6
- Loaded by app.html in the order its numeric prefix names. Classic
7
- script, no import/export: an ES module import carries no query string,
8
- and every static request here needs the per-launch session token. */
9
-
10
-
11
- /* ================================================================ OVERVIEW == */
12
-
13
- /* WHERE A PROBLEM IS ACTUALLY FIXED (v0.43.6).
14
- `orc doctor` reports every problem in one list, and the panel used to send
15
- the whole list to Maintenance with a single "Open Maintenance" button. That
16
- is right for the install-footprint findings — version skew, orphans, missing
17
- files, unwired hooks — because `orc update` / `doctor --fix` really is where
18
- those are repaired. It was WRONG for the ones whose fix lives on another
19
- panel, and `diy-stale` was the one people hit: the flow is recompiled with
20
- `orc diy compile`, which is a button on FLOW. Sending them to Maintenance
21
- pointed at a page with no control for the thing it was complaining about.
22
-
23
- So the routing is a table keyed on the finding id the CLI already emits.
24
- `null` means there is nothing to press anywhere — a dangling trace pointer
25
- clears itself on the next run, and offering a button for it would be a lie. */
26
- const FINDING_ROUTE = {
27
- "diy-stale": { panel: "flow", cta: "overview.item.diyStale.cta" },
28
- "trace-pointer-dangling": { panel: null },
29
- // v0.46.0. Correct by the rule, not by default: `export-stale` is cleared by
30
- // `orc export`, which IS a CLI write Maintenance can run — so Maintenance is
31
- // genuinely the panel that can clear it, not merely the fallback.
32
- "export-stale": { panel: "maintenance", cta: "overview.item.exportStale.cta" },
33
- "pact-broken": { panel: "pact", cta: "overview.item.pactBroken.cta" },
34
- // v0.48.1. Same rule again: `orc doc audit` and `orc doc ship` are both on
35
- // DOCS, so that is where a drifted document has to send you.
36
- "doc-drifted": { panel: "docs", cta: "overview.item.docDrifted.cta" },
37
- "boundary-refuse": { panel: "boundary", cta: "overview.item.boundaryRefuse.cta" },
38
- // v0.49.1. Same rule a third time. `orc wiki sync` is a BUTTON on Knowledge
39
- // and it is free, and `orc wiki plan` (which ranks the refresh) is the card
40
- // directly above it — so Knowledge is genuinely the panel that can clear
41
- // both, not merely the fallback.
42
- "wiki-unregistered": { panel: "knowledge", cta: "overview.item.wikiUnregistered.cta" },
43
- "wiki-debt": { panel: "knowledge", cta: "overview.item.wikiDebt.cta" },
44
- // v0.50.0. `orc doctor` collapses the whole `orc extra` subsystem into ONE
45
- // finding carrying the count and the ids — a doctor that recited eleven ids
46
- // for a subsystem most repos never arm is a doctor people learn to scroll
47
- // past. It routes to Extra because that is where every one of them is
48
- // rendered beside the connection it is about, and where the connection test
49
- // that clears most of them lives.
50
- "extra-findings": { panel: "extra", cta: "overview.item.extraFindings.cta" },
51
- // v0.56.0. The rule again, and this time the finding is about the TOOL rather
52
- // than the project: a legacy global `orc` package owns the `orc` command and
53
- // blocks every install source. `orc upgrade` is the thing that clears it (it
54
- // evicts the old package first), and `upgrade` is an action row on
55
- // MAINTENANCE — so that is genuinely the panel that can clear it.
56
- "legacy-global-package": { panel: "maintenance", cta: "overview.item.legacyGlobalPackage.cta" },
57
- // v1.0.0 W5. The rule again: a demoted run is cleared by `orc extra promote`,
58
- // and Recovery is the Extra tab where every other journal-shaped state is
59
- // already rendered — so that is genuinely the panel that can clear it.
60
- "extra-demoted-run": { panel: "extra", cta: "overview.item.extraDemotedRun.cta" },
61
- // v1.0.0 W16. An INSTALL-FOOTPRINT finding, so it takes the documented
62
- // default rather than an interesting exception: the thing that clears it is
63
- // `orc update`, which is an action row on MAINTENANCE. It is spelled out
64
- // rather than left to DEFAULT_FINDING_ROUTE because the CTA is specific --
65
- // a reader who sees "lane config drifted" should be told which button.
66
- "lane-keys-drifted": { panel: "maintenance", cta: "overview.item.laneKeysDrifted.cta" },
67
- // v1.3.0. The rule again, and cleanly: every one of these is cleared on the
68
- // CLI Hook Interface panel — the board is where an orphaned component is
69
- // removed, the caution strip is where an invalid layout is named, and the
70
- // Rebuild button is where a stale compile is fixed.
71
- "statusline-layout-orphaned": { panel: "hookui", cta: "overview.item.statuslineOrphaned.cta" },
72
- "statusline-layout-invalid": { panel: "hookui", cta: "overview.item.statuslineInvalid.cta" },
73
- "statusline-layout-unreadable": { panel: "hookui", cta: "overview.item.statuslineUnreadable.cta" },
74
- // v1.5.0. The rule again, and all four the same way: every command that
75
- // clears one of these — `orc test env`, `orc test report`, `orc test
76
- // security` — is on the TEST panel, beside the run it is about.
77
- // `test-evidence-unstaged` goes there too even though the fix is a line in
78
- // .gitignore that ORC will not write: Test is where the sentence explaining
79
- // WHY that folder must never be staged already lives.
80
- // v1.6.0. An INSTALL-FOOTPRINT finding takes the documented default:
81
- // `read-gate-unwired` is cleared by `orc update`, which is an action row on
82
- // MAINTENANCE. `read-gate-fallback` is NOT a button anywhere — the record
83
- // ages out on its own and there is nothing to press — so it gets
84
- // `panel: null` rather than a button that would do nothing, the same call
85
- // `trace-pointer-dangling` made.
86
- "read-gate-unwired": { panel: "maintenance", cta: "overview.item.readGateUnwired.cta" },
87
- "read-gate-fallback": { panel: null },
88
- "test-env-unhealthy": { panel: "test", cta: "overview.item.testEnvUnhealthy.cta" },
89
- "test-run-red": { panel: "test", cta: "overview.item.testRunRed.cta" },
90
- "test-unchecked-owasp": { panel: "test", cta: "overview.item.testUncheckedOwasp.cta" },
91
- "test-evidence-unstaged": { panel: "test", cta: "overview.item.testEvidenceUnstaged.cta" },
92
- };
93
- const DEFAULT_FINDING_ROUTE = { panel: "maintenance", cta: "overview.item.doctor.cta" };
94
- const findingRoute = (id) => FINDING_ROUTE[id] || DEFAULT_FINDING_ROUTE;
95
-
96
- PANELS.overview = function (host) {
97
- head(host, t("overview.title"), t("overview.sub"));
98
- section(
99
- host,
100
- () => read("/api/overview").then((r) => r.data),
101
- (d) => {
102
- const out = frag();
103
- const doctor = d.doctor || {};
104
- const w = d.wiki || {};
105
- const p = d.patterns || {};
106
- // Rows since v0.49.2: `{ slug, updated_ms, lane }`. Everything that only
107
- // wants a count still reads `.length`.
108
- const waiting = d.waiting || [];
109
- const findings = doctor.findings || [];
110
-
111
- /* --- the tiles ------------------------------------------------------ */
112
- const stats = el("div", "grid grid-3");
113
- stats.append(
114
- statTile(
115
- t("overview.tile.install"),
116
- doctor.ok ? t("overview.tile.installHealthy") : tn(findings.length, "overview.tile.installIssues"),
117
- doctor.installed_version
118
- ? t("overview.tile.installNote", { payload: doctor.installed_version, cli: doctor.package_version })
119
- : "",
120
- doctor.ok ? "ok" : "warn",
121
- doctor.ok ? null : "maintenance"
122
- )
123
- );
124
- stats.append(
125
- statTile(
126
- t("overview.tile.wiki"),
127
- // TIER and STATE are CLI vocabulary (FRESH / AGING / STALE /
128
- // unregistered) — the words the docs and every other lane use. They
129
- // are shown as-is in both languages; only the sentence under them is
130
- // translated.
131
- w.state === "registered" ? w.tier || t("overview.tile.wikiUnknown") : String(w.state || "none").toUpperCase(),
132
- w.state === "registered"
133
- ? t("overview.tile.wikiNote", { docs: w.docs, scan: w.last_scan || "?" })
134
- : t("overview.tile.wikiNone"),
135
- w.tier === "FRESH" ? "ok" : w.tier === "AGING" ? "warn" : w.state === "none" ? "" : "bad",
136
- "knowledge"
137
- )
138
- );
139
- stats.append(
140
- statTile(
141
- t("overview.tile.waiting"),
142
- String(waiting.length),
143
- t("overview.tile.waitingNote", { n: d.runs_total || 0 }),
144
- waiting.length ? "warn" : "ok",
145
- "runs"
146
- )
147
- );
148
- const langs = (p.patterns || []).map((x) => x.lang);
149
- stats.append(
150
- statTile(
151
- t("overview.tile.patterns"),
152
- String(langs.length),
153
- langs.length ? langs.join(", ") : t("overview.tile.patternsNone"),
154
- langs.length ? "ok" : "",
155
- "knowledge"
156
- )
157
- );
158
-
159
- /* v0.46.0 — three chips repeating the CLI's OWN state words. A chip with
160
- nothing to say still renders its good state: an absent chip and a
161
- healthy one must never look the same. */
162
- const pc = d.pact;
163
- const bc = d.boundary;
164
- const wd = d.wiki_debt;
165
- stats.append(
166
- statTile(
167
- t("overview.tile.pact"),
168
- pc && pc.ok
169
- ? (pc.counts.BROKEN ? pc.counts.BROKEN + " BROKEN" : pc.counts.DRIFTED ? pc.counts.DRIFTED + " DRIFTED" : pc.counts.HOLDING + " HOLDING")
170
- : t("overview.tile.none"),
171
- pc && pc.ok ? t("overview.tile.pactNote", { n: pc.entries, u: pc.counts.UNCHECKABLE }) : t("overview.tile.pactNone"),
172
- pc && pc.ok ? (pc.counts.BROKEN ? "bad" : pc.counts.DRIFTED ? "warn" : "ok") : "",
173
- pc && pc.ok ? "pact" : null
174
- )
175
- );
176
- stats.append(
177
- statTile(
178
- t("overview.tile.boundary"),
179
- bc && bc.cards && bc.cards.length
180
- ? (bc.counts.REFUSE ? bc.counts.REFUSE + " REFUSE" : bc.counts.ESCALATE ? bc.counts.ESCALATE + " ESCALATE" : bc.counts.EXECUTE + " EXECUTE")
181
- : t("overview.tile.none"),
182
- bc && bc.cards && bc.cards.length ? t("overview.tile.boundaryNote", { n: bc.cards.length, stale: bc.stale }) : t("overview.tile.boundaryNone"),
183
- bc && bc.cards && bc.cards.length ? (bc.counts.REFUSE ? "bad" : bc.counts.ESCALATE ? "warn" : "ok") : "",
184
- bc && bc.cards && bc.cards.length ? "boundary" : null
185
- )
186
- );
187
- stats.append(
188
- statTile(
189
- t("overview.tile.debt"),
190
- wd && wd.ok && wd.pending ? String(wd.pending) : "0",
191
- wd && wd.ok && wd.pending
192
- ? t("overview.tile.debtNote", { tier: wd.tier, tok: kTokUi((wd.tokens || {}).input + (wd.tokens || {}).cache_write + (wd.tokens || {}).cache_read + (wd.tokens || {}).output) })
193
- : t("overview.tile.debtNone"),
194
- wd && wd.ok && wd.pending ? "warn" : "ok",
195
- "knowledge"
196
- )
197
- );
198
-
199
- // The version check crosses the network, so it must never hold up the
200
- // tiles beside it: the tile renders in its pending state and fills itself
201
- // in when the answer lands.
202
- const vt = statTile(t("overview.tile.version"), t("common.checking"), t("overview.tile.versionChecking"), "");
203
- vt.classList.add("stat-pending");
204
- stats.append(vt);
205
- versionInfo()
206
- .then((v) => {
207
- const s = versionState(v);
208
- vt.classList.remove("stat-pending");
209
- vt.replaceChildren();
210
- vt.append(el("div", "stat-label", t("overview.tile.version")));
211
- const val = el("div", "stat-value" + (s.kind ? " stat-" + s.kind : ""));
212
- val.append(document.createTextNode(v && v.version ? "v" + v.version : "?"), chip(s.label, s.kind));
213
- vt.append(val, el("div", "stat-note", s.note));
214
- })
215
- .catch(() => {
216
- vt.classList.remove("stat-pending");
217
- vt.replaceChildren(
218
- el("div", "stat-label", t("overview.tile.version")),
219
- el("div", "stat-value", "?"),
220
- el("div", "stat-note", t("overview.tile.versionFailed"))
221
- );
222
- });
223
- out.append(stats);
224
-
225
- /* --- worth doing ----------------------------------------------------
226
- One list, in severity order, of everything that wants a decision — and
227
- every row carries the panel where the fix actually is. This is the
228
- panel's answer to "what now", and it is the only place the wiki tier
229
- turns into advice instead of a colour. */
230
- out.append(attentionCard(d, findings));
231
-
232
- /* --- waiting runs --------------------------------------------------- */
233
- if (waiting.length) {
234
- const c = card(t("overview.waiting.title"));
235
- c.append(el("div", "note", t("overview.waiting.note")));
236
- const list = el("div", "run-list");
237
- for (const r of waiting) {
238
- // The CARD CONTRACT (v0.49.2). `.run-card` is a four-column grid —
239
- // caret, chip, mid, age — and this row used to append three children
240
- // with no caret, so the chip landed in the 16px caret column and
241
- // printed over an 88px-wide slug. One column short is not a rounding
242
- // error; it is the whole card. This row navigates rather than expands,
243
- // so it declares `.no-caret` and fills the three columns that variant
244
- // has, in order.
245
- const b = el("button", "run-card no-caret");
246
- b.type = "button";
247
- b.append(chip("waiting", "warn"));
248
- const mid = el("div", "run-mid");
249
- mid.append(el("div", "run-slug", r.slug));
250
- mid.append(el("div", "run-where", r.lane ? r.lane + " · " + t("overview.waiting.where") : t("overview.waiting.where")));
251
- b.append(mid, el("div", "run-age", r.updated_ms ? t("overview.waiting.since", { age: relAge(r.updated_ms) }) : ""));
252
- b.addEventListener("click", () => {
253
- location.hash = "#/runs?slug=" + encodeURIComponent(r.slug);
254
- });
255
- list.append(b);
256
- // A CAUTION ROUTES TO THE PANEL THAT CAN CLEAR IT — and here the panel
257
- // that can clear it is this one. A waiting run is what blocks the
258
- // upgrade preview, so the way out sits beside the thing complaining.
259
- const done = el("button", "btn btn-sm btn-ghost", t("runs.close.button"));
260
- done.type = "button";
261
- done.addEventListener("click", (ev) => {
262
- ev.stopPropagation();
263
- confirmRunClose(r.slug, () => route());
264
- });
265
- const act = el("div", "row-actions run-card-actions");
266
- act.append(done);
267
- list.append(act);
268
- }
269
- c.append(list);
270
- out.append(c);
271
- }
272
-
273
- /* --- v0.54.0: foreign dispatches that never reported back ------------
274
- ONE LINE, and only when there is something to say. Money was spent and
275
- work is half-done, and nothing will look at it again unless somebody is
276
- told. It ROUTES to the panel that can clear it (Extra ▸ Recovery) — it
277
- never resumes anything, because continuing a third party's unfinished
278
- write without asking is the same class of act as routing off Claude
279
- without saying so. */
280
- const ej = d.extra_journal;
281
- if (ej && ej.orphans) {
282
- const c = card(t("overview.extraOrphan.title"));
283
- c.append(el("div", "note", t("overview.extraOrphan.note")));
284
- // `.no-caret` — this row NAVIGATES rather than expands, and every
285
- // `.run-card` variant declares its own column count (v0.49.2). A card
286
- // one column short is not a rounding error; it is the whole card.
287
- const b = el("button", "run-card no-caret");
288
- b.type = "button";
289
- b.append(chip(tn(ej.orphans, "overview.extraOrphan.chip"), "bad"));
290
- const mid = el("div", "run-mid");
291
- mid.append(el("div", "run-slug", t("overview.extraOrphan.slug")));
292
- mid.append(el("div", "run-where", t("overview.extraOrphan.where")));
293
- b.append(mid, el("div", "run-age", ""));
294
- b.addEventListener("click", () => {
295
- location.hash = "#/extra";
296
- });
297
- c.append(b);
298
- out.append(c);
299
- }
300
-
301
- /* --- the raw doctor list -------------------------------------------
302
- Kept below the actionable card, because it is the EVIDENCE: the exact
303
- message the CLI printed, unedited and untranslated, so what you read
304
- here is what you would read in a terminal. */
305
- if (!doctor.ok && findings.length) {
306
- const c = card(t("overview.doctor.title"));
307
- for (const f of findings) {
308
- const row = el("div", "finding");
309
- row.append(chip(f.fixable ? t("overview.doctor.fixable") : t("overview.doctor.manual"), f.fixable ? "info" : "warn"));
310
- const detail = el("div");
311
- detail.append(el("div", null, f.message));
312
- const r = findingRoute(f.id);
313
- if (r.panel) {
314
- const go = el("button", "btn btn-ghost btn-sm", t(r.cta));
315
- go.type = "button";
316
- go.addEventListener("click", () => (location.hash = "#/" + r.panel));
317
- detail.append(go);
318
- } else {
319
- detail.append(el("div", "note", t("overview.attention.nothingToDo")));
320
- }
321
- row.append(detail);
322
- c.append(row);
323
- }
324
- out.append(c);
325
- }
326
-
327
- /* --- where things are ----------------------------------------------- */
328
- const paths = card(t("overview.paths.title"));
329
- const where = d.where || {};
330
- paths.append(
331
- kvList(
332
- [
333
- [t("overview.paths.project"), where.project_root],
334
- [t("overview.paths.config"), where.config],
335
- [t("overview.paths.skills"), where.skills],
336
- [t("overview.paths.runs"), where.run_dir],
337
- [t("overview.paths.traces"), where.log_dir],
338
- ],
339
- true
340
- )
341
- );
342
- out.append(paths);
343
-
344
- const know = card(t("overview.know.title"));
345
- know.append(
346
- kvList([
347
- [t("overview.know.patterns"), langs.join(", ") || t("common.none")],
348
- [t("overview.know.tags"), w.crosslink_tags === undefined ? "" : String(w.crosslink_tags)],
349
- // `state` and `reason` are the CLI's — shown verbatim.
350
- [t("overview.know.diy"), d.diy ? `${d.diy.state} — ${d.diy.reason}` : ""],
351
- ])
352
- );
353
- out.append(know);
354
-
355
- return out;
356
- }
357
- );
358
- };
359
-
360
- // The actionable list. Each entry is {kind, title, body, panel, cta} and knows
361
- // where its fix lives — never a blanket "go to Maintenance".
362
- function attentionCard(d, findings) {
363
- const w = d.wiki || {};
364
- const p = d.patterns || {};
365
- const items = [];
366
-
367
- // 1. Install findings, routed per id.
368
- for (const f of findings) {
369
- const r = findingRoute(f.id);
370
- if (f.id === "diy-stale") {
371
- items.push({
372
- kind: "warn",
373
- title: t("overview.item.diyStale.title"),
374
- body: t("overview.item.diyStale.body"),
375
- evidence: d.diy && d.diy.reason,
376
- panel: "flow",
377
- cta: t("overview.item.diyStale.cta"),
378
- });
379
- } else if (r.panel) {
380
- items.push({
381
- kind: f.fixable ? "warn" : "bad",
382
- // A doctor finding is already a sentence written for a human — using it
383
- // as the title beats paraphrasing it into something less exact.
384
- title: f.message,
385
- panel: r.panel,
386
- cta: t(r.cta),
387
- });
388
- }
389
- }
390
-
391
- // 2. The wiki. THIS is the recommendation the panel was missing: an AGING
392
- // wiki is not an error, it is the moment a refresh is still cheap.
393
- if (!w.state || w.state === "none") {
394
- items.push({ kind: "info", title: t("overview.item.wikiNone.title"), body: t("overview.item.wikiNone.body"), panel: "knowledge", cta: t("overview.item.wikiNone.cta") });
395
- } else if (w.state !== "registered") {
396
- items.push({ kind: "warn", title: t("overview.item.wikiUnregistered.title"), body: t("overview.item.wikiUnregistered.body"), panel: "knowledge", cta: t("overview.item.wikiUnregistered.cta") });
397
- } else if (w.tier === "AGING") {
398
- items.push({
399
- kind: "warn",
400
- title: t("overview.item.wikiAging.title"),
401
- body: t("overview.item.wikiAging.body"),
402
- evidence: (w.reasons || [])[0],
403
- panel: "knowledge",
404
- cta: t("overview.item.wikiAging.cta"),
405
- });
406
- } else if (w.tier && w.tier !== "FRESH") {
407
- items.push({
408
- kind: "bad",
409
- title: t("overview.item.wikiStale.title"),
410
- body: t("overview.item.wikiStale.body"),
411
- evidence: (w.reasons || [])[0],
412
- panel: "knowledge",
413
- cta: t("overview.item.wikiStale.cta"),
414
- });
415
- }
416
-
417
- // 3. No cached pattern — not a fault, but the cheapest quality win there is.
418
- if (!(p.patterns || []).length)
419
- items.push({ kind: "info", title: t("overview.item.patterns.title"), body: t("overview.item.patterns.body"), panel: "knowledge", cta: t("overview.item.patterns.cta") });
420
-
421
- // 4. Runs left waiting.
422
- if ((d.waiting || []).length)
423
- items.push({
424
- kind: "warn",
425
- title: tn((d.waiting || []).length, "overview.item.waiting.title"),
426
- body: t("overview.item.waiting.body"),
427
- panel: "runs",
428
- cta: t("overview.item.waiting.cta"),
429
- });
430
-
431
- const c = card(t("overview.attention.title"), chip(String(items.length), items.length ? "warn" : "ok"));
432
- c.id = "attention-card";
433
-
434
- if (!items.length) {
435
- const ok = el("div", "all-clear");
436
- ok.append(el("div", "all-clear-mark", "✓"));
437
- const txt = el("div");
438
- txt.append(el("div", "all-clear-title", t("overview.attention.allClear")));
439
- txt.append(el("div", "note", t("overview.attention.allClearHint")));
440
- ok.append(txt);
441
- c.append(ok);
442
- }
443
-
444
- const list = el("div", "todo-list");
445
- const order = { bad: 0, warn: 1, info: 2 };
446
- items.sort((a, b) => (order[a.kind] || 3) - (order[b.kind] || 3));
447
- for (const it of items) {
448
- const row = el("button", "todo todo-" + it.kind);
449
- row.type = "button";
450
- row.append(el("span", "todo-mark"));
451
- const mid = el("div", "todo-body");
452
- mid.append(el("div", "todo-title", it.title));
453
- if (it.body) mid.append(el("div", "todo-text", it.body));
454
- // The CLI's own words for WHY, kept verbatim under our explanation.
455
- if (it.evidence) mid.append(el("div", "todo-evidence", it.evidence));
456
- row.append(mid, el("span", "todo-cta", it.cta));
457
- row.addEventListener("click", () => (location.hash = "#/" + it.panel));
458
- list.append(row);
459
- }
460
- if (items.length) c.append(list);
461
-
462
- // The update offer is appended asynchronously so a slow network check never
463
- // delays the list — it arrives as one more row when the answer does.
464
- versionInfo()
465
- .then((v) => {
466
- if (!v || !v.update_available) return;
467
- const row = el("button", "todo todo-info todo-late");
468
- row.type = "button";
469
- row.append(el("span", "todo-mark"));
470
- const mid = el("div", "todo-body");
471
- mid.append(el("div", "todo-title", t("overview.item.update.title", { latest: v.latest })));
472
- mid.append(el("div", "todo-text", t("overview.item.update.body", { version: v.version })));
473
- row.append(mid, el("span", "todo-cta", t("overview.item.update.cta")));
474
- row.addEventListener("click", () => (location.hash = "#/maintenance"));
475
- if (!list.isConnected) c.append(list);
476
- list.append(row);
477
- const count = c.querySelector(".card-head .chip");
478
- if (count) {
479
- count.textContent = String(list.children.length);
480
- count.className = "chip chip-warn";
481
- }
482
- const clear = c.querySelector(".all-clear");
483
- if (clear) clear.remove();
484
- })
485
- .catch(() => {});
486
-
487
- return c;
488
- }
1
+ "use strict";
2
+ /* panels/overview.js — orc ui client
3
+ FINDING_ROUTE (a caution routes to the panel that can CLEAR it), the
4
+ attention list, the stat tiles.
5
+
6
+ Loaded by app.html in the order its numeric prefix names. Classic
7
+ script, no import/export: an ES module import carries no query string,
8
+ and every static request here needs the per-launch session token. */
9
+
10
+
11
+ /* ================================================================ OVERVIEW == */
12
+
13
+ /* WHERE A PROBLEM IS ACTUALLY FIXED (v0.43.6).
14
+ `orc doctor` reports every problem in one list, and the panel used to send
15
+ the whole list to Maintenance with a single "Open Maintenance" button. That
16
+ is right for the install-footprint findings — version skew, orphans, missing
17
+ files, unwired hooks — because `orc update` / `doctor --fix` really is where
18
+ those are repaired. It was WRONG for the ones whose fix lives on another
19
+ panel, and `diy-stale` was the one people hit: the flow is recompiled with
20
+ `orc diy compile`, which is a button on FLOW. Sending them to Maintenance
21
+ pointed at a page with no control for the thing it was complaining about.
22
+
23
+ So the routing is a table keyed on the finding id the CLI already emits.
24
+ `null` means there is nothing to press anywhere — a dangling trace pointer
25
+ clears itself on the next run, and offering a button for it would be a lie. */
26
+ const FINDING_ROUTE = {
27
+ "diy-stale": { panel: "flow", cta: "overview.item.diyStale.cta" },
28
+ "trace-pointer-dangling": { panel: null },
29
+ // v0.46.0. Correct by the rule, not by default: `export-stale` is cleared by
30
+ // `orc export`, which IS a CLI write Maintenance can run — so Maintenance is
31
+ // genuinely the panel that can clear it, not merely the fallback.
32
+ "export-stale": { panel: "maintenance", cta: "overview.item.exportStale.cta" },
33
+ "pact-broken": { panel: "pact", cta: "overview.item.pactBroken.cta" },
34
+ // v0.48.1. Same rule again: `orc doc audit` and `orc doc ship` are both on
35
+ // DOCS, so that is where a drifted document has to send you.
36
+ "doc-drifted": { panel: "docs", cta: "overview.item.docDrifted.cta" },
37
+ "boundary-refuse": { panel: "boundary", cta: "overview.item.boundaryRefuse.cta" },
38
+ // v0.49.1. Same rule a third time. `orc wiki sync` is a BUTTON on Knowledge
39
+ // and it is free, and `orc wiki plan` (which ranks the refresh) is the card
40
+ // directly above it — so Knowledge is genuinely the panel that can clear
41
+ // both, not merely the fallback.
42
+ "wiki-unregistered": { panel: "knowledge", cta: "overview.item.wikiUnregistered.cta" },
43
+ "wiki-debt": { panel: "knowledge", cta: "overview.item.wikiDebt.cta" },
44
+ // v0.50.0. `orc doctor` collapses the whole `orc extra` subsystem into ONE
45
+ // finding carrying the count and the ids — a doctor that recited eleven ids
46
+ // for a subsystem most repos never arm is a doctor people learn to scroll
47
+ // past. It routes to Extra because that is where every one of them is
48
+ // rendered beside the connection it is about, and where the connection test
49
+ // that clears most of them lives.
50
+ "extra-findings": { panel: "extra", cta: "overview.item.extraFindings.cta" },
51
+ // v0.56.0. The rule again, and this time the finding is about the TOOL rather
52
+ // than the project: a legacy global `orc` package owns the `orc` command and
53
+ // blocks every install source. `orc upgrade` is the thing that clears it (it
54
+ // evicts the old package first), and `upgrade` is an action row on
55
+ // MAINTENANCE — so that is genuinely the panel that can clear it.
56
+ "legacy-global-package": { panel: "maintenance", cta: "overview.item.legacyGlobalPackage.cta" },
57
+ // v1.0.0 W5. The rule again: a demoted run is cleared by `orc extra promote`,
58
+ // and Recovery is the Extra tab where every other journal-shaped state is
59
+ // already rendered — so that is genuinely the panel that can clear it.
60
+ "extra-demoted-run": { panel: "extra", cta: "overview.item.extraDemotedRun.cta" },
61
+ // v1.0.0 W16. An INSTALL-FOOTPRINT finding, so it takes the documented
62
+ // default rather than an interesting exception: the thing that clears it is
63
+ // `orc update`, which is an action row on MAINTENANCE. It is spelled out
64
+ // rather than left to DEFAULT_FINDING_ROUTE because the CTA is specific --
65
+ // a reader who sees "lane config drifted" should be told which button.
66
+ "lane-keys-drifted": { panel: "maintenance", cta: "overview.item.laneKeysDrifted.cta" },
67
+ // v1.8.0. A DRIFTED code graph is cleared by the free `orc graph update`, which
68
+ // is a button on the Knowledge panel's code graph card — the panel that can
69
+ // CLEAR it, not the default.
70
+ "graph-drifted": { panel: "knowledge", cta: "overview.item.graphDrifted.cta" },
71
+ // v1.3.0. The rule again, and cleanly: every one of these is cleared on the
72
+ // CLI Hook Interface panel — the board is where an orphaned component is
73
+ // removed, the caution strip is where an invalid layout is named, and the
74
+ // Rebuild button is where a stale compile is fixed.
75
+ "statusline-layout-orphaned": { panel: "hookui", cta: "overview.item.statuslineOrphaned.cta" },
76
+ "statusline-layout-invalid": { panel: "hookui", cta: "overview.item.statuslineInvalid.cta" },
77
+ "statusline-layout-unreadable": { panel: "hookui", cta: "overview.item.statuslineUnreadable.cta" },
78
+ // v1.5.0. The rule again, and all four the same way: every command that
79
+ // clears one of these — `orc test env`, `orc test report`, `orc test
80
+ // security` — is on the TEST panel, beside the run it is about.
81
+ // `test-evidence-unstaged` goes there too even though the fix is a line in
82
+ // .gitignore that ORC will not write: Test is where the sentence explaining
83
+ // WHY that folder must never be staged already lives.
84
+ // v1.6.0. An INSTALL-FOOTPRINT finding takes the documented default:
85
+ // `read-gate-unwired` is cleared by `orc update`, which is an action row on
86
+ // MAINTENANCE. `read-gate-fallback` is NOT a button anywhere — the record
87
+ // ages out on its own and there is nothing to press — so it gets
88
+ // `panel: null` rather than a button that would do nothing, the same call
89
+ // `trace-pointer-dangling` made.
90
+ "read-gate-unwired": { panel: "maintenance", cta: "overview.item.readGateUnwired.cta" },
91
+ "read-gate-fallback": { panel: null },
92
+ "test-env-unhealthy": { panel: "test", cta: "overview.item.testEnvUnhealthy.cta" },
93
+ "test-run-red": { panel: "test", cta: "overview.item.testRunRed.cta" },
94
+ "test-unchecked-owasp": { panel: "test", cta: "overview.item.testUncheckedOwasp.cta" },
95
+ "test-evidence-unstaged": { panel: "test", cta: "overview.item.testEvidenceUnstaged.cta" },
96
+ };
97
+ const DEFAULT_FINDING_ROUTE = { panel: "maintenance", cta: "overview.item.doctor.cta" };
98
+ const findingRoute = (id) => FINDING_ROUTE[id] || DEFAULT_FINDING_ROUTE;
99
+
100
+ PANELS.overview = function (host) {
101
+ head(host, t("overview.title"), t("overview.sub"));
102
+ section(
103
+ host,
104
+ () => read("/api/overview").then((r) => r.data),
105
+ (d) => {
106
+ const out = frag();
107
+ const doctor = d.doctor || {};
108
+ const w = d.wiki || {};
109
+ const p = d.patterns || {};
110
+ // Rows since v0.49.2: `{ slug, updated_ms, lane }`. Everything that only
111
+ // wants a count still reads `.length`.
112
+ const waiting = d.waiting || [];
113
+ const findings = doctor.findings || [];
114
+
115
+ /* --- the tiles ------------------------------------------------------ */
116
+ const stats = el("div", "grid grid-3");
117
+ stats.append(
118
+ statTile(
119
+ t("overview.tile.install"),
120
+ doctor.ok ? t("overview.tile.installHealthy") : tn(findings.length, "overview.tile.installIssues"),
121
+ doctor.installed_version
122
+ ? t("overview.tile.installNote", { payload: doctor.installed_version, cli: doctor.package_version })
123
+ : "",
124
+ doctor.ok ? "ok" : "warn",
125
+ doctor.ok ? null : "maintenance"
126
+ )
127
+ );
128
+ stats.append(
129
+ statTile(
130
+ t("overview.tile.wiki"),
131
+ // TIER and STATE are CLI vocabulary (FRESH / AGING / STALE /
132
+ // unregistered) — the words the docs and every other lane use. They
133
+ // are shown as-is in both languages; only the sentence under them is
134
+ // translated.
135
+ w.state === "registered" ? w.tier || t("overview.tile.wikiUnknown") : String(w.state || "none").toUpperCase(),
136
+ w.state === "registered"
137
+ ? t("overview.tile.wikiNote", { docs: w.docs, scan: w.last_scan || "?" })
138
+ : t("overview.tile.wikiNone"),
139
+ w.tier === "FRESH" ? "ok" : w.tier === "AGING" ? "warn" : w.state === "none" ? "" : "bad",
140
+ "knowledge"
141
+ )
142
+ );
143
+ stats.append(
144
+ statTile(
145
+ t("overview.tile.waiting"),
146
+ String(waiting.length),
147
+ t("overview.tile.waitingNote", { n: d.runs_total || 0 }),
148
+ waiting.length ? "warn" : "ok",
149
+ "runs"
150
+ )
151
+ );
152
+ const langs = (p.patterns || []).map((x) => x.lang);
153
+ stats.append(
154
+ statTile(
155
+ t("overview.tile.patterns"),
156
+ String(langs.length),
157
+ langs.length ? langs.join(", ") : t("overview.tile.patternsNone"),
158
+ langs.length ? "ok" : "",
159
+ "knowledge"
160
+ )
161
+ );
162
+
163
+ /* v0.46.0 — three chips repeating the CLI's OWN state words. A chip with
164
+ nothing to say still renders its good state: an absent chip and a
165
+ healthy one must never look the same. */
166
+ const pc = d.pact;
167
+ const bc = d.boundary;
168
+ const wd = d.wiki_debt;
169
+ stats.append(
170
+ statTile(
171
+ t("overview.tile.pact"),
172
+ pc && pc.ok
173
+ ? (pc.counts.BROKEN ? pc.counts.BROKEN + " BROKEN" : pc.counts.DRIFTED ? pc.counts.DRIFTED + " DRIFTED" : pc.counts.HOLDING + " HOLDING")
174
+ : t("overview.tile.none"),
175
+ pc && pc.ok ? t("overview.tile.pactNote", { n: pc.entries, u: pc.counts.UNCHECKABLE }) : t("overview.tile.pactNone"),
176
+ pc && pc.ok ? (pc.counts.BROKEN ? "bad" : pc.counts.DRIFTED ? "warn" : "ok") : "",
177
+ pc && pc.ok ? "pact" : null
178
+ )
179
+ );
180
+ stats.append(
181
+ statTile(
182
+ t("overview.tile.boundary"),
183
+ bc && bc.cards && bc.cards.length
184
+ ? (bc.counts.REFUSE ? bc.counts.REFUSE + " REFUSE" : bc.counts.ESCALATE ? bc.counts.ESCALATE + " ESCALATE" : bc.counts.EXECUTE + " EXECUTE")
185
+ : t("overview.tile.none"),
186
+ bc && bc.cards && bc.cards.length ? t("overview.tile.boundaryNote", { n: bc.cards.length, stale: bc.stale }) : t("overview.tile.boundaryNone"),
187
+ bc && bc.cards && bc.cards.length ? (bc.counts.REFUSE ? "bad" : bc.counts.ESCALATE ? "warn" : "ok") : "",
188
+ bc && bc.cards && bc.cards.length ? "boundary" : null
189
+ )
190
+ );
191
+ stats.append(
192
+ statTile(
193
+ t("overview.tile.debt"),
194
+ wd && wd.ok && wd.pending ? String(wd.pending) : "0",
195
+ wd && wd.ok && wd.pending
196
+ ? t("overview.tile.debtNote", { tier: wd.tier, tok: kTokUi((wd.tokens || {}).input + (wd.tokens || {}).cache_write + (wd.tokens || {}).cache_read + (wd.tokens || {}).output) })
197
+ : t("overview.tile.debtNone"),
198
+ wd && wd.ok && wd.pending ? "warn" : "ok",
199
+ "knowledge"
200
+ )
201
+ );
202
+
203
+ // The version check crosses the network, so it must never hold up the
204
+ // tiles beside it: the tile renders in its pending state and fills itself
205
+ // in when the answer lands.
206
+ const vt = statTile(t("overview.tile.version"), t("common.checking"), t("overview.tile.versionChecking"), "");
207
+ vt.classList.add("stat-pending");
208
+ stats.append(vt);
209
+ versionInfo()
210
+ .then((v) => {
211
+ const s = versionState(v);
212
+ vt.classList.remove("stat-pending");
213
+ vt.replaceChildren();
214
+ vt.append(el("div", "stat-label", t("overview.tile.version")));
215
+ const val = el("div", "stat-value" + (s.kind ? " stat-" + s.kind : ""));
216
+ val.append(document.createTextNode(v && v.version ? "v" + v.version : "?"), chip(s.label, s.kind));
217
+ vt.append(val, el("div", "stat-note", s.note));
218
+ })
219
+ .catch(() => {
220
+ vt.classList.remove("stat-pending");
221
+ vt.replaceChildren(
222
+ el("div", "stat-label", t("overview.tile.version")),
223
+ el("div", "stat-value", "?"),
224
+ el("div", "stat-note", t("overview.tile.versionFailed"))
225
+ );
226
+ });
227
+ out.append(stats);
228
+
229
+ /* --- worth doing ----------------------------------------------------
230
+ One list, in severity order, of everything that wants a decision — and
231
+ every row carries the panel where the fix actually is. This is the
232
+ panel's answer to "what now", and it is the only place the wiki tier
233
+ turns into advice instead of a colour. */
234
+ out.append(attentionCard(d, findings));
235
+
236
+ /* --- waiting runs --------------------------------------------------- */
237
+ if (waiting.length) {
238
+ const c = card(t("overview.waiting.title"));
239
+ c.append(el("div", "note", t("overview.waiting.note")));
240
+ const list = el("div", "run-list");
241
+ for (const r of waiting) {
242
+ // The CARD CONTRACT (v0.49.2). `.run-card` is a four-column grid —
243
+ // caret, chip, mid, age — and this row used to append three children
244
+ // with no caret, so the chip landed in the 16px caret column and
245
+ // printed over an 88px-wide slug. One column short is not a rounding
246
+ // error; it is the whole card. This row navigates rather than expands,
247
+ // so it declares `.no-caret` and fills the three columns that variant
248
+ // has, in order.
249
+ const b = el("button", "run-card no-caret");
250
+ b.type = "button";
251
+ b.append(chip("waiting", "warn"));
252
+ const mid = el("div", "run-mid");
253
+ mid.append(el("div", "run-slug", r.slug));
254
+ mid.append(el("div", "run-where", r.lane ? r.lane + " · " + t("overview.waiting.where") : t("overview.waiting.where")));
255
+ b.append(mid, el("div", "run-age", r.updated_ms ? t("overview.waiting.since", { age: relAge(r.updated_ms) }) : ""));
256
+ b.addEventListener("click", () => {
257
+ location.hash = "#/runs?slug=" + encodeURIComponent(r.slug);
258
+ });
259
+ list.append(b);
260
+ // A CAUTION ROUTES TO THE PANEL THAT CAN CLEAR IT — and here the panel
261
+ // that can clear it is this one. A waiting run is what blocks the
262
+ // upgrade preview, so the way out sits beside the thing complaining.
263
+ const done = el("button", "btn btn-sm btn-ghost", t("runs.close.button"));
264
+ done.type = "button";
265
+ done.addEventListener("click", (ev) => {
266
+ ev.stopPropagation();
267
+ confirmRunClose(r.slug, () => route());
268
+ });
269
+ const act = el("div", "row-actions run-card-actions");
270
+ act.append(done);
271
+ list.append(act);
272
+ }
273
+ c.append(list);
274
+ out.append(c);
275
+ }
276
+
277
+ /* --- v0.54.0: foreign dispatches that never reported back ------------
278
+ ONE LINE, and only when there is something to say. Money was spent and
279
+ work is half-done, and nothing will look at it again unless somebody is
280
+ told. It ROUTES to the panel that can clear it (Extra ▸ Recovery) — it
281
+ never resumes anything, because continuing a third party's unfinished
282
+ write without asking is the same class of act as routing off Claude
283
+ without saying so. */
284
+ const ej = d.extra_journal;
285
+ if (ej && ej.orphans) {
286
+ const c = card(t("overview.extraOrphan.title"));
287
+ c.append(el("div", "note", t("overview.extraOrphan.note")));
288
+ // `.no-caret` — this row NAVIGATES rather than expands, and every
289
+ // `.run-card` variant declares its own column count (v0.49.2). A card
290
+ // one column short is not a rounding error; it is the whole card.
291
+ const b = el("button", "run-card no-caret");
292
+ b.type = "button";
293
+ b.append(chip(tn(ej.orphans, "overview.extraOrphan.chip"), "bad"));
294
+ const mid = el("div", "run-mid");
295
+ mid.append(el("div", "run-slug", t("overview.extraOrphan.slug")));
296
+ mid.append(el("div", "run-where", t("overview.extraOrphan.where")));
297
+ b.append(mid, el("div", "run-age", ""));
298
+ b.addEventListener("click", () => {
299
+ location.hash = "#/extra";
300
+ });
301
+ c.append(b);
302
+ out.append(c);
303
+ }
304
+
305
+ /* --- the raw doctor list -------------------------------------------
306
+ Kept below the actionable card, because it is the EVIDENCE: the exact
307
+ message the CLI printed, unedited and untranslated, so what you read
308
+ here is what you would read in a terminal. */
309
+ if (!doctor.ok && findings.length) {
310
+ const c = card(t("overview.doctor.title"));
311
+ for (const f of findings) {
312
+ const row = el("div", "finding");
313
+ row.append(chip(f.fixable ? t("overview.doctor.fixable") : t("overview.doctor.manual"), f.fixable ? "info" : "warn"));
314
+ const detail = el("div");
315
+ detail.append(el("div", null, f.message));
316
+ const r = findingRoute(f.id);
317
+ if (r.panel) {
318
+ const go = el("button", "btn btn-ghost btn-sm", t(r.cta));
319
+ go.type = "button";
320
+ go.addEventListener("click", () => (location.hash = "#/" + r.panel));
321
+ detail.append(go);
322
+ } else {
323
+ detail.append(el("div", "note", t("overview.attention.nothingToDo")));
324
+ }
325
+ row.append(detail);
326
+ c.append(row);
327
+ }
328
+ out.append(c);
329
+ }
330
+
331
+ /* --- where things are ----------------------------------------------- */
332
+ const paths = card(t("overview.paths.title"));
333
+ const where = d.where || {};
334
+ paths.append(
335
+ kvList(
336
+ [
337
+ [t("overview.paths.project"), where.project_root],
338
+ [t("overview.paths.config"), where.config],
339
+ [t("overview.paths.skills"), where.skills],
340
+ [t("overview.paths.runs"), where.run_dir],
341
+ [t("overview.paths.traces"), where.log_dir],
342
+ ],
343
+ true
344
+ )
345
+ );
346
+ out.append(paths);
347
+
348
+ const know = card(t("overview.know.title"));
349
+ know.append(
350
+ kvList([
351
+ [t("overview.know.patterns"), langs.join(", ") || t("common.none")],
352
+ [t("overview.know.tags"), w.crosslink_tags === undefined ? "" : String(w.crosslink_tags)],
353
+ // `state` and `reason` are the CLI's — shown verbatim.
354
+ [t("overview.know.diy"), d.diy ? `${d.diy.state} — ${d.diy.reason}` : ""],
355
+ ])
356
+ );
357
+ out.append(know);
358
+
359
+ return out;
360
+ }
361
+ );
362
+ };
363
+
364
+ // The actionable list. Each entry is {kind, title, body, panel, cta} and knows
365
+ // where its fix lives — never a blanket "go to Maintenance".
366
+ function attentionCard(d, findings) {
367
+ const w = d.wiki || {};
368
+ const p = d.patterns || {};
369
+ const items = [];
370
+
371
+ // 1. Install findings, routed per id.
372
+ for (const f of findings) {
373
+ const r = findingRoute(f.id);
374
+ if (f.id === "diy-stale") {
375
+ items.push({
376
+ kind: "warn",
377
+ title: t("overview.item.diyStale.title"),
378
+ body: t("overview.item.diyStale.body"),
379
+ evidence: d.diy && d.diy.reason,
380
+ panel: "flow",
381
+ cta: t("overview.item.diyStale.cta"),
382
+ });
383
+ } else if (r.panel) {
384
+ items.push({
385
+ kind: f.fixable ? "warn" : "bad",
386
+ // A doctor finding is already a sentence written for a human — using it
387
+ // as the title beats paraphrasing it into something less exact.
388
+ title: f.message,
389
+ panel: r.panel,
390
+ cta: t(r.cta),
391
+ });
392
+ }
393
+ }
394
+
395
+ // 2. The wiki. THIS is the recommendation the panel was missing: an AGING
396
+ // wiki is not an error, it is the moment a refresh is still cheap.
397
+ if (!w.state || w.state === "none") {
398
+ items.push({ kind: "info", title: t("overview.item.wikiNone.title"), body: t("overview.item.wikiNone.body"), panel: "knowledge", cta: t("overview.item.wikiNone.cta") });
399
+ } else if (w.state !== "registered") {
400
+ items.push({ kind: "warn", title: t("overview.item.wikiUnregistered.title"), body: t("overview.item.wikiUnregistered.body"), panel: "knowledge", cta: t("overview.item.wikiUnregistered.cta") });
401
+ } else if (w.tier === "AGING") {
402
+ items.push({
403
+ kind: "warn",
404
+ title: t("overview.item.wikiAging.title"),
405
+ body: t("overview.item.wikiAging.body"),
406
+ evidence: (w.reasons || [])[0],
407
+ panel: "knowledge",
408
+ cta: t("overview.item.wikiAging.cta"),
409
+ });
410
+ } else if (w.tier && w.tier !== "FRESH") {
411
+ items.push({
412
+ kind: "bad",
413
+ title: t("overview.item.wikiStale.title"),
414
+ body: t("overview.item.wikiStale.body"),
415
+ evidence: (w.reasons || [])[0],
416
+ panel: "knowledge",
417
+ cta: t("overview.item.wikiStale.cta"),
418
+ });
419
+ }
420
+
421
+ // 3. No cached pattern — not a fault, but the cheapest quality win there is.
422
+ if (!(p.patterns || []).length)
423
+ items.push({ kind: "info", title: t("overview.item.patterns.title"), body: t("overview.item.patterns.body"), panel: "knowledge", cta: t("overview.item.patterns.cta") });
424
+
425
+ // 4. Runs left waiting.
426
+ if ((d.waiting || []).length)
427
+ items.push({
428
+ kind: "warn",
429
+ title: tn((d.waiting || []).length, "overview.item.waiting.title"),
430
+ body: t("overview.item.waiting.body"),
431
+ panel: "runs",
432
+ cta: t("overview.item.waiting.cta"),
433
+ });
434
+
435
+ const c = card(t("overview.attention.title"), chip(String(items.length), items.length ? "warn" : "ok"));
436
+ c.id = "attention-card";
437
+
438
+ if (!items.length) {
439
+ const ok = el("div", "all-clear");
440
+ ok.append(el("div", "all-clear-mark", "✓"));
441
+ const txt = el("div");
442
+ txt.append(el("div", "all-clear-title", t("overview.attention.allClear")));
443
+ txt.append(el("div", "note", t("overview.attention.allClearHint")));
444
+ ok.append(txt);
445
+ c.append(ok);
446
+ }
447
+
448
+ const list = el("div", "todo-list");
449
+ const order = { bad: 0, warn: 1, info: 2 };
450
+ items.sort((a, b) => (order[a.kind] || 3) - (order[b.kind] || 3));
451
+ for (const it of items) {
452
+ const row = el("button", "todo todo-" + it.kind);
453
+ row.type = "button";
454
+ row.append(el("span", "todo-mark"));
455
+ const mid = el("div", "todo-body");
456
+ mid.append(el("div", "todo-title", it.title));
457
+ if (it.body) mid.append(el("div", "todo-text", it.body));
458
+ // The CLI's own words for WHY, kept verbatim under our explanation.
459
+ if (it.evidence) mid.append(el("div", "todo-evidence", it.evidence));
460
+ row.append(mid, el("span", "todo-cta", it.cta));
461
+ row.addEventListener("click", () => (location.hash = "#/" + it.panel));
462
+ list.append(row);
463
+ }
464
+ if (items.length) c.append(list);
465
+
466
+ // The update offer is appended asynchronously so a slow network check never
467
+ // delays the list — it arrives as one more row when the answer does.
468
+ versionInfo()
469
+ .then((v) => {
470
+ if (!v || !v.update_available) return;
471
+ const row = el("button", "todo todo-info todo-late");
472
+ row.type = "button";
473
+ row.append(el("span", "todo-mark"));
474
+ const mid = el("div", "todo-body");
475
+ mid.append(el("div", "todo-title", t("overview.item.update.title", { latest: v.latest })));
476
+ mid.append(el("div", "todo-text", t("overview.item.update.body", { version: v.version })));
477
+ row.append(mid, el("span", "todo-cta", t("overview.item.update.cta")));
478
+ row.addEventListener("click", () => (location.hash = "#/maintenance"));
479
+ if (!list.isConnected) c.append(list);
480
+ list.append(row);
481
+ const count = c.querySelector(".card-head .chip");
482
+ if (count) {
483
+ count.textContent = String(list.children.length);
484
+ count.className = "chip chip-warn";
485
+ }
486
+ const clear = c.querySelector(".all-clear");
487
+ if (clear) clear.remove();
488
+ })
489
+ .catch(() => {});
490
+
491
+ return c;
492
+ }