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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/app-runner.sh +70 -13
- package/autonomy/docker-run.sh +215 -13
- package/autonomy/loki +129 -4
- package/autonomy/prd-checklist.sh +33 -4
- package/autonomy/run.sh +121 -3
- package/autonomy/sandbox.sh +46 -7
- package/autonomy/spec-interrogation.sh +263 -26
- package/dashboard/__init__.py +1 -1
- package/dashboard/migration_engine.py +177 -84
- package/dashboard/server.py +6 -0
- package/docs/FEAT-PRDREUSE-DOCKER-PLAN.md +144 -0
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +140 -139
- 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/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.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.
|
|
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",
|
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
|
|