loki-mode 7.61.0 → 7.63.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/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.61.0'
60
+ __version__ = '7.63.0'
@@ -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() + 'Z',
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() + 'Z',
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() + 'Z'
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() + 'Z',
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
- self._cache = json.load(f)
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.relevance_score = min(1.0, topic.relevance_score + match_boost)
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 relevance score, descending
231
- relevant.sort(key=lambda t: t.relevance_score, reverse=True)
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
 
@@ -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
- self._metrics.layer2_tokens = layer2_tokens
147
- remaining_tokens -= layer2_tokens
148
-
149
- # Collect timeline context for each relevant topic
150
- topic_ids = {t.id for t in relevant_topics}
151
- timeline_context: Dict[str, List[Dict[str, Any]]] = {}
152
-
153
- for topic in relevant_topics:
154
- topic_entries = self.timeline_layer.get_recent_for_topic(topic.id)
155
- if topic_entries:
156
- timeline_context[topic.id] = topic_entries
157
-
158
- # Check if timeline provides sufficient context
159
- if self.sufficient_context(timeline_context, query):
160
- # Add timeline entries as context
161
- for topic_id, entries in timeline_context.items():
162
- for entry in entries:
163
- memories.append({
164
- "id": topic_id,
165
- "type": "timeline",
166
- "content": entry,
167
- })
168
- self._metrics.calculate_savings(index.get("total_tokens_available", 0))
169
- return memories, self._metrics
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
- self._cache = json.load(f)
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.61.0",
4
+ "version": "7.63.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.61.0",
5
+ "version": "7.63.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",
@@ -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 TWO checks (mirroring verifyUnified rather than a bare disk-vs
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
- * - No anchor recorded yet -> { present:false, valid:true, reason:... }.
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, present: false, chain: chain,
547
- manifest: { present: false, valid: true, reason: 'no manifest-link anchor recorded' },
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,
@@ -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 | React, Express, SQLite | 15-20 min |
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`. These complete quickly and validate your setup.
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