loki-mode 10.11.0 → 10.11.2

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 (75) hide show
  1. package/SKILL.md +2 -2
  2. package/VERSION +1 -1
  3. package/dashboard/__init__.py +1 -1
  4. package/docs/CLI-REFERENCE.md +1 -1
  5. package/docs/INSTALLATION.md +1 -1
  6. package/docs/v10/BOARD.md +3 -3
  7. package/docs/v10/DEPS.md +3 -0
  8. package/docs/v10/FAILURE-CLASSES.md +17 -0
  9. package/docs/v10/PROGRESS.md +6 -0
  10. package/loki-ts/dist/loki.js +640 -639
  11. package/mcp/__init__.py +1 -1
  12. package/package.json +1 -1
  13. package/packages/control-plane/dist/server.js +214 -209
  14. package/packages/control-plane/package.json +3 -1
  15. package/packages/control-plane/src/ask/tools.ts +111 -0
  16. package/packages/control-plane/src/ask/tools_server.ts +46 -0
  17. package/packages/control-plane/src/server/routes/artifacts.ts +3 -3
  18. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  19. package/web-app/dist/assets/{AdminPage-qTfCHm93.js → AdminPage-CmgIgBFY.js} +1 -1
  20. package/web-app/dist/assets/{Avatar-V5Wfzf34.js → Avatar-Dm3ypJzh.js} +1 -1
  21. package/web-app/dist/assets/{Badge-B7fNko8Z.js → Badge-0siYUwAA.js} +1 -1
  22. package/web-app/dist/assets/{Button-C_3eInAe.js → Button-EMKQXvZe.js} +1 -1
  23. package/web-app/dist/assets/{CockpitPage-Ir2wag2q.js → CockpitPage-Z60eDvJ9.js} +1 -1
  24. package/web-app/dist/assets/{ComparePage-qbC63b0U.js → ComparePage-D4L7nViy.js} +1 -1
  25. package/web-app/dist/assets/{ErrorBoundary-BTEqFTCi.js → ErrorBoundary--v9DugrJ.js} +1 -1
  26. package/web-app/dist/assets/{EvidenceReceiptPanel-CcICm1gj.js → EvidenceReceiptPanel-CA866XsM.js} +1 -1
  27. package/web-app/dist/assets/{GitHubIssuesPanel-CXCWYQqX.js → GitHubIssuesPanel-Bi6FJmR2.js} +1 -1
  28. package/web-app/dist/assets/{GitHubPRsPanel-DmzhZYpS.js → GitHubPRsPanel-CgSfcWOg.js} +1 -1
  29. package/web-app/dist/assets/{HomePage-BRPspBy7.js → HomePage-BxQtk9Ec.js} +2 -2
  30. package/web-app/dist/assets/{LoginPage-C1Nb7Vf9.js → LoginPage-CCSxWlFA.js} +1 -1
  31. package/web-app/dist/assets/{MagicPage-CeXc60VN.js → MagicPage-B4eYm0jJ.js} +1 -1
  32. package/web-app/dist/assets/{MetricsPage-MuDDdqNu.js → MetricsPage-Bp4Hh0p2.js} +1 -1
  33. package/web-app/dist/assets/{NotFoundPage-8iOaj85A.js → NotFoundPage-DTXk_JK4.js} +1 -1
  34. package/web-app/dist/assets/{ProjectPage-DOKcPhxr.js → ProjectPage-6S1v0GHS.js} +3 -3
  35. package/web-app/dist/assets/{ProjectsPage-CNKaqvuO.js → ProjectsPage-9foqEwG2.js} +1 -1
  36. package/web-app/dist/assets/{SettingsPage-uZT6dOT1.js → SettingsPage-CWOG_hhD.js} +1 -1
  37. package/web-app/dist/assets/{ShowcasePage-CHzrwMZ3.js → ShowcasePage-Du9LYlao.js} +1 -1
  38. package/web-app/dist/assets/{SystemSettingsPage-DMHj5Xub.js → SystemSettingsPage-BQconaDX.js} +1 -1
  39. package/web-app/dist/assets/{TeamsPage-IGY1oZ2o.js → TeamsPage-_NsqtVmC.js} +1 -1
  40. package/web-app/dist/assets/{TemplatesPage-Byxc8XyZ.js → TemplatesPage-DyTEGBpR.js} +1 -1
  41. package/web-app/dist/assets/{TerminalOutput-CRamR4sR.js → TerminalOutput-XHOYTpky.js} +1 -1
  42. package/web-app/dist/assets/{activity-DQfL1H8b.js → activity-B9KiMdEs.js} +1 -1
  43. package/web-app/dist/assets/{bell-CVksERF2.js → bell-Oe4lG6kK.js} +1 -1
  44. package/web-app/dist/assets/{bot-jSx_C-35.js → bot-D86Qmxfy.js} +1 -1
  45. package/web-app/dist/assets/{check-o3bOhMvE.js → check-BvW6_ni0.js} +1 -1
  46. package/web-app/dist/assets/{chevron-left-DBhM8Y2E.js → chevron-left-DYA62-3g.js} +1 -1
  47. package/web-app/dist/assets/{circle-alert-vCzO_mB8.js → circle-alert-CHW8ye4p.js} +1 -1
  48. package/web-app/dist/assets/{clock-Bj125ZaJ.js → clock-QbQ2XfbM.js} +1 -1
  49. package/web-app/dist/assets/{cloud-DpLr6uQJ.js → cloud-ylzrY7eL.js} +1 -1
  50. package/web-app/dist/assets/{code-xml-VuYrHYvT.js → code-xml-DdKKw2Y9.js} +1 -1
  51. package/web-app/dist/assets/{database-DBmPbwaG.js → database-BLY_OWs3.js} +1 -1
  52. package/web-app/dist/assets/{dollar-sign-DEajFwH1.js → dollar-sign-CsFIa1e2.js} +1 -1
  53. package/web-app/dist/assets/{file-code-corner-BidTyjV_.js → file-code-corner-Df4nyrCW.js} +1 -1
  54. package/web-app/dist/assets/{file-plus-DJ4dSvxw.js → file-plus-_MWEY2WY.js} +1 -1
  55. package/web-app/dist/assets/{globe-BnGEwsOq.js → globe-Dpc7QU9m.js} +1 -1
  56. package/web-app/dist/assets/{hammer-KCPviXRE.js → hammer-B-PldT0c.js} +1 -1
  57. package/web-app/dist/assets/{index-CdK6NQG3.js → index-gq0R3j-o.js} +3 -3
  58. package/web-app/dist/assets/{layers-7G4hsaDk.js → layers-BjcNwHLZ.js} +1 -1
  59. package/web-app/dist/assets/{loader-circle-CWVxvRmJ.js → loader-circle-CoTgcqkV.js} +1 -1
  60. package/web-app/dist/assets/{lock-BEH3dNM0.js → lock-JEI8O6TS.js} +1 -1
  61. package/web-app/dist/assets/{package-DvijfmPC.js → package-CXWkNS40.js} +1 -1
  62. package/web-app/dist/assets/{plus-CEwJzXGx.js → plus-CRK4U5b0.js} +1 -1
  63. package/web-app/dist/assets/{refresh-cw-Ct7rMsNS.js → refresh-cw-CrzWr4cw.js} +1 -1
  64. package/web-app/dist/assets/{rotate-ccw-CoTyya_B.js → rotate-ccw-DV7UZyUw.js} +1 -1
  65. package/web-app/dist/assets/{scroll-text-CbsOnRXr.js → scroll-text-CXEqRrXU.js} +1 -1
  66. package/web-app/dist/assets/{server-MUPVBbag.js → server-CDFfFP10.js} +1 -1
  67. package/web-app/dist/assets/{shield-alert-CWwdqRUv.js → shield-alert-B5EC_m6_.js} +1 -1
  68. package/web-app/dist/assets/{thumbs-up-CPVKKyLS.js → thumbs-up-BeLDemd2.js} +1 -1
  69. package/web-app/dist/assets/{trash-2-BqzCzljk.js → trash-2-B7kS86-0.js} +1 -1
  70. package/web-app/dist/assets/{trending-down-DAo5VuPc.js → trending-down-BGvuLayo.js} +1 -1
  71. package/web-app/dist/assets/{trending-up-BCUggli9.js → trending-up-BkPM2tiK.js} +1 -1
  72. package/web-app/dist/assets/{upload-iQijlCY2.js → upload-CXVI0GRU.js} +1 -1
  73. package/web-app/dist/assets/{usePolling-B6kojZR_.js → usePolling-CMGMVsMA.js} +1 -1
  74. package/web-app/dist/assets/{user-DR3XkyHZ.js → user-DPcXNtfj.js} +1 -1
  75. package/web-app/dist/index.html +1 -1
