loki-mode 10.10.2 → 10.10.3
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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/dashboard/__init__.py +1 -1
- package/docs/CLI-REFERENCE.md +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/v10/BOARD.md +2 -0
- package/docs/v10/FAILURE-CLASSES.md +13 -0
- package/docs/v10/PROGRESS.md +9 -0
- package/docs/v10/cp-redesign/PARITY.md +188 -0
- package/docs/v10/cp-redesign/legacy/INDEX.md +1167 -0
- package/loki-ts/dist/loki.js +616 -614
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/packages/control-plane/dist/server.js +417 -336
- package/packages/control-plane/src/server/diffstat.ts +77 -0
- package/packages/control-plane/src/server/repos.ts +5 -2
- package/packages/control-plane/src/server/routes/index.ts +1 -1
- package/packages/control-plane/src/server/runs.ts +3 -1
- package/packages/control-plane/ui/dist/assets/index-D27veKDf.js +152 -0
- package/packages/control-plane/ui/dist/index.html +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/web-app/dist/assets/{AdminPage-BvbxMtZO.js → AdminPage-Dyx5OHmP.js} +1 -1
- package/web-app/dist/assets/{Avatar-Dc7Scg02.js → Avatar-WVxt-kYj.js} +1 -1
- package/web-app/dist/assets/{Badge-BmVuOqbM.js → Badge-16vWJ-6u.js} +1 -1
- package/web-app/dist/assets/{Button-DvzqhxRU.js → Button-CWVPIMTW.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-D8AZo7Ha.js → CockpitPage-D5WhogJK.js} +1 -1
- package/web-app/dist/assets/{ComparePage-NmqaTXDz.js → ComparePage-CjJfjxcT.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-PGl8Kzoq.js → ErrorBoundary-CNnObh81.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-2NY4n9gv.js → EvidenceReceiptPanel-Cn1PE_JV.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-DBO_LHHM.js → GitHubIssuesPanel-CcH9neSU.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-K7m9uJ1N.js → GitHubPRsPanel-C-n6inCN.js} +1 -1
- package/web-app/dist/assets/{HomePage-qNQzAJxM.js → HomePage-BZ7PCPFR.js} +2 -2
- package/web-app/dist/assets/{LoginPage-esu3OK65.js → LoginPage-CJsigIUa.js} +1 -1
- package/web-app/dist/assets/{MagicPage-CCdVvXrA.js → MagicPage-ClZaKmXT.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-bEvugU2t.js → MetricsPage-ELQn9-cf.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-BlEjQ1wX.js → NotFoundPage-Dc_zp3RF.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-Bnug_Yzv.js → ProjectPage-B5K1NDuH.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-M8r1vfpn.js → ProjectsPage-C2l5SiIN.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-NqAJpB48.js → SettingsPage-DkDNp6Pd.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-8FggxL1P.js → ShowcasePage-1zasFSKz.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-3Sz3kFOO.js → SystemSettingsPage-hIm_4NUx.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-nV-6vdWM.js → TeamsPage-DUb_TA22.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-Brbc_VR9.js → TemplatesPage-B1mj-eA4.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-D1yta5ac.js → TerminalOutput-BX1UwelI.js} +1 -1
- package/web-app/dist/assets/{activity-oEeKuqFe.js → activity-B8APKMuh.js} +1 -1
- package/web-app/dist/assets/{bell-3NJtqBRR.js → bell-BS8WL-tI.js} +1 -1
- package/web-app/dist/assets/{bot-CuvJZ6x1.js → bot-e9I6yy12.js} +1 -1
- package/web-app/dist/assets/{check-n4289IZw.js → check-BbgR7Ovw.js} +1 -1
- package/web-app/dist/assets/{chevron-left-CmQeio7h.js → chevron-left-BcDqv7aX.js} +1 -1
- package/web-app/dist/assets/{circle-alert-CmmVW-Pd.js → circle-alert-BnDT8wxe.js} +1 -1
- package/web-app/dist/assets/{clock-QFqk7GlA.js → clock-LPz79lks.js} +1 -1
- package/web-app/dist/assets/{cloud-BNkSokmq.js → cloud-BcgBi5T1.js} +1 -1
- package/web-app/dist/assets/{code-xml-nWeketqj.js → code-xml-5zcykgva.js} +1 -1
- package/web-app/dist/assets/{database-C83MXTmJ.js → database-D9KLqhHq.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-Blg6TO4y.js → dollar-sign-CrINoZ4o.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-Tk11oUzm.js → file-code-corner-CA7G72ip.js} +1 -1
- package/web-app/dist/assets/{file-plus-DF_eyA6I.js → file-plus-D8tHdHn_.js} +1 -1
- package/web-app/dist/assets/{globe-D5hDqqmn.js → globe-CT0Ts5bO.js} +1 -1
- package/web-app/dist/assets/{hammer-C2hm9lzl.js → hammer-tUpu0LJk.js} +1 -1
- package/web-app/dist/assets/{index-pMxOIwXq.js → index-DdjeiWUI.js} +3 -3
- package/web-app/dist/assets/{layers-XWQKGXg0.js → layers-BzzJ7w3C.js} +1 -1
- package/web-app/dist/assets/{loader-circle-BRuvorm2.js → loader-circle-BBdMdxIV.js} +1 -1
- package/web-app/dist/assets/{lock-BSGvQf5v.js → lock-Cg9DkEh7.js} +1 -1
- package/web-app/dist/assets/{package-3FJYUYgJ.js → package-DHs8IEJR.js} +1 -1
- package/web-app/dist/assets/{plus-CeIx6g9P.js → plus-70JSAqB1.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-ChutW42g.js → refresh-cw-DjggkboT.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-BdSjUAk-.js → rotate-ccw-c1NNyyL5.js} +1 -1
- package/web-app/dist/assets/{scroll-text-BjFqxpw3.js → scroll-text-B49wQrJ2.js} +1 -1
- package/web-app/dist/assets/{server-C7UEKdol.js → server-BHKG-dDi.js} +1 -1
- package/web-app/dist/assets/{shield-alert-DY8fpEs7.js → shield-alert-Dx9MoBGm.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-Cz1san1g.js → thumbs-up-frEbS59j.js} +1 -1
- package/web-app/dist/assets/{trash-2-D2MvGYeP.js → trash-2-1Gd8pXFG.js} +1 -1
- package/web-app/dist/assets/{trending-down-CxO3OYOB.js → trending-down-BpUeO-FP.js} +1 -1
- package/web-app/dist/assets/{trending-up-DCHKPJLe.js → trending-up-CvhWp8qO.js} +1 -1
- package/web-app/dist/assets/{upload-CdPJpSHC.js → upload-DZuMugqd.js} +1 -1
- package/web-app/dist/assets/{usePolling-DAH6K3ST.js → usePolling-D5KUSkfs.js} +1 -1
- package/web-app/dist/assets/{user-Byyt01W4.js → user-BF9vws84.js} +1 -1
- package/web-app/dist/index.html +1 -1
- package/packages/control-plane/ui/dist/assets/index-BrA22FUs.js +0 -152
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Changed files for a finished run when its events carry no diff: the run's receipt.json names base_sha and head_sha, and
|
|
2
|
+
// `git diff --numstat base head` in the run's discovered repo gives the per-file counts. Shas are validated as hex, git is
|
|
3
|
+
// spawned without a shell and with an explicit env, and a failure of any step reads as null (unmeasured), never as zero files.
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { closeSync, fstatSync, openSync, readSync, realpathSync } from "node:fs";
|
|
6
|
+
import { join, sep } from "node:path";
|
|
7
|
+
import { and, eq } from "drizzle-orm";
|
|
8
|
+
import type { Db } from "../db/migrate.ts";
|
|
9
|
+
import { localRepos } from "../db/schema.ts";
|
|
10
|
+
|
|
11
|
+
export interface DiffStat {
|
|
12
|
+
base: string;
|
|
13
|
+
head: string;
|
|
14
|
+
files: { path: string; added: number | null; removed: number | null }[];
|
|
15
|
+
added: number;
|
|
16
|
+
removed: number;
|
|
17
|
+
source: "git";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
21
|
+
const SHA = /^[0-9a-f]{40,64}$/;
|
|
22
|
+
const MAX_RECEIPT_BYTES = 1024 * 1024;
|
|
23
|
+
const GIT_TIMEOUT_MS = 5000;
|
|
24
|
+
const cache = new Map<string, DiffStat>();
|
|
25
|
+
|
|
26
|
+
const gitEnv = (): NodeJS.ProcessEnv => ({ PATH: process.env.PATH ?? "", HOME: process.env.HOME ?? "", LC_ALL: "C", GIT_TERMINAL_PROMPT: "0", GIT_OPTIONAL_LOCKS: "0" });
|
|
27
|
+
|
|
28
|
+
/** Parses `git diff --numstat` output; a binary file ("-\t-") keeps null counts. */
|
|
29
|
+
export function parseNumstat(out: string): DiffStat["files"] {
|
|
30
|
+
const files: DiffStat["files"] = [];
|
|
31
|
+
for (const line of out.split("\n")) {
|
|
32
|
+
const m = /^(\d+|-)\t(\d+|-)\t(.+)$/.exec(line);
|
|
33
|
+
if (!m) continue;
|
|
34
|
+
files.push({ path: m[3]!, added: m[1] === "-" ? null : Number(m[1]), removed: m[2] === "-" ? null : Number(m[2]) });
|
|
35
|
+
}
|
|
36
|
+
return files;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readReceipt(runDir: string): { base_sha?: unknown; head_sha?: unknown } | null {
|
|
40
|
+
let fd: number;
|
|
41
|
+
try { fd = openSync(join(runDir, "receipt.json"), "r"); } catch { return null; }
|
|
42
|
+
try {
|
|
43
|
+
const st = fstatSync(fd);
|
|
44
|
+
if (!st.isFile() || st.size > MAX_RECEIPT_BYTES) return null;
|
|
45
|
+
const buf = Buffer.alloc(st.size);
|
|
46
|
+
readSync(fd, buf, 0, st.size, 0);
|
|
47
|
+
const j = JSON.parse(buf.toString("utf8"));
|
|
48
|
+
return j && typeof j === "object" ? j : null;
|
|
49
|
+
} catch { return null; } finally { closeSync(fd); }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Null when the run is unknown, has no local repo or receipt, has no valid shas, or git cannot answer. Only successes are cached (a finished run's range never changes). */
|
|
53
|
+
export function diffStatFor(db: Db, source: string, run: string): DiffStat | null {
|
|
54
|
+
if (!ID.test(source) || !ID.test(run) || source.includes("..") || run.includes("..")) return null;
|
|
55
|
+
const key = `${source}/${run}`;
|
|
56
|
+
const hit = cache.get(key);
|
|
57
|
+
if (hit) return hit;
|
|
58
|
+
const repo = db.select({ p: localRepos.realpath }).from(localRepos).where(and(eq(localRepos.sourceId, source))).get();
|
|
59
|
+
if (!repo) return null;
|
|
60
|
+
let repoReal: string, runsRoot: string, runDir: string;
|
|
61
|
+
try {
|
|
62
|
+
repoReal = realpathSync(repo.p);
|
|
63
|
+
runsRoot = realpathSync(join(repoReal, ".loki", "runs"));
|
|
64
|
+
runDir = realpathSync(join(runsRoot, run));
|
|
65
|
+
} catch { return null; }
|
|
66
|
+
if (runDir !== join(runsRoot, run) || !runDir.startsWith(runsRoot + sep)) return null;
|
|
67
|
+
const r = readReceipt(runDir);
|
|
68
|
+
const base = typeof r?.base_sha === "string" ? r.base_sha : "", head = typeof r?.head_sha === "string" ? r.head_sha : "";
|
|
69
|
+
if (!SHA.test(base) || !SHA.test(head)) return null;
|
|
70
|
+
try {
|
|
71
|
+
const out = execFileSync("git", ["-C", repoReal, "-c", "core.quotePath=false", "diff", "--numstat", "--no-renames", `${base}..${head}`, "--"], { env: gitEnv(), timeout: GIT_TIMEOUT_MS, maxBuffer: 8 * 1024 * 1024, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
72
|
+
const files = parseNumstat(out);
|
|
73
|
+
const stat: DiffStat = { base, head, files, added: files.reduce((n, f) => n + (f.added ?? 0), 0), removed: files.reduce((n, f) => n + (f.removed ?? 0), 0), source: "git" };
|
|
74
|
+
cache.set(key, stat);
|
|
75
|
+
return stat;
|
|
76
|
+
} catch { return null; }
|
|
77
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// local_repos: the server-side map from source_id to a real repo path. Filled only by local discovery (never by /v1/ingest);
|
|
2
2
|
// the path never leaves the server. The API exposes display names only.
|
|
3
|
+
import { basename, resolve } from "node:path";
|
|
3
4
|
import type { Hono } from "hono";
|
|
4
5
|
import type { Db } from "../db/migrate.ts";
|
|
5
6
|
import { localRepos } from "../db/schema.ts";
|
|
@@ -22,6 +23,8 @@ export function repoNames(db: Db): string[] {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/** GET /v1/repos. `act` is the loopback-only router; `peerIsLoopback` checks the real socket address. */
|
|
25
|
-
export function mountRepos(act: Hono, db: Db, peerIsLoopback: (c: Parameters<Parameters<Hono["get"]>[1]>[0]) => boolean): void {
|
|
26
|
-
|
|
26
|
+
export function mountRepos(act: Hono, db: Db, peerIsLoopback: (c: Parameters<Parameters<Hono["get"]>[1]>[0]) => boolean, repoDir?: string): void {
|
|
27
|
+
// default_repo is the folder name of the directory the service was launched from (what a run with no repo chip uses); the path itself is never sent.
|
|
28
|
+
const defaultRepo = repoDir ? basename(resolve(repoDir)) || null : null;
|
|
29
|
+
act.get("/v1/repos", (c) => peerIsLoopback(c) ? c.json({ repos: repoNames(db), default_repo: defaultRepo }) : c.json({ error: "loopback only" }, 403));
|
|
27
30
|
}
|
|
@@ -46,7 +46,7 @@ export interface RouteCtx {
|
|
|
46
46
|
export const routeModules: ReadonlyArray<(ctx: RouteCtx) => void> = [artifacts, stream, start, control, stats, cost, costLedger, fleet, metrics, checkpoints, memory, sessionControl, config, providers, verify, integrations, notify, audit, mergeRisk];
|
|
47
47
|
|
|
48
48
|
export function registerRoutes(ctx: RouteCtx): void {
|
|
49
|
-
mountRepos(ctx.act, ctx.db, ctx.peerIsLoopback);
|
|
49
|
+
mountRepos(ctx.act, ctx.db, ctx.peerIsLoopback, ctx.repoDir);
|
|
50
50
|
for (const m of routeModules) m(ctx);
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -4,6 +4,7 @@ import type { EventEnvelope } from "../../../../loki-ts/src/engine10/types.ts";
|
|
|
4
4
|
import type { Db } from "../db/migrate.ts";
|
|
5
5
|
import { events, runs } from "../db/schema.ts";
|
|
6
6
|
import { blockedQuestion } from "./answer.ts";
|
|
7
|
+
import { diffStatFor } from "./diffstat.ts";
|
|
7
8
|
import { effectiveVerdict, pubkeysFromEnv, sealedPrefix, SUCCESS_VERDICTS, verifyRunIntegrity } from "./integrity.ts";
|
|
8
9
|
|
|
9
10
|
const str = (x: unknown): string | null => (typeof x === "string" ? x : null);
|
|
@@ -65,7 +66,8 @@ export function taskTitle(db: Db, sourceId: string, runId: string): string | nul
|
|
|
65
66
|
// A run with no run.completed yet is running: add its live fields (stage, files) from the stored events.
|
|
66
67
|
const withLive = (db: Db, r: typeof runs.$inferSelect) => {
|
|
67
68
|
const p = { ...parseRun(r), title: taskTitle(db, r.sourceId, r.runId) };
|
|
68
|
-
|
|
69
|
+
// A finished run's events carry no diff; its receipt's base and head give one (git numstat, null when unmeasurable).
|
|
70
|
+
return r.endedAt ? { ...p, current_stage: null, files_touched: [] as string[], diff_stat: diffStatFor(db, r.sourceId, r.runId) } : { ...p, ...liveInfo(loadEvents(db, r.sourceId, r.runId)), diff_stat: null };
|
|
69
71
|
};
|
|
70
72
|
|
|
71
73
|
/** Live view of one run from its events so far: current stage, files touched. Elapsed is computed per request. */
|