loki-mode 7.61.0 → 7.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/app-runner.sh +70 -13
- package/autonomy/run.sh +9 -2
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/memory/cross_project.py +2 -2
- package/memory/knowledge_graph.py +2 -2
- package/memory/layers/index_layer.py +29 -13
- package/memory/layers/loader.py +34 -25
- package/memory/layers/timeline_layer.py +7 -5
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/src/audit/crosslink.js +89 -7
- package/templates/README.md +36 -15
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.
|
|
6
|
+
# Loki Mode v7.62.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -406,4 +406,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
406
406
|
|
|
407
407
|
---
|
|
408
408
|
|
|
409
|
-
**v7.
|
|
409
|
+
**v7.62.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.62.0
|
package/autonomy/app-runner.sh
CHANGED
|
@@ -1374,20 +1374,59 @@ app_runner_health_check() {
|
|
|
1374
1374
|
return 1
|
|
1375
1375
|
fi
|
|
1376
1376
|
|
|
1377
|
-
# For HTTP apps, try an HTTP health check
|
|
1377
|
+
# For HTTP apps, try an HTTP health check.
|
|
1378
1378
|
if [ -n "$_APP_RUNNER_PORT" ] && [ "$_APP_RUNNER_PORT" -gt 0 ] 2>/dev/null; then
|
|
1379
|
-
|
|
1379
|
+
# The health signal is "is the server answering HTTP at all", NOT "does /
|
|
1380
|
+
# return 2xx". Loki generates plenty of apps that legitimately serve a
|
|
1381
|
+
# non-2xx on the root path (an API-only FastAPI/Express backend 404s on
|
|
1382
|
+
# `/`, anything behind auth 401s). Those are serving correctly, so a
|
|
1383
|
+
# status-strict probe (curl -f, which fails on >=400) would mark a healthy
|
|
1384
|
+
# backend unhealthy and trigger a needless restart -> a restart storm /
|
|
1385
|
+
# false crash. What genuinely means "no longer serving" -- a hung event
|
|
1386
|
+
# loop, a deadlock, a wedged dev server -- is a connection that times out
|
|
1387
|
+
# or is refused, i.e. NO HTTP response at all. So we read the HTTP status
|
|
1388
|
+
# code: any code returned (2xx/3xx/4xx/5xx) means the server answered and
|
|
1389
|
+
# is alive; "000" is curl's sentinel for connect-failure/timeout/reset
|
|
1390
|
+
# and is the only thing we treat as a crash.
|
|
1391
|
+
# If curl is unavailable we cannot probe HTTP at all; fall back to the
|
|
1392
|
+
# old, more tolerant signal (PID alive == healthy) rather than declaring
|
|
1393
|
+
# every HTTP app wedged and triggering a restart storm. curl is the only
|
|
1394
|
+
# HTTP client this function uses.
|
|
1395
|
+
if ! command -v curl >/dev/null 2>&1; then
|
|
1380
1396
|
_write_health "true"
|
|
1381
1397
|
_write_app_state "running"
|
|
1382
1398
|
return 0
|
|
1383
|
-
|
|
1384
|
-
|
|
1399
|
+
fi
|
|
1400
|
+
# On connect-failure/timeout curl already prints "000" via %{http_code}
|
|
1401
|
+
# and exits non-zero; do NOT append our own "000" (a `|| echo 000` would
|
|
1402
|
+
# concatenate to "000000"). The trailing `|| true` swallows the non-zero
|
|
1403
|
+
# exit (matching this file's guarded command-substitution convention, e.g.
|
|
1404
|
+
# the _GIT_DIFF_HASH / port reads) so the watchdog never aborts under a
|
|
1405
|
+
# future `set -e`; the empty fallback then maps to "000".
|
|
1406
|
+
local _http_code
|
|
1407
|
+
_http_code=$(curl -s -o /dev/null -m 5 -w '%{http_code}' \
|
|
1408
|
+
"http://localhost:${_APP_RUNNER_PORT}/" 2>/dev/null || true)
|
|
1409
|
+
_http_code="${_http_code:-000}"
|
|
1410
|
+
if [ "$_http_code" != "000" ]; then
|
|
1385
1411
|
_write_health "true"
|
|
1412
|
+
_write_app_state "running"
|
|
1386
1413
|
return 0
|
|
1414
|
+
else
|
|
1415
|
+
# No HTTP response: the process is alive (kill -0 passed above) but is
|
|
1416
|
+
# not serving on its declared port -- a wedged/hung/deadlocked server.
|
|
1417
|
+
# Previously this branch wrote ok:true unconditionally, so the HTTP
|
|
1418
|
+
# signal could never report a failure and a wedged server stayed
|
|
1419
|
+
# "healthy" forever. Report the failure honestly so the watchdog can
|
|
1420
|
+
# act on it. We deliberately do NOT flip state.json to "crashed" here
|
|
1421
|
+
# (mirroring the dead-PID precedent above at the kill -0 check); the
|
|
1422
|
+
# watchdog owns the crashed transition after its circuit breaker, so a
|
|
1423
|
+
# single transient blip does not prematurely mark the app crashed.
|
|
1424
|
+
_write_health "false"
|
|
1425
|
+
return 1
|
|
1387
1426
|
fi
|
|
1388
1427
|
fi
|
|
1389
1428
|
|
|
1390
|
-
# Non-HTTP: PID alive is sufficient
|
|
1429
|
+
# Non-HTTP: PID alive is sufficient (no URL/port to probe)
|
|
1391
1430
|
_write_health "true"
|
|
1392
1431
|
return 0
|
|
1393
1432
|
}
|
|
@@ -1477,17 +1516,35 @@ app_runner_watchdog() {
|
|
|
1477
1516
|
return 0
|
|
1478
1517
|
fi
|
|
1479
1518
|
|
|
1480
|
-
# Process alive,
|
|
1519
|
+
# Process alive: kill -0 only proves the PID exists, not that the app is
|
|
1520
|
+
# actually serving. A hung event loop, a deadlock, or a wedged dev server
|
|
1521
|
+
# all pass kill -0 forever while never answering a request, so the old
|
|
1522
|
+
# "alive == healthy" shortcut let a wedged HTTP app run un-restarted and
|
|
1523
|
+
# left health.json stale. Mirror the compose branch: defer to
|
|
1524
|
+
# app_runner_health_check (HTTP-aware for apps that declared a port), and
|
|
1525
|
+
# treat an unhealthy-but-alive process as a crash so the same circuit
|
|
1526
|
+
# breaker + backoff + restart path handles it.
|
|
1481
1527
|
if kill -0 "$_APP_RUNNER_PID" 2>/dev/null; then
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1528
|
+
if app_runner_health_check; then
|
|
1529
|
+
# BUG 3 fix: a confirmed-healthy observation clears the accumulated
|
|
1530
|
+
# crash count so the breaker fires only on 5 CONSECUTIVE failures,
|
|
1531
|
+
# not on 5 cumulative crashes that were each successfully recovered
|
|
1532
|
+
# over a long session (which would trip the breaker on a HEALTHY app).
|
|
1533
|
+
_APP_RUNNER_CRASH_COUNT=0
|
|
1534
|
+
return 0
|
|
1535
|
+
fi
|
|
1536
|
+
# Alive but not healthy (e.g. HTTP probe failed for an app that declared
|
|
1537
|
+
# a port). Fall through to the crash path below, but first terminate the
|
|
1538
|
+
# wedged process: it is still bound to the port, so app_runner_start's
|
|
1539
|
+
# port-conflict guard would otherwise refuse to start and the breaker
|
|
1540
|
+
# would trip while the orphan keeps serving hung responses (a restart
|
|
1541
|
+
# storm). app_runner_stop performs a full process-tree teardown and
|
|
1542
|
+
# clears _APP_RUNNER_PID / app.pid, leaving a clean slate for restart.
|
|
1543
|
+
log_warn "App Runner: process alive but unhealthy (not serving) -- treating as crash"
|
|
1544
|
+
app_runner_stop
|
|
1488
1545
|
fi
|
|
1489
1546
|
|
|
1490
|
-
# Process is dead
|
|
1547
|
+
# Process is dead (or was just torn down because it was alive-but-wedged)
|
|
1491
1548
|
_APP_RUNNER_CRASH_COUNT=$(( _APP_RUNNER_CRASH_COUNT + 1 ))
|
|
1492
1549
|
log_warn "App Runner: process died (crash #$_APP_RUNNER_CRASH_COUNT)"
|
|
1493
1550
|
|
package/autonomy/run.sh
CHANGED
|
@@ -9809,9 +9809,16 @@ CPEOF
|
|
|
9809
9809
|
old_cp="${checkpoint_dir}/${old_cp}"
|
|
9810
9810
|
rm -rf "$old_cp" 2>/dev/null || true
|
|
9811
9811
|
done
|
|
9812
|
-
# Rebuild index atomically from remaining checkpoints (sorted by epoch)
|
|
9812
|
+
# Rebuild index atomically from remaining checkpoints (sorted by epoch).
|
|
9813
|
+
# BUG-ST-012: sort on the checkpoint dir BASENAME, not the full path.
|
|
9814
|
+
# Checkpoint ids are cp-<iter>-<epoch> so basename field 3 is the epoch,
|
|
9815
|
+
# but a full path like .../loki-mode/.loki/.../cp-N-EPOCH/metadata.json has
|
|
9816
|
+
# extra hyphens (e.g. the loki-mode cwd) that shift the epoch out of field 3.
|
|
9817
|
+
# Prefix each path with a basename-derived key, sort on it, then strip it.
|
|
9813
9818
|
local tmp_index="${index_file}.tmp.$$"
|
|
9814
|
-
for remaining in $(find "$checkpoint_dir" -maxdepth 2 -name "metadata.json" -path "*/cp-*/*" 2>/dev/null
|
|
9819
|
+
for remaining in $(find "$checkpoint_dir" -maxdepth 2 -name "metadata.json" -path "*/cp-*/*" 2>/dev/null \
|
|
9820
|
+
| while read -r mp; do printf '%s\t%s\n' "$(basename "$(dirname "$mp")")" "$mp"; done \
|
|
9821
|
+
| sort -t'-' -k3 -n | cut -f2-); do
|
|
9815
9822
|
[ -f "$remaining" ] || continue
|
|
9816
9823
|
_CP_META="$remaining" python3 -c "
|
|
9817
9824
|
import json,os
|
package/dashboard/__init__.py
CHANGED
package/docs/INSTALLATION.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
|
|
4
4
|
|
|
5
|
-
**Version:** v7.
|
|
5
|
+
**Version:** v7.62.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -395,7 +395,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
|
|
|
395
395
|
# Run Loki Mode in Docker (Claude provider, API-key auth)
|
|
396
396
|
docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
|
|
397
397
|
-v $(pwd):/workspace -w /workspace \
|
|
398
|
-
asklokesh/loki-mode:7.
|
|
398
|
+
asklokesh/loki-mode:7.62.0 start ./my-spec.md
|
|
399
399
|
```
|
|
400
400
|
|
|
401
401
|
##### docker compose + .env (no host install)
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var r6=Object.defineProperty;var t6=($)=>$;function i6($,Q){this[$]=t6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)r6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:i6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var D1={};h(D1,{lokiDir:()=>P,homeLokiDir:()=>n$,findRepoRootForVersion:()=>o$,REPO_ROOT:()=>g});import{resolve as n,dirname as d$}from"path";import{fileURLToPath as e6}from"url";import{existsSync as P$}from"fs";import{homedir as $Q}from"os";function QQ(){let $=S1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=d$($);if(Z===$)break;$=Z}return n(S1,"..","..","..")}function o$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=d$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function n$(){return n($Q(),".loki")}var S1,g;var b=L(()=>{S1=d$(e6(import.meta.url));g=QQ()});import{readFileSync as ZQ}from"fs";import{resolve as zQ,dirname as XQ}from"path";import{fileURLToPath as KQ}from"url";function j$(){if($$!==null)return $$;let $="7.
|
|
2
|
+
var r6=Object.defineProperty;var t6=($)=>$;function i6($,Q){this[$]=t6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)r6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:i6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var D1={};h(D1,{lokiDir:()=>P,homeLokiDir:()=>n$,findRepoRootForVersion:()=>o$,REPO_ROOT:()=>g});import{resolve as n,dirname as d$}from"path";import{fileURLToPath as e6}from"url";import{existsSync as P$}from"fs";import{homedir as $Q}from"os";function QQ(){let $=S1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=d$($);if(Z===$)break;$=Z}return n(S1,"..","..","..")}function o$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=d$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function n$(){return n($Q(),".loki")}var S1,g;var b=L(()=>{S1=d$(e6(import.meta.url));g=QQ()});import{readFileSync as ZQ}from"fs";import{resolve as zQ,dirname as XQ}from"path";import{fileURLToPath as KQ}from"url";function j$(){if($$!==null)return $$;let $="7.62.0";if(typeof $==="string"&&$.length>0)return $$=$,$$;try{let Q=XQ(KQ(import.meta.url)),Z=o$(Q);$$=ZQ(zQ(Z,"VERSION"),"utf-8").trim()}catch{$$="unknown"}return $$}var $$=null;var a$=L(()=>{b()});var b1={};h(b1,{runOrThrow:()=>qQ,run:()=>k,commandVersion:()=>WQ,commandExists:()=>f,ShellError:()=>s$});async function k($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,W]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function qQ($,Q={}){let Z=await k($,Q);if(Z.exitCode!==0)throw new s$(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=VQ($),Z=await k(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function VQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function WQ($,Q="--version"){if(!await f($))return null;let z=await k([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var s$;var d=L(()=>{s$=class s$ extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function a($){return JQ?"":$}var JQ,T,S,_,wZ,I,R,y,V;var c=L(()=>{JQ=(process.env.NO_COLOR??"").length>0;T=a("\x1B[0;31m"),S=a("\x1B[0;32m"),_=a("\x1B[1;33m"),wZ=a("\x1B[0;34m"),I=a("\x1B[0;36m"),R=a("\x1B[1m"),y=a("\x1B[2m"),V=a("\x1B[0m")});import{existsSync as wQ}from"fs";async function Q$(){if(G$!==void 0)return G$;let $="/opt/homebrew/bin/python3.12";if(wQ($))return G$=$,$;let Q=await f("python3.12");if(Q)return G$=Q,Q;let Z=await f("python3");return G$=Z,Z}async function Z$($,Q={}){let Z=await Q$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return k([Z,"-c",$],Q)}var G$;var q$=L(()=>{d()});var e1={};h(e1,{runStatus:()=>uQ});import{existsSync as v,readFileSync as W$,readdirSync as d1,statSync as o1}from"fs";import{resolve as C,basename as DQ}from"path";import{homedir as CQ}from"os";function n1($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function a1($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*k$/Q);if(X>k$)X=k$;let q=k$-X,K=S;if(z>=80)K=T;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),J=n1($),U=n1(Q);return` ${R}${Z}${V} ${K}[${W}]${V} ${z}% (${J} / ${U})`}async function hQ(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${V}
|
|
3
3
|
`),process.stdout.write(`Install with:
|
|
4
4
|
`),process.stdout.write(` brew install jq (macOS)
|
|
5
5
|
`),process.stdout.write(` apt install jq (Debian/Ubuntu)
|
|
@@ -790,4 +790,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
790
790
|
`),2}default:return process.stderr.write(`Unknown command: ${Q}
|
|
791
791
|
`),process.stderr.write(s6),2}}l1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var KZ=await XZ(Bun.argv.slice(2));process.exit(KZ);
|
|
792
792
|
|
|
793
|
-
//# debugId=
|
|
793
|
+
//# debugId=77DAD44E0F11F25F64756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/memory/cross_project.py
CHANGED
|
@@ -45,7 +45,7 @@ class CrossProjectIndex:
|
|
|
45
45
|
'path': str(child),
|
|
46
46
|
'name': child.name,
|
|
47
47
|
'memory_dir': str(memory_dir),
|
|
48
|
-
'discovered_at': datetime.now(timezone.utc).isoformat()
|
|
48
|
+
'discovered_at': datetime.now(timezone.utc).isoformat(),
|
|
49
49
|
})
|
|
50
50
|
return projects
|
|
51
51
|
|
|
@@ -58,7 +58,7 @@ class CrossProjectIndex:
|
|
|
58
58
|
projects = self.discover_projects()
|
|
59
59
|
index = {
|
|
60
60
|
'projects': [],
|
|
61
|
-
'built_at': datetime.now(timezone.utc).isoformat()
|
|
61
|
+
'built_at': datetime.now(timezone.utc).isoformat(),
|
|
62
62
|
'total_episodes': 0,
|
|
63
63
|
'total_patterns': 0,
|
|
64
64
|
'total_skills': 0,
|
|
@@ -46,7 +46,7 @@ class OrganizationKnowledgeGraph:
|
|
|
46
46
|
with open(pattern_file) as f:
|
|
47
47
|
pattern = json.load(f)
|
|
48
48
|
pattern['_source_project'] = str(project_dir)
|
|
49
|
-
pattern['_extracted_at'] = datetime.now(timezone.utc).isoformat()
|
|
49
|
+
pattern['_extracted_at'] = datetime.now(timezone.utc).isoformat()
|
|
50
50
|
all_patterns.append(pattern)
|
|
51
51
|
except (json.JSONDecodeError, IOError):
|
|
52
52
|
continue
|
|
@@ -112,7 +112,7 @@ class OrganizationKnowledgeGraph:
|
|
|
112
112
|
graph = {
|
|
113
113
|
'nodes': [],
|
|
114
114
|
'edges': [],
|
|
115
|
-
'built_at': datetime.now(timezone.utc).isoformat()
|
|
115
|
+
'built_at': datetime.now(timezone.utc).isoformat(),
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
for project_dir in project_dirs:
|
|
@@ -26,15 +26,26 @@ class Topic:
|
|
|
26
26
|
Attributes:
|
|
27
27
|
id: Unique identifier for the topic
|
|
28
28
|
summary: Brief summary of the topic content
|
|
29
|
-
relevance_score: How relevant this topic is (0.0 to 1.0)
|
|
29
|
+
relevance_score: How relevant this topic is (0.0 to 1.0). This is the
|
|
30
|
+
STORED value and is what to_dict() persists.
|
|
30
31
|
token_count: Estimated tokens in the full memory
|
|
31
32
|
last_accessed: When this topic was last accessed
|
|
33
|
+
match_score: Transient, per-query ranking score (stored relevance
|
|
34
|
+
plus a keyword-match boost). None when no query boost applies.
|
|
35
|
+
Never persisted by to_dict(); used only for ranking/threshold
|
|
36
|
+
decisions within a single retrieval call.
|
|
32
37
|
"""
|
|
33
38
|
id: str
|
|
34
39
|
summary: str
|
|
35
40
|
relevance_score: float = 0.5
|
|
36
41
|
token_count: int = 0
|
|
37
42
|
last_accessed: Optional[str] = None
|
|
43
|
+
match_score: Optional[float] = None
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def effective_score(self) -> float:
|
|
47
|
+
"""Ranking score for this query: match_score when set, else stored relevance."""
|
|
48
|
+
return self.match_score if self.match_score is not None else self.relevance_score
|
|
38
49
|
|
|
39
50
|
def to_dict(self) -> Dict[str, Any]:
|
|
40
51
|
"""Convert to dictionary for JSON serialization."""
|
|
@@ -80,12 +91,17 @@ class IndexLayer:
|
|
|
80
91
|
"""
|
|
81
92
|
self.base_path = Path(base_path)
|
|
82
93
|
self.index_path = self.base_path / "index.json"
|
|
83
|
-
self._cache: Optional[Dict[str, Any]] = None
|
|
84
94
|
|
|
85
95
|
def load(self) -> Dict[str, Any]:
|
|
86
96
|
"""
|
|
87
97
|
Load index.json from disk.
|
|
88
98
|
|
|
99
|
+
Always re-reads from disk: these files are tiny (~100 token target)
|
|
100
|
+
and are written by separate processes (the dashboard reads
|
|
101
|
+
index.json via server.py while the orchestrator writes it), so an
|
|
102
|
+
in-memory cache cannot be invalidated correctly across processes.
|
|
103
|
+
An honest fresh read beats a stale cache for retrieval accuracy.
|
|
104
|
+
|
|
89
105
|
Returns:
|
|
90
106
|
Index dictionary with version, topics, and metadata
|
|
91
107
|
"""
|
|
@@ -94,8 +110,7 @@ class IndexLayer:
|
|
|
94
110
|
|
|
95
111
|
try:
|
|
96
112
|
with open(self.index_path, "r") as f:
|
|
97
|
-
|
|
98
|
-
return self._cache
|
|
113
|
+
return json.load(f)
|
|
99
114
|
except (json.JSONDecodeError, IOError):
|
|
100
115
|
return self._create_empty_index()
|
|
101
116
|
|
|
@@ -133,8 +148,6 @@ class IndexLayer:
|
|
|
133
148
|
pass
|
|
134
149
|
raise
|
|
135
150
|
|
|
136
|
-
self._cache = index
|
|
137
|
-
|
|
138
151
|
def update(self, memories: List[Dict[str, Any]]) -> None:
|
|
139
152
|
"""
|
|
140
153
|
Rebuild index from a list of memories.
|
|
@@ -216,19 +229,22 @@ class IndexLayer:
|
|
|
216
229
|
summary_lower = topic.summary.lower()
|
|
217
230
|
summary_words = set(summary_lower.split())
|
|
218
231
|
|
|
219
|
-
# Calculate match score based on word overlap
|
|
232
|
+
# Calculate match score based on word overlap.
|
|
233
|
+
# The boost is applied to a SEPARATE transient match_score, never
|
|
234
|
+
# to the stored relevance_score, so callers still see the stored
|
|
235
|
+
# value while ranking and the Layer-3 gate use the boosted score.
|
|
220
236
|
common_words = query_words & summary_words
|
|
221
|
-
if common_words:
|
|
222
|
-
# Boost relevance based on word matches
|
|
237
|
+
if common_words and query_words:
|
|
223
238
|
match_boost = len(common_words) / len(query_words) * 0.3
|
|
224
|
-
topic.
|
|
239
|
+
topic.match_score = min(1.0, topic.relevance_score + match_boost)
|
|
225
240
|
relevant.append(topic)
|
|
226
241
|
elif topic.relevance_score >= 0.8:
|
|
227
|
-
# Include high-relevance topics even without exact match
|
|
242
|
+
# Include high-relevance topics even without exact match.
|
|
243
|
+
# No keyword boost: ranking falls back to stored relevance.
|
|
228
244
|
relevant.append(topic)
|
|
229
245
|
|
|
230
|
-
# Sort by
|
|
231
|
-
relevant.sort(key=lambda t: t.
|
|
246
|
+
# Sort by effective (match-or-stored) score, descending
|
|
247
|
+
relevant.sort(key=lambda t: t.effective_score, reverse=True)
|
|
232
248
|
|
|
233
249
|
return relevant
|
|
234
250
|
|
package/memory/layers/loader.py
CHANGED
|
@@ -140,33 +140,42 @@ class ProgressiveLoader:
|
|
|
140
140
|
self._metrics.calculate_savings(index.get("total_tokens_available", 0))
|
|
141
141
|
return memories, self._metrics
|
|
142
142
|
|
|
143
|
-
# Layer 2: Load timeline for relevant topics
|
|
143
|
+
# Layer 2: Load timeline for relevant topics.
|
|
144
|
+
# Affordability gate: the timeline must fit the remaining budget. If the
|
|
145
|
+
# full timeline costs more than we can afford, loading and appending all
|
|
146
|
+
# of it would drive remaining_tokens negative and violate max_tokens
|
|
147
|
+
# (Layer 3 already has this guard; Layer 2 did not). When it does not
|
|
148
|
+
# fit, skip the timeline-as-sufficient-context shortcut and fall through
|
|
149
|
+
# to the budget-aware Layer 3 path instead of overspending.
|
|
144
150
|
timeline = self.timeline_layer.load()
|
|
145
151
|
layer2_tokens = self.timeline_layer.get_token_count()
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
152
|
+
timeline_affordable = layer2_tokens <= remaining_tokens
|
|
153
|
+
|
|
154
|
+
if timeline_affordable:
|
|
155
|
+
self._metrics.layer2_tokens = layer2_tokens
|
|
156
|
+
remaining_tokens -= layer2_tokens
|
|
157
|
+
|
|
158
|
+
# Collect timeline context for each relevant topic
|
|
159
|
+
topic_ids = {t.id for t in relevant_topics}
|
|
160
|
+
timeline_context: Dict[str, List[Dict[str, Any]]] = {}
|
|
161
|
+
|
|
162
|
+
for topic in relevant_topics:
|
|
163
|
+
topic_entries = self.timeline_layer.get_recent_for_topic(topic.id)
|
|
164
|
+
if topic_entries:
|
|
165
|
+
timeline_context[topic.id] = topic_entries
|
|
166
|
+
|
|
167
|
+
# Check if timeline provides sufficient context
|
|
168
|
+
if self.sufficient_context(timeline_context, query):
|
|
169
|
+
# Add timeline entries as context
|
|
170
|
+
for topic_id, entries in timeline_context.items():
|
|
171
|
+
for entry in entries:
|
|
172
|
+
memories.append({
|
|
173
|
+
"id": topic_id,
|
|
174
|
+
"type": "timeline",
|
|
175
|
+
"content": entry,
|
|
176
|
+
})
|
|
177
|
+
self._metrics.calculate_savings(index.get("total_tokens_available", 0))
|
|
178
|
+
return memories, self._metrics
|
|
170
179
|
|
|
171
180
|
# Layer 3: Load full memories for high-relevance topics
|
|
172
181
|
if remaining_tokens > 0:
|
|
@@ -37,12 +37,17 @@ class TimelineLayer:
|
|
|
37
37
|
"""
|
|
38
38
|
self.base_path = Path(base_path)
|
|
39
39
|
self.timeline_path = self.base_path / "timeline.json"
|
|
40
|
-
self._cache: Optional[Dict[str, Any]] = None
|
|
41
40
|
|
|
42
41
|
def load(self) -> Dict[str, Any]:
|
|
43
42
|
"""
|
|
44
43
|
Load timeline.json from disk.
|
|
45
44
|
|
|
45
|
+
Always re-reads from disk: these files are tiny (~500 token target)
|
|
46
|
+
and are written by separate processes (the dashboard reads
|
|
47
|
+
timeline.json via server.py while the orchestrator writes it), so an
|
|
48
|
+
in-memory cache cannot be invalidated correctly across processes.
|
|
49
|
+
An honest fresh read beats a stale cache for retrieval accuracy.
|
|
50
|
+
|
|
46
51
|
Returns:
|
|
47
52
|
Timeline dictionary with actions, decisions, and context
|
|
48
53
|
"""
|
|
@@ -51,8 +56,7 @@ class TimelineLayer:
|
|
|
51
56
|
|
|
52
57
|
try:
|
|
53
58
|
with open(self.timeline_path, "r") as f:
|
|
54
|
-
|
|
55
|
-
return self._cache
|
|
59
|
+
return json.load(f)
|
|
56
60
|
except (json.JSONDecodeError, IOError):
|
|
57
61
|
return self._create_empty_timeline()
|
|
58
62
|
|
|
@@ -94,8 +98,6 @@ class TimelineLayer:
|
|
|
94
98
|
pass
|
|
95
99
|
raise
|
|
96
100
|
|
|
97
|
-
self._cache = timeline
|
|
98
|
-
|
|
99
101
|
def add_action(
|
|
100
102
|
self,
|
|
101
103
|
action: str,
|
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": "7.
|
|
4
|
+
"version": "7.62.0",
|
|
5
5
|
"description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"agent",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "loki-mode",
|
|
4
4
|
"displayName": "Loki Mode",
|
|
5
|
-
"version": "7.
|
|
5
|
+
"version": "7.62.0",
|
|
6
6
|
"description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Autonomi",
|
package/src/audit/crosslink.js
CHANGED
|
@@ -508,10 +508,48 @@ function linkManifest(opts) {
|
|
|
508
508
|
};
|
|
509
509
|
}
|
|
510
510
|
|
|
511
|
+
/**
|
|
512
|
+
* Read the witnessed agent-chain high-water mark.
|
|
513
|
+
*
|
|
514
|
+
* The witness file (witness.jsonl) records, on each append, the agent
|
|
515
|
+
* chain's `agentEntries` count at witness time. Because the file is
|
|
516
|
+
* append-only and the agent chain only grows, the MAX recorded
|
|
517
|
+
* `agentEntries` is a lower bound on how long the agent chain has ever
|
|
518
|
+
* legitimately been. If the live chain is now SHORTER than that, the
|
|
519
|
+
* trailing portion of the chain was truncated -- which a bare
|
|
520
|
+
* verifyChain() (genesis-to-tip linkage with no count anchor) cannot
|
|
521
|
+
* detect, because a truncated prefix re-links cleanly.
|
|
522
|
+
*
|
|
523
|
+
* @returns {object} { present, highWater } -- highWater:0 and
|
|
524
|
+
* present:false when no witness file / no usable counts exist.
|
|
525
|
+
*/
|
|
526
|
+
function witnessAgentHighWater(opts) {
|
|
527
|
+
opts = opts || {};
|
|
528
|
+
var witnessFile = opts.witnessFile ||
|
|
529
|
+
path.join((opts.projectDir || process.cwd()), '.loki', 'audit', WITNESS_FILE);
|
|
530
|
+
if (!fs.existsSync(witnessFile)) {
|
|
531
|
+
return { present: false, highWater: 0, witnessFile: witnessFile };
|
|
532
|
+
}
|
|
533
|
+
var content = fs.readFileSync(witnessFile, 'utf8').trim();
|
|
534
|
+
if (!content) return { present: false, highWater: 0, witnessFile: witnessFile };
|
|
535
|
+
var lines = content.split('\n');
|
|
536
|
+
var high = 0;
|
|
537
|
+
var sawCount = false;
|
|
538
|
+
for (var i = 0; i < lines.length; i++) {
|
|
539
|
+
var rec;
|
|
540
|
+
try { rec = JSON.parse(lines[i]); } catch (_) { continue; }
|
|
541
|
+
if (rec && typeof rec.agentEntries === 'number') {
|
|
542
|
+
sawCount = true;
|
|
543
|
+
if (rec.agentEntries > high) high = rec.agentEntries;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return { present: sawCount, highWater: high, witnessFile: witnessFile };
|
|
547
|
+
}
|
|
548
|
+
|
|
511
549
|
/**
|
|
512
550
|
* Verify the run-manifest link against the evidence chain.
|
|
513
551
|
*
|
|
514
|
-
* Composes
|
|
552
|
+
* Composes THREE checks (mirroring verifyUnified rather than a bare disk-vs
|
|
515
553
|
* -recorded compare):
|
|
516
554
|
* 1. Agent chain integrity (AuditLog.verifyChain()). This catches an
|
|
517
555
|
* edit to the ANCHOR entry itself (e.g. someone rewrites the recorded
|
|
@@ -519,14 +557,30 @@ function linkManifest(opts) {
|
|
|
519
557
|
* 2. Manifest reconciliation: re-hash the on-disk manifest and require
|
|
520
558
|
* it to equal the hash recorded by the MOST RECENT manifest-link
|
|
521
559
|
* anchor. A mutated manifest no longer matches -> tamper detected.
|
|
560
|
+
* 3. Trailing-truncation detection via the append-only witness file.
|
|
561
|
+
* verifyChain() validates previousHash linkage from genesis with NO
|
|
562
|
+
* count anchor, so an attacker who edits .loki/loki-run.json AND
|
|
563
|
+
* truncates .loki/audit/audit.jsonl to drop the trailing
|
|
564
|
+
* manifest-link anchor leaves a SHORTER but internally-consistent
|
|
565
|
+
* chain that verifies clean (and reports present:false, which a
|
|
566
|
+
* caller must NOT read as a pass). We cross-check the witness file's
|
|
567
|
+
* recorded agentEntries high-water mark against the live chain
|
|
568
|
+
* length: if the chain is now shorter than a previously-witnessed
|
|
569
|
+
* count, the trail was truncated and we return valid:false with
|
|
570
|
+
* truncationSuspected:true.
|
|
522
571
|
*
|
|
523
|
-
* HONEST empty cases (distinguishable from a real pass via `present`
|
|
524
|
-
*
|
|
572
|
+
* HONEST empty cases (distinguishable from a real pass via `present` and
|
|
573
|
+
* `truncationSuspected`):
|
|
574
|
+
* - No anchor recorded yet -> { present:false, ... }. valid is true ONLY
|
|
575
|
+
* when no witness exists or the chain still meets the witnessed
|
|
576
|
+
* high-water mark; a witnessed-then-truncated chain reports
|
|
577
|
+
* valid:false + truncationSuspected:true even on this absent-anchor
|
|
578
|
+
* path, so an absent anchor can never be silently read as verified.
|
|
525
579
|
* - Anchor exists but the manifest file is now gone -> manifest.valid
|
|
526
580
|
* is false (the pinned manifest is missing/cannot be reconciled).
|
|
527
581
|
*
|
|
528
582
|
* @param {object} [opts] projectDir / logDir / manifestPath as linkManifest.
|
|
529
|
-
* @returns {object} { valid, present, chain, manifest }
|
|
583
|
+
* @returns {object} { valid, present, truncationSuspected, chain, manifest, witness }
|
|
530
584
|
*/
|
|
531
585
|
function verifyManifestLink(opts) {
|
|
532
586
|
opts = opts || {};
|
|
@@ -537,14 +591,39 @@ function verifyManifestLink(opts) {
|
|
|
537
591
|
var entries = log.readEntries();
|
|
538
592
|
log.destroy();
|
|
539
593
|
|
|
594
|
+
// Trailing-truncation guard: compare the live chain length against the
|
|
595
|
+
// highest agentEntries count any witness ever recorded. A shrink means
|
|
596
|
+
// the chain was truncated below a point it provably once reached.
|
|
597
|
+
var hw = witnessAgentHighWater(opts);
|
|
598
|
+
var chainLen = typeof chain.entries === 'number' ? chain.entries : entries.length;
|
|
599
|
+
var truncationSuspected = hw.present && chainLen < hw.highWater;
|
|
600
|
+
var witnessInfo = {
|
|
601
|
+
present: hw.present,
|
|
602
|
+
witnessedHighWater: hw.highWater,
|
|
603
|
+
currentChainLength: chainLen,
|
|
604
|
+
truncationSuspected: truncationSuspected,
|
|
605
|
+
};
|
|
606
|
+
|
|
540
607
|
var anchors = entries.filter(function (e) {
|
|
541
608
|
return e.what === MANIFEST_LINK_ACTION;
|
|
542
609
|
});
|
|
543
610
|
|
|
544
611
|
if (anchors.length === 0) {
|
|
545
612
|
return {
|
|
546
|
-
valid: !!chain.valid
|
|
547
|
-
|
|
613
|
+
valid: !!chain.valid && !truncationSuspected,
|
|
614
|
+
present: false,
|
|
615
|
+
truncationSuspected: truncationSuspected,
|
|
616
|
+
chain: chain,
|
|
617
|
+
witness: witnessInfo,
|
|
618
|
+
manifest: {
|
|
619
|
+
present: false,
|
|
620
|
+
valid: !truncationSuspected,
|
|
621
|
+
reason: truncationSuspected
|
|
622
|
+
? 'audit chain truncated below witnessed length ' + hw.highWater +
|
|
623
|
+
' (current ' + chainLen + '); manifest-link anchor may have been ' +
|
|
624
|
+
'dropped by trailing-truncation -- absent anchor is NOT a pass'
|
|
625
|
+
: 'no manifest-link anchor recorded',
|
|
626
|
+
},
|
|
548
627
|
};
|
|
549
628
|
}
|
|
550
629
|
|
|
@@ -574,9 +653,11 @@ function verifyManifestLink(opts) {
|
|
|
574
653
|
}
|
|
575
654
|
|
|
576
655
|
return {
|
|
577
|
-
valid: !!chain.valid && manifest.valid,
|
|
656
|
+
valid: !!chain.valid && manifest.valid && !truncationSuspected,
|
|
578
657
|
present: true,
|
|
658
|
+
truncationSuspected: truncationSuspected,
|
|
579
659
|
chain: chain,
|
|
660
|
+
witness: witnessInfo,
|
|
580
661
|
manifest: manifest,
|
|
581
662
|
};
|
|
582
663
|
}
|
|
@@ -592,6 +673,7 @@ module.exports = {
|
|
|
592
673
|
defaultDashboardAuditDir: defaultDashboardAuditDir,
|
|
593
674
|
linkManifest: linkManifest,
|
|
594
675
|
verifyManifestLink: verifyManifestLink,
|
|
676
|
+
witnessAgentHighWater: witnessAgentHighWater,
|
|
595
677
|
hashManifest: hashManifest,
|
|
596
678
|
defaultManifestPath: defaultManifestPath,
|
|
597
679
|
CROSSLINK_ACTION: CROSSLINK_ACTION,
|
package/templates/README.md
CHANGED
|
@@ -18,39 +18,60 @@ loki init my-project --template saas-starter
|
|
|
18
18
|
|
|
19
19
|
## Templates
|
|
20
20
|
|
|
21
|
+
The tier below is the complexity that Loki Mode's `detect_complexity` routine
|
|
22
|
+
(`autonomy/run.sh`) actually assigns to each PRD. Complexity is auto-detected
|
|
23
|
+
from the PRD's structure (its section count and length), not from the size of
|
|
24
|
+
the finished product. A short, lightly-sectioned spec like `simple-todo-app`
|
|
25
|
+
detects as Simple; a richly-sectioned spec (more than 10 h2/h3 sections, or
|
|
26
|
+
more than 1000 words) detects as Complex, even when the app it describes is a
|
|
27
|
+
single static page. So a few visually small projects (for example
|
|
28
|
+
`static-landing-page`) land under Complex purely because their PRD is deeply
|
|
29
|
+
sectioned. The Est. Time column reflects build effort, which does not always
|
|
30
|
+
track the detected tier.
|
|
31
|
+
|
|
21
32
|
### Simple
|
|
22
33
|
|
|
34
|
+
PRD detects as Simple: fewer than 3 sections, fewer than 5 features, and under
|
|
35
|
+
200 words.
|
|
36
|
+
|
|
23
37
|
| Template | Description | Tech Stack | Est. Time |
|
|
24
38
|
|----------|-------------|------------|-----------|
|
|
25
|
-
| [simple-todo-app.md](simple-todo-app.md) | Minimal todo app for testing Loki Mode basics |
|
|
26
|
-
| [static-landing-page.md](static-landing-page.md) | SaaS landing page with hero, features, pricing, FAQ | HTML, CSS, vanilla JS | 10-15 min |
|
|
27
|
-
| [api-only.md](api-only.md) | REST API for notes with full CRUD and tests | Express, in-memory, Vitest | 15-20 min |
|
|
39
|
+
| [simple-todo-app.md](simple-todo-app.md) | Minimal todo app for testing Loki Mode basics | HTML, CSS, vanilla JS (localStorage) | 15-20 min |
|
|
28
40
|
|
|
29
41
|
### Standard
|
|
30
42
|
|
|
43
|
+
PRD detects as Standard: between the Simple and Complex thresholds (roughly
|
|
44
|
+
3 to 10 sections and under 1000 words).
|
|
45
|
+
|
|
31
46
|
| Template | Description | Tech Stack | Est. Time |
|
|
32
47
|
|----------|-------------|------------|-----------|
|
|
33
|
-
| [rest-api.md](rest-api.md) | REST API with CRUD, pagination, filtering, Swagger docs (no auth) | Express, TypeScript, Prisma, SQLite | 25-35 min |
|
|
34
|
-
| [rest-api-auth.md](rest-api-auth.md) | REST API with JWT auth, registration, login, refresh, rate limiting | Express/FastAPI, PostgreSQL, JWT, bcrypt | 30-45 min |
|
|
35
|
-
| [cli-tool.md](cli-tool.md) | File organizer CLI with subcommands, config, watch mode, undo | Node.js, Commander.js, chalk, chokidar | 30-45 min |
|
|
36
|
-
| [discord-bot.md](discord-bot.md) | Moderation bot with slash commands, auto-mod, reaction roles | discord.js, SQLite, node-cron | 45-60 min |
|
|
37
|
-
| [chrome-extension.md](chrome-extension.md) | Tab manager extension with groups, sessions, search, memory monitor | Manifest V3, vanilla JS, Chrome APIs | 30-45 min |
|
|
38
|
-
| [blog-platform.md](blog-platform.md) | Blog with markdown CMS, categories, RSS feed, SEO | Next.js, CodeMirror, SQLite, TailwindCSS | 45-60 min |
|
|
39
|
-
| [full-stack-demo.md](full-stack-demo.md) | Bookmark manager with tags, search, and filtering | React, Express, SQLite, TailwindCSS | 30-60 min |
|
|
40
|
-
| [web-scraper.md](web-scraper.md) | Configurable scraper with pagination, robots.txt, multi-format export | Python, httpx, BeautifulSoup4, SQLite | 30-45 min |
|
|
41
|
-
| [data-pipeline.md](data-pipeline.md) | ETL pipeline with multi-source ingestion, transforms, monitoring | Python, Pydantic, SQLAlchemy, Click | 30-45 min |
|
|
42
48
|
| [dashboard.md](dashboard.md) | Real-time analytics dashboard with charts, tables, drag-and-drop layout | React, Recharts, TanStack Table, WebSocket | 45-60 min |
|
|
49
|
+
| [data-pipeline.md](data-pipeline.md) | ETL pipeline with multi-source ingestion, transforms, monitoring | Python, Pydantic, SQLAlchemy, Click | 30-45 min |
|
|
43
50
|
| [game.md](game.md) | Browser-based 2D game with enemy AI, scoring, levels, high scores | HTML5 Canvas, TypeScript, Web Audio API | 30-45 min |
|
|
44
|
-
| [slack-bot.md](slack-bot.md) | Slack bot with slash commands, events, interactive messages, scheduling | Node.js, Bolt SDK, SQLite | 30-45 min |
|
|
45
|
-
| [npm-library.md](npm-library.md) | npm package with TypeScript, dual ESM/CJS, tree shaking, auto docs | TypeScript, tsup, Vitest, typedoc | 30-45 min |
|
|
46
51
|
| [microservice.md](microservice.md) | Containerized service with health checks, logging, Prometheus metrics | Express, TypeScript, Docker, Prisma, pino | 30-45 min |
|
|
52
|
+
| [npm-library.md](npm-library.md) | npm package with TypeScript, dual ESM/CJS, tree shaking, auto docs | TypeScript, tsup, Vitest, typedoc | 30-45 min |
|
|
53
|
+
| [web-scraper.md](web-scraper.md) | Configurable scraper with pagination, robots.txt, multi-format export | Python, httpx, BeautifulSoup4, SQLite | 30-45 min |
|
|
47
54
|
|
|
48
55
|
### Complex
|
|
49
56
|
|
|
57
|
+
PRD detects as Complex: more than 10 sections, OR more than 15 features, OR
|
|
58
|
+
more than 1000 words. Most templates land here because their PRDs are deeply
|
|
59
|
+
sectioned, regardless of the finished app's size.
|
|
60
|
+
|
|
50
61
|
| Template | Description | Tech Stack | Est. Time |
|
|
51
62
|
|----------|-------------|------------|-----------|
|
|
63
|
+
| [api-only.md](api-only.md) | REST API for notes with full CRUD and tests | Express, in-memory, Vitest | 15-20 min |
|
|
64
|
+
| [static-landing-page.md](static-landing-page.md) | SaaS landing page with hero, features, pricing, FAQ | HTML, CSS, vanilla JS | 10-15 min |
|
|
65
|
+
| [slack-bot.md](slack-bot.md) | Slack bot with slash commands, events, interactive messages, scheduling | Node.js, Bolt SDK, SQLite | 30-45 min |
|
|
66
|
+
| [full-stack-demo.md](full-stack-demo.md) | Bookmark manager with tags, search, and filtering | React, Express, SQLite, TailwindCSS | 30-60 min |
|
|
67
|
+
| [cli-tool.md](cli-tool.md) | File organizer CLI with subcommands, config, watch mode, undo | Node.js, Commander.js, chalk, chokidar | 30-45 min |
|
|
68
|
+
| [discord-bot.md](discord-bot.md) | Moderation bot with slash commands, auto-mod, reaction roles | discord.js, SQLite, node-cron | 45-60 min |
|
|
69
|
+
| [chrome-extension.md](chrome-extension.md) | Tab manager extension with groups, sessions, search, memory monitor | Manifest V3, vanilla JS, Chrome APIs | 30-45 min |
|
|
52
70
|
| [mobile-app.md](mobile-app.md) | Habit tracker with streaks, reminders, calendar, charts | React Native (Expo), Zustand, AsyncStorage | 45-60 min |
|
|
53
71
|
| [saas-starter.md](saas-starter.md) | SaaS app with auth, OAuth, Stripe billing, admin dashboard | Next.js, Prisma, PostgreSQL, Stripe, NextAuth | 60-90 min |
|
|
72
|
+
| [blog-platform.md](blog-platform.md) | Blog with markdown CMS, categories, RSS feed, SEO | Next.js, CodeMirror, SQLite, TailwindCSS | 45-60 min |
|
|
73
|
+
| [rest-api.md](rest-api.md) | REST API with CRUD, pagination, filtering, Swagger docs (no auth) | Express, TypeScript, Prisma, SQLite | 25-35 min |
|
|
74
|
+
| [rest-api-auth.md](rest-api-auth.md) | REST API with JWT auth, registration, login, refresh, rate limiting | Express/FastAPI, PostgreSQL, JWT, bcrypt | 30-45 min |
|
|
54
75
|
| [e-commerce.md](e-commerce.md) | Storefront with catalog, cart, Stripe checkout, order management | Next.js, Prisma, PostgreSQL, Stripe | 60-90 min |
|
|
55
76
|
| [ai-chatbot.md](ai-chatbot.md) | RAG chatbot with document upload, vector search, streaming responses | Next.js, OpenAI API, ChromaDB, Vercel AI SDK | 60-90 min |
|
|
56
77
|
|
|
@@ -74,7 +95,7 @@ Every template follows a consistent structure:
|
|
|
74
95
|
|
|
75
96
|
## Choosing a Template
|
|
76
97
|
|
|
77
|
-
**First time using Loki Mode?** Start with `simple-todo-app.md` or `api-only.md`.
|
|
98
|
+
**First time using Loki Mode?** Start with `simple-todo-app.md` (the one Simple-tier template) or `api-only.md`. Both complete quickly and validate your setup. Note that `api-only.md` detects as Complex despite finishing fast, because its PRD is heavily sectioned.
|
|
78
99
|
|
|
79
100
|
**Testing full capabilities?** Use `full-stack-demo.md`. It exercises frontend, backend, database, and code review agents without taking too long.
|
|
80
101
|
|