package/mcp/__init__.py CHANGED
@@ -75,4 +75,4 @@ try:
75
75
  except ImportError:
76
76
  __all__ = ['mcp']
77
77
 
78
- __version__ = '10.11.0'
78
+ __version__ = '10.11.2'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "loki-mode",
3
3
  "mcpName": "io.github.asklokesh/loki-mode",
4
- "version": "10.11.0",
4
+ "version": "10.11.2",
5
5
  "description": "An autonomous software factory that knows what it is supposed to deliver, and proves it did.",
6
6
  "keywords": [
7
7
  "agent",
@@ -16933,204 +16933,11 @@ function dropSet(repoDir, staged, preexistingDirty, env = process.env) {
16933
16933
  import { basename as basename9, dirname as dirname7 } from "path";
16934
16934
 
16935
16935
  // ../../loki-ts/src/engine10/testmap.ts
16936
- import { basename as basename8, extname, join as join24, relative as relative2, sep as sep9 } from "path";
16937
- var SKIP_DIRS = new Set([
16938
- ".git",
16939
- "node_modules",
16940
- "dist",
16941
- "build",
16942
- "target",
16943
- "coverage",
16944
- ".venv",
16945
- "venv",
16946
- "__pycache__",
16947
- ".tox",
16948
- ".pytest_cache",
16949
- ".loki",
16950
- ".next"
16951
- ]);
16952
- var JS_TEST_RE = /\.(test|spec)\.[cm]?[jt]sx?$/;
16953
- var PY_TEST_RE = /^(test_.+|.+_test)\.py$/;
16954
- var GO_TEST_RE = /_test\.go$/;
16955
- function isTestFile(rel) {
16956
- const name = basename8(rel);
16957
- return JS_TEST_RE.test(name) || PY_TEST_RE.test(name) || GO_TEST_RE.test(name);
16958
- }
16959
-
16960
- // ../../loki-ts/src/features/speed/unit_mode.ts
16961
- import { closeSync as closeSync6, constants, fstatSync as fstatSync4, lstatSync as lstatSync4, openSync as openSync6, readSync as readSync4, realpathSync as realpathSync12 } from "fs";
16962
- var MAX_SPEC_BYTES = 1024 * 1024;
16963
- var MAX_TOKEN_BUDGET = 1e9;
16964
- var norm2 = (p) => p.replace(/^\.\//, "");
16965
- var CTRL = /[\x00-\x1f\x7f]/;
16966
- var strs = (v) => Array.isArray(v) && v.every((x) => typeof x === "string" && !CTRL.test(x)) ? v.map(norm2) : null;
16967
- function readBounded(path) {
16968
- const l = lstatSync4(path);
16969
- if (l.isSymbolicLink() || !l.isFile() || l.size > MAX_SPEC_BYTES)
16970
- return null;
16971
- const fd = openSync6(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
16972
- try {
16973
- const st = fstatSync4(fd);
16974
- if (!st.isFile() || st.size > MAX_SPEC_BYTES)
16975
- return null;
16976
- const buf = Buffer.alloc(st.size + 1);
16977
- let n2 = 0;
16978
- for (;; ) {
16979
- const r = readSync4(fd, buf, n2, buf.length - n2, null);
16980
- if (r <= 0)
16981
- break;
16982
- n2 += r;
16983
- if (n2 >= buf.length)
16984
- return null;
16985
- }
16986
- return buf.subarray(0, n2).toString("utf8");
16987
- } finally {
16988
- closeSync6(fd);
16989
- }
16990
- }
16991
- function parseText(txt) {
16992
- try {
16993
- const j = JSON.parse(txt), ws = strs(j.writeSet), pack = strs(j.pack), tb = j.tokenBudget;
16994
- if (typeof j.id !== "string" || !j.id || !ws || ws.length === 0 || !pack || typeof tb !== "number" || !Number.isFinite(tb) || !(tb > 0) || tb > MAX_TOKEN_BUDGET)
16995
- return null;
16996
- return { id: j.id, writeSet: ws, pack, tokenBudget: tb };
16997
- } catch {
16998
- return null;
16999
- }
17000
- }
17001
- function parseSpec(path) {
17002
- try {
17003
- const txt = readBounded(path);
17004
- return txt === null ? null : parseText(txt);
17005
- } catch {
17006
- return null;
17007
- }
17008
- }
17009
- var memo = new Map;
17010
- function unitSpec(env = process.env) {
17011
- const path = env["LOKI_UNIT_SPEC"], frozen = env["LOKI_UNIT_SPEC_FROZEN"];
17012
- if (env["LOKI_SPEED"] === "0" || !path)
17013
- return null;
17014
- if (frozen !== undefined)
17015
- return frozen.length <= MAX_SPEC_BYTES ? parseText(frozen) : null;
17016
- if (!memo.has(path))
17017
- memo.set(path, parseSpec(path));
17018
- return memo.get(path) ?? null;
17019
- }
17020
- var inWriteSet = (spec, f) => spec.writeSet.some((w) => w.endsWith("/") ? f.startsWith(w) : f === w);
17021
- function unitFlags(staged, env = process.env, pre = {}) {
17022
- const s = unitSpec(env);
17023
- return s ? staged.filter(({ f }) => !inWriteSet(s, f) && !Object.hasOwn(pre, f)).map(({ f }) => f) : null;
17024
- }
17025
-
17026
- // ../../loki-ts/src/e10ext/scope.ts
17027
- var SCOPE_UNDETERMINED = "scope not determined; all edits committed";
17028
- var outsideNote = (f) => `outside stated scope: ${f}`;
17029
- var esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
17030
- var mentions = (text2, name) => new RegExp(`(?<![\\w./-])${esc(name)}(?![\\w-]|\\.\\w)`).test(text2);
17031
- var STOP = new Set(["the", "and", "for", "with", "that", "this", "all", "any", "src", "from", "into", "each", "across", "apply", "add", "use", "make"]);
17032
- var stem = (w) => w.length > 3 && w.endsWith("s") ? w.slice(0, -1) : w;
17033
- var surfaceTokens = (text2) => new Set((text2.toLowerCase().match(/[a-z][a-z0-9_-]{2,}/g) ?? []).filter((w) => !STOP.has(w)).map(stem));
17034
- var dirSegs = (f) => f.split("/").slice(0, -1).map((d) => stem(d.toLowerCase()));
17035
- function contractText(o) {
17036
- const c = o.intake?.contract_snapshot?.contract;
17037
- return Array.isArray(c?.criteria) ? c.criteria.map((x) => typeof x?.text === "string" ? x.text : "").join(`
17038
- `) : "";
17039
- }
17040
- function outsideScope(o, staged) {
17041
- const rel = Array.isArray(o.plan?.relevant_files) ? o.plan.relevant_files.filter((x) => typeof x === "string") : [];
17042
- const plan = typeof o.plan?.plan === "string" ? o.plan.plan : "";
17043
- if (rel.length === 0 && plan.trim() === "")
17044
- return null;
17045
- const text2 = `${plan}
17046
- ${typeof o.intake?.task === "string" ? o.intake.task : ""}
17047
- ${contractText(o)}`, pre = o.intake?.preexisting_dirty ?? {};
17048
- const toks = surfaceTokens(text2), plannedDirs = new Set(rel.map((r) => dirname7(r)).filter((d) => d !== "."));
17049
- return staged.filter(({ st, f }) => st !== "A" && !isTestFile(f) && !Object.hasOwn(pre, f) && !rel.includes(f) && !mentions(text2, f) && !mentions(text2, basename9(f)) && !plannedDirs.has(dirname7(f)) && !dirSegs(f).some((d) => toks.has(d))).map(({ f }) => f);
17050
- }
17051
- function flagOutsideScope(o, staged) {
17052
- const unit = unitFlags(staged, process.env, o.intake?.preexisting_dirty ?? {});
17053
- const un = outsideScope(o, staged), seen = new Set((unit ?? []).map(outsideNote));
17054
- return [...(unit ?? []).map(outsideNote), ...un ? un.map(outsideNote).filter((n2) => !seen.has(n2)) : staged.length > 0 ? [SCOPE_UNDETERMINED] : []];
17055
- }
17056
-
17057
- // ../../loki-ts/src/util/receipt_signer.ts
17058
- var RECEIPT_SIGNER_BASENAME = "receipt-ed25519.pem";
17059
-
17060
- // ../../loki-ts/src/util/shell.ts
17061
- var MAX_STDOUT_BYTES = 16 * 1024 * 1024;
17062
- async function readStreamCapped(stream, maxBytes = MAX_STDOUT_BYTES) {
17063
- const reader = stream.getReader();
17064
- const decoder = new TextDecoder;
17065
- let out2 = "";
17066
- let total = 0;
17067
- try {
17068
- while (total < maxBytes) {
17069
- const { done, value } = await reader.read();
17070
- if (done)
17071
- break;
17072
- if (!value)
17073
- continue;
17074
- total += value.byteLength;
17075
- if (total > maxBytes) {
17076
- const keep = value.byteLength - (total - maxBytes);
17077
- out2 += decoder.decode(value.subarray(0, keep), { stream: true });
17078
- break;
17079
- }
17080
- out2 += decoder.decode(value, { stream: true });
17081
- }
17082
- out2 += decoder.decode();
17083
- } finally {
17084
- try {
17085
- await reader.cancel();
17086
- } catch {}
17087
- reader.releaseLock();
17088
- }
17089
- return out2;
17090
- }
17091
- async function run(argv, opts = {}) {
17092
- const proc = Bun.spawn({
17093
- cmd: [...argv],
17094
- stdout: "pipe",
17095
- stderr: "pipe",
17096
- env: opts.env ? { ...process.env, ...opts.env } : process.env,
17097
- cwd: opts.cwd
17098
- });
17099
- let timer;
17100
- let killTimer;
17101
- if (opts.timeoutMs && opts.timeoutMs > 0) {
17102
- timer = setTimeout(() => {
17103
- try {
17104
- proc.kill("SIGTERM");
17105
- } catch {}
17106
- killTimer = setTimeout(() => {
17107
- try {
17108
- proc.kill("SIGKILL");
17109
- } catch {}
17110
- }, 2000);
17111
- }, opts.timeoutMs);
17112
- }
17113
- try {
17114
- const [stdout, stderr, exitCode] = await Promise.all([
17115
- readStreamCapped(proc.stdout),
17116
- new Response(proc.stderr).text(),
17117
- proc.exited
17118
- ]);
17119
- return { stdout, stderr, exitCode };
17120
- } finally {
17121
- if (timer)
17122
- clearTimeout(timer);
17123
- if (killTimer)
17124
- clearTimeout(killTimer);
17125
- }
17126
- }
17127
-
17128
- // ../../loki-ts/src/features/visual_evidence.ts
17129
- import { spawn as spawn5 } from "child_process";
16936
+ import { basename as basename8, extname, join as join26, relative as relative2, sep as sep10 } from "path";
17130
16937
 
17131
16938
  // ../../loki-ts/src/project_model/discover.ts
17132
16939
  import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync19, rmSync as rmSync4, writeFileSync as writeFileSync7 } from "fs";
17133
- import { join as join26 } from "path";
16940
+ import { join as join25 } from "path";
17134
16941
 
17135
16942
  // ../../loki-ts/src/engine10/types.ts
17136
16943
  var STAGE_BUDGETS = {
@@ -17147,23 +16954,23 @@ var STAGE_BUDGETS = {
17147
16954
  };
17148
16955
 
17149
16956
  // ../../loki-ts/src/project_model/schema.ts
17150
- import { existsSync as existsSync13, lstatSync as lstatSync5, realpathSync as realpathSync13, statSync as statSync10 } from "fs";
17151
- import { isAbsolute, join as join25, normalize, sep as sep10 } from "path";
16957
+ import { existsSync as existsSync13, lstatSync as lstatSync4, realpathSync as realpathSync12, statSync as statSync10 } from "fs";
16958
+ import { isAbsolute, join as join24, normalize, sep as sep9 } from "path";
17152
16959
  var PROJECT_MODEL_SCHEMA = "loki.v10.project/1";
17153
16960
  var COMMAND_KINDS = ["test", "lint", "build", "start"];
17154
16961
  var isRec = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
17155
16962
  var isStr = (v) => typeof v === "string" && v.trim() !== "";
17156
16963
  function safeRel(p) {
17157
- const n2 = normalize(p.trim()).split(sep10).join("/").replace(/\/$/, "");
16964
+ const n2 = normalize(p.trim()).split(sep9).join("/").replace(/\/$/, "");
17158
16965
  if (n2 === "" || isAbsolute(n2) || n2 === ".." || n2.startsWith("../"))
17159
16966
  return null;
17160
16967
  return n2;
17161
16968
  }
17162
16969
  function insideRepo(repoDir, rel) {
17163
16970
  try {
17164
- const real2 = realpathSync13(join25(repoDir, rel));
17165
- const base = realpathSync13(repoDir);
17166
- return real2 === base || real2.startsWith(base + sep10);
16971
+ const real2 = realpathSync12(join24(repoDir, rel));
16972
+ const base = realpathSync12(repoDir);
16973
+ return real2 === base || real2.startsWith(base + sep9);
17167
16974
  } catch {
17168
16975
  return false;
17169
16976
  }
@@ -17174,7 +16981,7 @@ function checkCitation(repoDir, raw2) {
17174
16981
  const rel = safeRel(raw2.replace(/:\d+(?:-\d+)?$/, ""));
17175
16982
  if (rel === null)
17176
16983
  return `citation "${raw2}" is not a repo-relative path`;
17177
- const abs = join25(repoDir, rel);
16984
+ const abs = join24(repoDir, rel);
17178
16985
  if (!existsSync13(abs) || !statSync10(abs).isFile() || !insideRepo(repoDir, rel))
17179
16986
  return `citation "${raw2}" names a file that does not exist`;
17180
16987
  return null;
@@ -17186,7 +16993,7 @@ function checkFingerprint(repoDir, raw2) {
17186
16993
  if (rel === null)
17187
16994
  return `fingerprint "${raw2}" is not a repo-relative path`;
17188
16995
  try {
17189
- if (!lstatSync5(join25(repoDir, rel)).isFile() || !insideRepo(repoDir, rel))
16996
+ if (!lstatSync4(join24(repoDir, rel)).isFile() || !insideRepo(repoDir, rel))
17190
16997
  return `fingerprint "${raw2}" is not a regular file inside the repo`;
17191
16998
  } catch {
17192
16999
  return `fingerprint "${raw2}" names a file that does not exist`;
@@ -17214,7 +17021,7 @@ function checkDir(repoDir, v, where, errs) {
17214
17021
  errs.push(`${where}: must be a repo-relative directory ("." for the repo root)`);
17215
17022
  return ".";
17216
17023
  }
17217
- const abs = join25(repoDir, rel);
17024
+ const abs = join24(repoDir, rel);
17218
17025
  if (!existsSync13(abs) || !statSync10(abs).isDirectory() || !insideRepo(repoDir, rel))
17219
17026
  errs.push(`${where}: directory "${rel}" does not exist`);
17220
17027
  return rel;
@@ -17331,7 +17138,7 @@ var PROJECT_FILE = ".loki/project.json";
17331
17138
  var UNKNOWN_TTL_MS = 60 * 60 * 1000;
17332
17139
  function loadCached(repoDir, now = Date.now()) {
17333
17140
  try {
17334
- return parseCached(repoDir, JSON.parse(readFileSync19(join26(repoDir, PROJECT_FILE), "utf8")), now);
17141
+ return parseCached(repoDir, JSON.parse(readFileSync19(join25(repoDir, PROJECT_FILE), "utf8")), now);
17335
17142
  } catch {
17336
17143
  return null;
17337
17144
  }
@@ -17403,7 +17210,205 @@ function isMultiRoot(api) {
17403
17210
  return !!api && api.known() && api.packages().some((p) => p.root !== ".");
17404
17211
  }
17405
17212
 
17213
+ // ../../loki-ts/src/project_model/scope.ts
17214
+ var apis = new WeakMap;
17215
+ var imports = new WeakMap;
17216
+ var dropped = new WeakMap;
17217
+
17218
+ // ../../loki-ts/src/engine10/testmap.ts
17219
+ var SKIP_DIRS = new Set([
17220
+ ".git",
17221
+ "node_modules",
17222
+ "dist",
17223
+ "build",
17224
+ "target",
17225
+ "coverage",
17226
+ ".venv",
17227
+ "venv",
17228
+ "__pycache__",
17229
+ ".tox",
17230
+ ".pytest_cache",
17231
+ ".loki",
17232
+ ".next"
17233
+ ]);
17234
+ var JS_TEST_RE = /\.(test|spec)\.[cm]?[jt]sx?$/;
17235
+ var PY_TEST_RE = /^(test_.+|.+_test)\.py$/;
17236
+ var GO_TEST_RE = /_test\.go$/;
17237
+ function isTestFile(rel) {
17238
+ const name = basename8(rel);
17239
+ return JS_TEST_RE.test(name) || PY_TEST_RE.test(name) || GO_TEST_RE.test(name);
17240
+ }
17241
+
17242
+ // ../../loki-ts/src/features/speed/unit_mode.ts
17243
+ import { closeSync as closeSync6, constants, fstatSync as fstatSync4, lstatSync as lstatSync5, openSync as openSync6, readSync as readSync4, realpathSync as realpathSync13 } from "fs";
17244
+ var MAX_SPEC_BYTES = 1024 * 1024;
17245
+ var MAX_TOKEN_BUDGET = 1e9;
17246
+ var norm2 = (p) => p.replace(/^\.\//, "");
17247
+ var CTRL = /[\x00-\x1f\x7f]/;
17248
+ var strs = (v) => Array.isArray(v) && v.every((x) => typeof x === "string" && !CTRL.test(x)) ? v.map(norm2) : null;
17249
+ function readBounded(path) {
17250
+ const l = lstatSync5(path);
17251
+ if (l.isSymbolicLink() || !l.isFile() || l.size > MAX_SPEC_BYTES)
17252
+ return null;
17253
+ const fd = openSync6(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
17254
+ try {
17255
+ const st = fstatSync4(fd);
17256
+ if (!st.isFile() || st.size > MAX_SPEC_BYTES)
17257
+ return null;
17258
+ const buf = Buffer.alloc(st.size + 1);
17259
+ let n2 = 0;
17260
+ for (;; ) {
17261
+ const r = readSync4(fd, buf, n2, buf.length - n2, null);
17262
+ if (r <= 0)
17263
+ break;
17264
+ n2 += r;
17265
+ if (n2 >= buf.length)
17266
+ return null;
17267
+ }
17268
+ return buf.subarray(0, n2).toString("utf8");
17269
+ } finally {
17270
+ closeSync6(fd);
17271
+ }
17272
+ }
17273
+ function parseText(txt) {
17274
+ try {
17275
+ const j = JSON.parse(txt), ws = strs(j.writeSet), pack = strs(j.pack), tb = j.tokenBudget;
17276
+ if (typeof j.id !== "string" || !j.id || !ws || ws.length === 0 || !pack || typeof tb !== "number" || !Number.isFinite(tb) || !(tb > 0) || tb > MAX_TOKEN_BUDGET)
17277
+ return null;
17278
+ return { id: j.id, writeSet: ws, pack, tokenBudget: tb };
17279
+ } catch {
17280
+ return null;
17281
+ }
17282
+ }
17283
+ function parseSpec(path) {
17284
+ try {
17285
+ const txt = readBounded(path);
17286
+ return txt === null ? null : parseText(txt);
17287
+ } catch {
17288
+ return null;
17289
+ }
17290
+ }
17291
+ var memo = new Map;
17292
+ function unitSpec(env = process.env) {
17293
+ const path = env["LOKI_UNIT_SPEC"], frozen = env["LOKI_UNIT_SPEC_FROZEN"];
17294
+ if (env["LOKI_SPEED"] === "0" || !path)
17295
+ return null;
17296
+ if (frozen !== undefined)
17297
+ return frozen.length <= MAX_SPEC_BYTES ? parseText(frozen) : null;
17298
+ if (!memo.has(path))
17299
+ memo.set(path, parseSpec(path));
17300
+ return memo.get(path) ?? null;
17301
+ }
17302
+ var inWriteSet = (spec, f) => spec.writeSet.some((w) => w.endsWith("/") ? f.startsWith(w) : f === w);
17303
+ function unitFlags(staged, env = process.env, pre = {}) {
17304
+ const s = unitSpec(env);
17305
+ return s ? staged.filter(({ f }) => !inWriteSet(s, f) && !Object.hasOwn(pre, f)).map(({ f }) => f) : null;
17306
+ }
17307
+
17308
+ // ../../loki-ts/src/e10ext/scope.ts
17309
+ var SCOPE_UNDETERMINED = "scope not determined; all edits committed";
17310
+ var outsideNote = (f) => `outside stated scope: ${f}`;
17311
+ var esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
17312
+ var mentions = (text2, name) => new RegExp(`(?<![\\w./-])${esc(name)}(?![\\w-]|\\.\\w)`).test(text2);
17313
+ var STOP = new Set(["the", "and", "for", "with", "that", "this", "all", "any", "src", "from", "into", "each", "across", "apply", "add", "use", "make"]);
17314
+ var stem = (w) => w.length > 3 && w.endsWith("s") ? w.slice(0, -1) : w;
17315
+ var surfaceTokens = (text2) => new Set((text2.toLowerCase().match(/[a-z][a-z0-9_-]{2,}/g) ?? []).filter((w) => !STOP.has(w)).map(stem));
17316
+ var dirSegs = (f) => f.split("/").slice(0, -1).map((d) => stem(d.toLowerCase()));
17317
+ function contractText(o) {
17318
+ const c = o.intake?.contract_snapshot?.contract;
17319
+ return Array.isArray(c?.criteria) ? c.criteria.map((x) => typeof x?.text === "string" ? x.text : "").join(`
17320
+ `) : "";
17321
+ }
17322
+ function outsideScope(o, staged) {
17323
+ const rel = Array.isArray(o.plan?.relevant_files) ? o.plan.relevant_files.filter((x) => typeof x === "string") : [];
17324
+ const plan = typeof o.plan?.plan === "string" ? o.plan.plan : "";
17325
+ if (rel.length === 0 && plan.trim() === "")
17326
+ return null;
17327
+ const text2 = `${plan}
17328
+ ${typeof o.intake?.task === "string" ? o.intake.task : ""}
17329
+ ${contractText(o)}`, pre = o.intake?.preexisting_dirty ?? {};
17330
+ const toks = surfaceTokens(text2), plannedDirs = new Set(rel.map((r) => dirname7(r)).filter((d) => d !== "."));
17331
+ return staged.filter(({ st, f }) => st !== "A" && !isTestFile(f) && !Object.hasOwn(pre, f) && !rel.includes(f) && !mentions(text2, f) && !mentions(text2, basename9(f)) && !plannedDirs.has(dirname7(f)) && !dirSegs(f).some((d) => toks.has(d))).map(({ f }) => f);
17332
+ }
17333
+ function flagOutsideScope(o, staged) {
17334
+ const unit = unitFlags(staged, process.env, o.intake?.preexisting_dirty ?? {});
17335
+ const un = outsideScope(o, staged), seen = new Set((unit ?? []).map(outsideNote));
17336
+ return [...(unit ?? []).map(outsideNote), ...un ? un.map(outsideNote).filter((n2) => !seen.has(n2)) : staged.length > 0 ? [SCOPE_UNDETERMINED] : []];
17337
+ }
17338
+
17339
+ // ../../loki-ts/src/util/receipt_signer.ts
17340
+ var RECEIPT_SIGNER_BASENAME = "receipt-ed25519.pem";
17341
+
17342
+ // ../../loki-ts/src/util/shell.ts
17343
+ var MAX_STDOUT_BYTES = 16 * 1024 * 1024;
17344
+ async function readStreamCapped(stream, maxBytes = MAX_STDOUT_BYTES) {
17345
+ const reader = stream.getReader();
17346
+ const decoder = new TextDecoder;
17347
+ let out2 = "";
17348
+ let total = 0;
17349
+ try {
17350
+ while (total < maxBytes) {
17351
+ const { done, value } = await reader.read();
17352
+ if (done)
17353
+ break;
17354
+ if (!value)
17355
+ continue;
17356
+ total += value.byteLength;
17357
+ if (total > maxBytes) {
17358
+ const keep = value.byteLength - (total - maxBytes);
17359
+ out2 += decoder.decode(value.subarray(0, keep), { stream: true });
17360
+ break;
17361
+ }
17362
+ out2 += decoder.decode(value, { stream: true });
17363
+ }
17364
+ out2 += decoder.decode();
17365
+ } finally {
17366
+ try {
17367
+ await reader.cancel();
17368
+ } catch {}
17369
+ reader.releaseLock();
17370
+ }
17371
+ return out2;
17372
+ }
17373
+ async function run(argv, opts = {}) {
17374
+ const proc = Bun.spawn({
17375
+ cmd: [...argv],
17376
+ stdout: "pipe",
17377
+ stderr: "pipe",
17378
+ env: opts.env ? { ...process.env, ...opts.env } : process.env,
17379
+ cwd: opts.cwd
17380
+ });
17381
+ let timer;
17382
+ let killTimer;
17383
+ if (opts.timeoutMs && opts.timeoutMs > 0) {
17384
+ timer = setTimeout(() => {
17385
+ try {
17386
+ proc.kill("SIGTERM");
17387
+ } catch {}
17388
+ killTimer = setTimeout(() => {
17389
+ try {
17390
+ proc.kill("SIGKILL");
17391
+ } catch {}
17392
+ }, 2000);
17393
+ }, opts.timeoutMs);
17394
+ }
17395
+ try {
17396
+ const [stdout, stderr, exitCode] = await Promise.all([
17397
+ readStreamCapped(proc.stdout),
17398
+ new Response(proc.stderr).text(),
17399
+ proc.exited
17400
+ ]);
17401
+ return { stdout, stderr, exitCode };
17402
+ } finally {
17403
+ if (timer)
17404
+ clearTimeout(timer);
17405
+ if (killTimer)
17406
+ clearTimeout(killTimer);
17407
+ }
17408
+ }
17409
+
17406
17410
  // ../../loki-ts/src/features/visual_evidence.ts
17411
+ import { spawn as spawn5 } from "child_process";
17407
17412
  import { createHash as createHash6, randomBytes as randomBytes2 } from "crypto";
17408
17413
  import { existsSync as existsSync15, lstatSync as lstatSync6, mkdirSync as mkdirSync7, readdirSync as readdirSync9, readFileSync as readFileSync20, writeFileSync as writeFileSync8 } from "fs";
17409
17414
  import { createServer } from "net";
@@ -18087,7 +18092,7 @@ var commitStage = {
18087
18092
  return sat;
18088
18093
  if (drop.length > 0 && (await git(ctx, ["--literal-pathspecs", "reset", "-q", ctx.baseSha, "--", ...drop.map(({ f }) => f)])).code !== 0)
18089
18094
  return { status: "failed", data: {}, reason: "git reset failed" };
18090
- const dropped = new Set(drop.map(({ f }) => f)), notes = flagOutsideScope(ctx.outputs(), staged.filter(({ f }) => !dropped.has(f)));
18095
+ const dropped2 = new Set(drop.map(({ f }) => f)), notes = flagOutsideScope(ctx.outputs(), staged.filter(({ f }) => !dropped2.has(f)));
18091
18096
  if ((await git(ctx, ["diff", "--cached", "--quiet"])).code === 0)
18092
18097
  return { status: "completed", data: { committed: false, scope_notes: notes } };
18093
18098
  const title = (str3(ctx.outputs().intake?.title) ?? `run ${ctx.runId}`).split(`
@@ -18189,7 +18194,7 @@ var sealStage = {
18189
18194
  }
18190
18195
  if (!diffOk)
18191
18196
  notProven.add("diff not computed (git diff-tree failed)");
18192
- const dropped = new Set;
18197
+ const dropped2 = new Set;
18193
18198
  const tcs = o.verify?.["test_counts"];
18194
18199
  const cnt = (x) => {
18195
18200
  const c = x;
@@ -18204,7 +18209,7 @@ var sealStage = {
18204
18209
  const mine = assertDeltaNotes(ctx.repoDir, ctx.baseSha, head, t, str3(o.intake?.task) ?? "", cnt(tc?.b), cnt(tc?.h));
18205
18210
  if (!mine || mine.length !== vl.length || mine.some((n2) => !vl.includes(n2)))
18206
18211
  for (const v of vl)
18207
- dropped.add(v);
18212
+ dropped2.add(v);
18208
18213
  }
18209
18214
  for (const c of checks)
18210
18215
  if (c.result === "not_run")
@@ -18212,7 +18217,7 @@ var sealStage = {
18212
18217
  for (const f of strs2(o.verify?.flaky))
18213
18218
  notProven.add(`flaky test: ${f}`);
18214
18219
  for (const n2 of verifyNotProven)
18215
- if (!dropped.has(n2))
18220
+ if (!dropped2.has(n2))
18216
18221
  notProven.add(n2);
18217
18222
  for (const n2 of [...strs2(o.commit?.not_proven), ...strs2(o.commit?.scope_notes)])
18218
18223
  notProven.add(n2);
@@ -11,9 +11,11 @@
11
11
  "db:generate": "drizzle-kit generate --dialect sqlite --schema ./src/db/schema.ts --out ./drizzle"
12
12
  },
13
13
  "dependencies": {
14
+ "@modelcontextprotocol/sdk": "^1.29.0",
14
15
  "drizzle-orm": "^0.45.3",
15
16
  "hono": "^4.13.12",
16
- "yaml": "^2.9.1"
17
+ "yaml": "^2.9.1",
18
+ "zod": "^4.4.3"
17
19
  },
18
20
  "devDependencies": {
19
21
  "@playwright/test": "1.58.1",
@@ -0,0 +1,111 @@
1
+ // CP-ASK slice 2: read-only data tools for Ask Loki, as pure functions over the CP database.
2
+ // Nothing here writes, spawns a build, or returns a path, URL or token. Run, event and artifact text is untrusted data.
3
+ // Cost and events reuse the route handlers through an in-process Hono (no HTTP, no duplicated logic); the artifact
4
+ // allowlist and read cap come from routes/artifacts.ts.
5
+ import { realpathSync } from "node:fs";
6
+ import { join, sep } from "node:path";
7
+ import { eq } from "drizzle-orm";
8
+ import { Hono } from "hono";
9
+ import type { Db } from "../db/migrate.ts";
10
+ import { localRepos } from "../db/schema.ts";
11
+ import { mount as mountArtifacts, NAME, readCapped, safeId } from "../server/routes/artifacts.ts";
12
+ import { mount as mountCost } from "../server/routes/cost.ts";
13
+ import { computeStats } from "../server/routes/stats.ts";
14
+ import type { RouteCtx } from "../server/routes/index.ts";
15
+ import { repoNames } from "../server/repos.ts";
16
+ import { listRuns, runDetail } from "../server/runs.ts";
17
+
18
+ export type ToolResult = Record<string, unknown>;
19
+ export const TOOL_NAMES = ["runs_search", "run_get", "run_events", "run_artifact", "runs_compare", "stats", "cost", "repos_list"] as const;
20
+
21
+ const err = (error: string): ToolResult => ({ error });
22
+ const isoOk = (s: string | undefined) => s === undefined || (!Number.isNaN(Date.parse(s)) && /^\d{4}-\d{2}-\d{2}/.test(s));
23
+
24
+ function internal(db: Db): Hono {
25
+ const app = new Hono();
26
+ const ctx = { app, act: new Hono(), db } as unknown as RouteCtx;
27
+ mountCost(ctx);
28
+ mountArtifacts(ctx);
29
+ return app;
30
+ }
31
+
32
+ async function internalGet(db: Db, path: string): Promise<ToolResult> {
33
+ const r = await internal(db).request(path);
34
+ const body = (await r.json().catch(() => ({ error: "unreadable response" }))) as ToolResult;
35
+ return r.status === 200 ? body : err(typeof body.error === "string" ? body.error : `request failed (${r.status})`);
36
+ }
37
+
38
+ export function runsSearch(db: Db, a: { verdict?: string; repo?: string; since?: string; until?: string; group_id?: string; limit?: number; cursor?: string }): ToolResult {
39
+ if (!isoOk(a.since) || !isoOk(a.until)) return err("since and until must be ISO dates");
40
+ if (a.limit !== undefined && (!Number.isInteger(a.limit) || a.limit < 1 || a.limit > 200)) return err("limit must be 1..200");
41
+ return listRuns(db, a);
42
+ }
43
+
44
+ export function runGet(db: Db, a: { source_id: string; run_id: string }): ToolResult {
45
+ if (!safeId(a.source_id) || !safeId(a.run_id)) return err("invalid source_id or run_id");
46
+ return runDetail(db, a.source_id, a.run_id) ?? err("run not found");
47
+ }
48
+
49
+ export async function runEvents(db: Db, a: { source_id: string; run_id: string; after?: number; limit?: number }): Promise<ToolResult> {
50
+ if (!safeId(a.source_id) || !safeId(a.run_id)) return err("invalid source_id or run_id");
51
+ if (a.after !== undefined && (!Number.isInteger(a.after) || a.after < 0)) return err("after must be a non-negative integer");
52
+ if (a.limit !== undefined && (!Number.isInteger(a.limit) || a.limit < 1)) return err("limit must be a positive integer");
53
+ const q = new URLSearchParams();
54
+ if (a.after !== undefined) q.set("after", String(a.after));
55
+ if (a.limit !== undefined) q.set("limit", String(a.limit));
56
+ return internalGet(db, `/v1/runs/${a.source_id}/${a.run_id}/events?${q}`);
57
+ }
58
+
59
+ /** One allowlisted artifact of a run, read through the same containment the HTTP route uses. Binary (png) is refused. */
60
+ export function runArtifact(db: Db, a: { source_id: string; run_id: string; name: string }): ToolResult {
61
+ if (!safeId(a.source_id) || !safeId(a.run_id)) return err("invalid source_id or run_id");
62
+ if (typeof a.name !== "string" || !NAME.test(a.name)) return err("artifact is not on the allowlist");
63
+ if (a.name.endsWith(".png")) return err("binary artifacts are not readable through this tool");
64
+ if (!runDetail(db, a.source_id, a.run_id)) return err("run not found");
65
+ const repo = db.select({ p: localRepos.realpath }).from(localRepos).where(eq(localRepos.sourceId, a.source_id)).get();
66
+ if (!repo) return err("artifact not available");
67
+ try {
68
+ const runsRoot = realpathSync(join(repo.p, ".loki", "runs"));
69
+ const runDir = realpathSync(join(runsRoot, a.run_id));
70
+ if (runDir !== join(runsRoot, a.run_id)) return err("artifact not found");
71
+ const file = realpathSync(join(runDir, a.name));
72
+ if (!file.startsWith(runDir + sep)) return err("artifact not found");
73
+ const r = readCapped(file);
74
+ if (r.status !== 200) return err(r.status === 413 ? "artifact too large" : "artifact not found");
75
+ return { name: a.name, content: r.body.toString("utf8") };
76
+ } catch { return err("artifact not found"); }
77
+ }
78
+
79
+ /** Both runs side by side: ids, verdicts, costs, tokens and the cost delta (b minus a). */
80
+ export function runsCompare(db: Db, a: { source_a: string; run_a: string; source_b: string; run_b: string }): ToolResult {
81
+ const out: Record<string, unknown>[] = [];
82
+ for (const [s, r] of [[a.source_a, a.run_a], [a.source_b, a.run_b]] as const) {
83
+ const d = runGet(db, { source_id: s, run_id: r }) as Record<string, any>;
84
+ if (d.error) return err(`${s}/${r}: ${d.error}`);
85
+ out.push({
86
+ source_id: d.source_id, run_id: d.run_id, origin_repo: d.origin_repo, verdict: d.verdict, effective_verdict: d.effective_verdict,
87
+ status: d.status, provider: d.provider, model: d.model, cost_usd: d.cost_usd, partial_usd: d.partial_usd,
88
+ input_tokens: d.input_tokens, output_tokens: d.output_tokens, wall_s: d.wall_s, pr_url: d.pr_url, tampered: d.tampered,
89
+ });
90
+ }
91
+ const ca = out[0]?.cost_usd, cb = out[1]?.cost_usd;
92
+ const delta = typeof ca === "number" && typeof cb === "number" ? Math.round((cb - ca) * 1e6) / 1e6 : null;
93
+ return { runs: out, cost_delta_usd: delta };
94
+ }
95
+
96
+ export function stats(db: Db, a: { since?: string }): ToolResult {
97
+ if (!isoOk(a.since)) return err("since must be an ISO date");
98
+ return computeStats(db, a.since === undefined ? null : new Date(a.since).toISOString()) as unknown as ToolResult;
99
+ }
100
+
101
+ export function cost(db: Db, a: { group?: string; since?: string }): Promise<ToolResult> | ToolResult {
102
+ if (!isoOk(a.since)) return err("since must be an ISO date");
103
+ const q = new URLSearchParams({ group: a.group ?? "day" });
104
+ if (a.since) q.set("since", a.since);
105
+ return internalGet(db, `/v1/stats/cost?${q}`);
106
+ }
107
+
108
+ /** Display names only, never paths. */
109
+ export function reposList(db: Db): ToolResult {
110
+ return { repos: repoNames(db) };
111
+ }