kodelyth-ecc 1.5.9 → 1.7.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/.github/workflows/publish.yml +19 -0
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +455 -0
- package/CLAUDE.md +48 -16
- package/README.md +265 -81
- package/VERSION +1 -1
- package/actions/ecc-review/README.md +243 -0
- package/actions/ecc-review/action.yml +158 -0
- package/actions/ecc-review/post-comment.js +171 -0
- package/actions/ecc-review/run-review.js +285 -0
- package/agents/backdoor-hunter.md +260 -0
- package/agents/chaos-engineer.md +251 -0
- package/agents/code-stealer-detector.md +228 -0
- package/agents/jailbreak-tester.md +222 -0
- package/agents/license-violation-finder.md +212 -0
- package/agents/prompt-injection-hunter.md +126 -0
- package/agents/secret-hunter.md +209 -0
- package/agents/supply-chain-auditor.md +195 -0
- package/bin/kodelyth-ecc.js +875 -1
- package/bundles/enterprise.md +172 -0
- package/bundles/indie-hacker.md +106 -0
- package/bundles/red-team.md +138 -0
- package/cat +0 -0
- package/commands/dashboard.md +67 -0
- package/commands/devil-mode.md +121 -0
- package/commands/memory-evolve.md +71 -0
- package/commands/replay.md +61 -0
- package/commands/route-model.md +48 -0
- package/commands/swarm.md +68 -0
- package/commands/verify-supply-chain.md +59 -0
- package/docs/dashboard.md +211 -0
- package/docs/evolve.md +303 -0
- package/docs/mcp-clients.md +167 -0
- package/docs/mcp.md +178 -0
- package/docs/replay.md +244 -0
- package/docs/supply-chain.md +207 -0
- package/docs/swarm.md +243 -0
- package/hooks/hooks.json +52 -0
- package/hooks/memory/auto-recall.js +29 -1
- package/hooks/safety/README.md +124 -0
- package/hooks/safety/lib/patterns.js +179 -0
- package/hooks/safety/prompt-injection-guard.js +179 -0
- package/hooks/safety/token-budget.js +229 -0
- package/install.ps1 +199 -2
- package/install.sh +286 -4
- package/package.json +30 -4
- package/rules/common/agent-intent-routing.md +182 -0
- package/rules/common/cost-aware-model-routing.md +152 -0
- package/scripts/dashboard/data.js +353 -0
- package/scripts/dashboard/server.js +324 -0
- package/scripts/dashboard/static/index.html +645 -0
- package/scripts/evolve/analyze.js +303 -0
- package/scripts/evolve/proposals.js +162 -0
- package/scripts/evolve/stats.js +219 -0
- package/scripts/mcp/catalog.js +244 -0
- package/scripts/mcp/client.js +181 -0
- package/scripts/mcp/prompts.js +133 -0
- package/scripts/mcp/resources.js +94 -0
- package/scripts/mcp/server.js +153 -0
- package/scripts/mcp/tools.js +465 -0
- package/scripts/replay/bundle.js +191 -0
- package/scripts/replay/replay.js +107 -0
- package/scripts/router/classify.js +232 -0
- package/scripts/supply-chain/manifest.js +154 -0
- package/scripts/supply-chain/sbom.js +202 -0
- package/scripts/supply-chain/verify.js +102 -0
- package/scripts/swarm/build-plan.js +193 -0
- package/skills/cost-aware-model-routing/SKILL.md +153 -0
- package/skills/kodelyth-quickstart/SKILL.md +7 -0
- package/skills/observability-dashboard/SKILL.md +119 -0
- package/skills/self-evolving-memory/SKILL.md +175 -0
- package/skills/session-replay/SKILL.md +199 -0
- package/skills/supply-chain-verification/SKILL.md +201 -0
- package/skills/swarm-orchestrator/SKILL.md +177 -0
- package/social/card-install.svg +1 -1
- package/social/facebook-group/POST.md +121 -0
- package/social/facebook-group/fb-1-3am-debug.png +0 -0
- package/social/facebook-group/fb-1-3am-debug.svg +97 -0
- package/social/facebook-group/fb-2-cpu-upgrade.png +0 -0
- package/social/facebook-group/fb-2-cpu-upgrade.svg +132 -0
- package/social/facebook-group/fb-3-before-after.png +0 -0
- package/social/facebook-group/fb-3-before-after.svg +94 -0
- package/social/facebook-v150.svg +5 -5
- package/social/github-social-preview.svg +119 -100
- package/social/readme-hero.svg +11 -11
- package/social/section-agents.svg +57 -0
- package/social/section-author.svg +54 -0
- package/social/section-dashboard.svg +59 -0
- package/social/section-devil.svg +54 -0
- package/social/section-hooks.svg +51 -0
- package/social/section-install.svg +42 -0
- package/social/section-learning.svg +52 -0
- package/social/section-mcp.svg +46 -0
- package/social/section-memory.svg +57 -0
- package/social/section-parallel.svg +72 -0
- package/social/section-routing.svg +51 -0
- package/social/x-card-agents-grid.svg +6 -6
- package/tests/dashboard/data.test.js +235 -0
- package/tests/dashboard/server.test.js +240 -0
- package/tests/evolve/analyze.test.js +169 -0
- package/tests/evolve/proposals.test.js +173 -0
- package/tests/evolve/stats.test.js +159 -0
- package/tests/mcp/catalog.test.js +98 -0
- package/tests/mcp/client.test.js +109 -0
- package/tests/mcp/resources-prompts.test.js +70 -0
- package/tests/mcp/tools.test.js +159 -0
- package/tests/replay/bundle.test.js +181 -0
- package/tests/replay/replay.test.js +131 -0
- package/tests/router/classify.test.js +164 -0
- package/tests/safety/patterns.test.js +94 -0
- package/tests/safety/prompt-injection-guard.test.js +111 -0
- package/tests/safety/token-budget.test.js +119 -0
- package/tests/supply-chain/manifest.test.js +147 -0
- package/tests/supply-chain/sbom.test.js +170 -0
- package/tests/supply-chain/verify.test.js +146 -0
- package/tests/swarm/build-plan.test.js +188 -0
- package/wiki/Agent-Reference.md +58 -7
- package/wiki/FAQ.md +204 -7
- package/wiki/Home.md +104 -28
- package/wiki/Hook-Reference.md +1 -1
- package/wiki/Installation-Guide.md +109 -6
- package/wiki/Platform-Support.md +239 -25
- package/wiki/Skill-Reference.md +101 -6
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Cost-Aware Model Routing — Auto-Pick the Right Model Tier per Task
|
|
2
|
+
|
|
3
|
+
This rule teaches the AI to **classify the current task and recommend the right model tier** instead of defaulting to one expensive model for everything. Pairs with the `agent-intent-routing` rule (agent selection) and the `token-budget` safety hook (spend control) to keep ECC sessions both effective AND affordable.
|
|
4
|
+
|
|
5
|
+
> **The 80/20 of cost in agent workflows:** 80% of turns are trivial (rename, format, doc, summary, list-files) and burn frontier-model tokens for no benefit. Routing them to a fast/cheap tier saves 60–80% of monthly spend with no measurable quality loss.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## The Rule
|
|
10
|
+
|
|
11
|
+
Before composing your response, classify the task by **scope, ambiguity, and risk**, then either:
|
|
12
|
+
|
|
13
|
+
1. **Route silently** — the user already picked a model that matches the tier (do nothing).
|
|
14
|
+
2. **Suggest a tier change** — emit a one-line recommendation when the active model is wildly mismatched (frontier model on a doc-typo fix, or a tiny model on a security audit).
|
|
15
|
+
|
|
16
|
+
Always be transparent. Never silently switch models. Always teach the user how to make the decision next time.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Tier definitions
|
|
21
|
+
|
|
22
|
+
| Tier | Examples | Default models (provider-agnostic) |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| **trivial** | Single-line rename, format-only, fix typo, add JSDoc, generate plain summary, list files, regex tweak, version bump, capitalisation. Output usually < 50 lines. | Claude Haiku · Gemini Flash · GPT-4.1-mini · Qwen 2.5 Coder 7B (local) · Ollama (Llama 3.1 8B). |
|
|
25
|
+
| **standard** | Code review of a single PR, write a small unit test, refactor one file, fix a non-critical bug, write a focused doc section, design a small data model. | Claude Sonnet · GPT-4.1 · Gemini Pro · Qwen 2.5 Coder 32B. |
|
|
26
|
+
| **hard** | Architecture decision, debug-blitz of intermittent prod bug, security audit, multi-file refactor (3+ files), incident postmortem, design review, devil-mode adversarial sweep, agent harness changes, anything `code-architect`/`incident-commander`/`debug-detective` would own. | Claude Opus · GPT-5 · Gemini Ultra · best-available frontier model. |
|
|
27
|
+
|
|
28
|
+
**The names are reference points** — pick the equivalent tier in whatever provider stack the user has configured. The classification is what matters, not the brand.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Classification heuristics (signals to weigh)
|
|
33
|
+
|
|
34
|
+
Use these signals together — single signals are noisy, three or four together are reliable:
|
|
35
|
+
|
|
36
|
+
### Trivial signals
|
|
37
|
+
- One file, < 50 lines of context
|
|
38
|
+
- Pure formatting / renaming / documentation work
|
|
39
|
+
- No tests need updating
|
|
40
|
+
- The user's question fits in a single sentence
|
|
41
|
+
- No ambiguity ("rename `getUserName` to `getUserDisplayName`")
|
|
42
|
+
- Verbs: format, rename, capitalize, fix typo, add comment, list, count, summarize
|
|
43
|
+
|
|
44
|
+
### Standard signals
|
|
45
|
+
- 1–3 files in scope
|
|
46
|
+
- Behavior change + tests required
|
|
47
|
+
- One subsystem
|
|
48
|
+
- Verbs: refactor, review, fix bug, write test, optimize, document, migrate (small)
|
|
49
|
+
- The user pasted a stack trace from one place
|
|
50
|
+
|
|
51
|
+
### Hard signals
|
|
52
|
+
- Cross-cutting concern (auth, payments, telemetry, retries, concurrency)
|
|
53
|
+
- Multiple subsystems / 4+ files
|
|
54
|
+
- Security implications (PII, secrets, authz, supply chain)
|
|
55
|
+
- Production / incident framing ("urgent", "down", "leaking", "live")
|
|
56
|
+
- Architectural framing ("decide", "design", "should we", "trade-off")
|
|
57
|
+
- The user invoked `code-architect`, `incident-commander`, `debug-detective`, `security-reviewer`, `chaos-engineer`, `backdoor-hunter`, `prompt-injection-hunter`
|
|
58
|
+
- Devil-mode parallel commands
|
|
59
|
+
- The session has already burned > 30k tokens (compounding context)
|
|
60
|
+
- Agent handoff chains involving 3+ specialists
|
|
61
|
+
|
|
62
|
+
If signals are **mixed or contradictory**, default to **standard** and surface the ambiguity to the user.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Output format when suggesting a tier change
|
|
67
|
+
|
|
68
|
+
Emit exactly one block, then proceed normally:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
[model-router] task=<trivial|standard|hard> · suggested=<tier> · current=<active model or "unknown">
|
|
72
|
+
why: <one short sentence on the dominant signals>
|
|
73
|
+
next: <how to switch — provider-specific, e.g. "use --model claude-haiku" or "switch to /model haiku">
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Only emit this block when the active model is mismatched.** If the user is already on the right tier, route silently.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Per-team configurability
|
|
81
|
+
|
|
82
|
+
Teams override the defaults via env vars or `.kodelyth/router.json`:
|
|
83
|
+
|
|
84
|
+
### Env vars
|
|
85
|
+
|
|
86
|
+
| Variable | Effect |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `KODELYTH_ROUTER=off` | Disable the rule entirely. The AI never suggests model changes. |
|
|
89
|
+
| `KODELYTH_ROUTER_TRIVIAL=<model-id>` | Override trivial tier. |
|
|
90
|
+
| `KODELYTH_ROUTER_STANDARD=<model-id>` | Override standard tier. |
|
|
91
|
+
| `KODELYTH_ROUTER_HARD=<model-id>` | Override hard tier. |
|
|
92
|
+
| `KODELYTH_ROUTER_DEFAULT=<trivial\|standard\|hard>` | Default tier for ambiguous tasks (default `standard`). |
|
|
93
|
+
|
|
94
|
+
### Project file: `.kodelyth/router.json`
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"trivial": "claude-haiku-4-5-20251001",
|
|
99
|
+
"standard": "claude-sonnet-4-6",
|
|
100
|
+
"hard": "claude-opus-4-1",
|
|
101
|
+
"default": "standard",
|
|
102
|
+
"notes": "Set by infra team. Bump trivial→sonnet for any task that touches /payments."
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
A `notes` field is purely human — the AI surfaces it when emitting a recommendation so the team's reasoning rides along.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Hard rules (never break these)
|
|
111
|
+
|
|
112
|
+
1. **Security audits, incident triage, and devil-mode never downgrade.** If the active task involves `security-reviewer`, `incident-commander`, `debug-detective`, `prompt-injection-hunter`, `supply-chain-auditor`, `secret-hunter`, `backdoor-hunter`, `chaos-engineer`, `jailbreak-tester`, `code-stealer-detector`, or `license-violation-finder` — recommend **hard** tier even if other signals look trivial.
|
|
113
|
+
2. **Production framing wins.** "Down", "leaking", "broken in prod", "incident", "outage" → hard.
|
|
114
|
+
3. **No silent demotion of multi-file refactors** — if 3+ files are in scope, never recommend trivial.
|
|
115
|
+
4. **Respect explicit user choice.** If the user said "use haiku" or "stay on sonnet", do not override. Surface only if a new turn introduces signals that conflict.
|
|
116
|
+
5. **Budget-aware:** if the `token-budget` safety hook reports usage > 70% of budget, bias one tier downward when ambiguous to extend the session.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Examples
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
User: "rename getUserName to getUserDisplayName everywhere"
|
|
124
|
+
[model-router] task=trivial · suggested=trivial · current=opus
|
|
125
|
+
why: single rename, mechanical, no behavior change.
|
|
126
|
+
next: switch to /model claude-haiku to save ~80% on this turn.
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
User: "production is down and the auth service is throwing 502s, help"
|
|
131
|
+
[model-router] task=hard · suggested=hard · current=haiku
|
|
132
|
+
why: production incident with security-relevant subsystem.
|
|
133
|
+
next: switch to /model claude-opus before continuing — this is incident-commander territory.
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
User: "review this 40-line PR"
|
|
138
|
+
(active model: sonnet — silent route, no block emitted)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Why this rule exists
|
|
144
|
+
|
|
145
|
+
LLM cost is the #1 reason teams stop using agent toolkits. Two complementary primitives:
|
|
146
|
+
|
|
147
|
+
- The **token-budget safety hook** prevents catastrophic blow-ups (hard cap per session).
|
|
148
|
+
- This **routing rule** prevents the slow leak (frontier-model overuse on trivial work).
|
|
149
|
+
|
|
150
|
+
Together they typically cut LLM bills by 50–70% with no measurable quality drop on standard work.
|
|
151
|
+
|
|
152
|
+
For implementation patterns inside user code (your own pipelines that call LLMs), see the [`cost-aware-llm-pipeline`](../../skills/cost-aware-llm-pipeline/SKILL.md) skill.
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
// scripts/dashboard/data.js
|
|
2
|
+
//
|
|
3
|
+
// Local observability dashboard: pure aggregators (v1.7.0).
|
|
4
|
+
//
|
|
5
|
+
// All data the dashboard renders comes from:
|
|
6
|
+
// - The MCP catalog (filesystem reads of agents/, skills/, commands/, rules/, bundles/)
|
|
7
|
+
// - The BM25 memory store (~/.kodelyth/memory/)
|
|
8
|
+
// - The evolve signal streams (~/.kodelyth/evolve/)
|
|
9
|
+
// - The token-budget hook state (${KODELYTH_TOKEN_BUDGET_DIR})
|
|
10
|
+
// - The orchestration session dirs (.orchestration/<session>/)
|
|
11
|
+
//
|
|
12
|
+
// Every aggregator is pure: takes its inputs explicitly OR uses well-defined
|
|
13
|
+
// env-overridable defaults. No global mutable state. Errors are swallowed
|
|
14
|
+
// and replaced with sane empty defaults — the dashboard must never crash
|
|
15
|
+
// because one data source is unavailable.
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
23
|
+
|
|
24
|
+
// Lazy + defensive requires — the dashboard renders even if one module is broken.
|
|
25
|
+
function safeRequire(rel) {
|
|
26
|
+
try { return require(path.join(ROOT, rel)); } catch { return null; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const memoryStore = safeRequire('scripts/memory/store.js');
|
|
30
|
+
const catalog = safeRequire('scripts/mcp/catalog.js');
|
|
31
|
+
const evolveStats = safeRequire('scripts/evolve/stats.js');
|
|
32
|
+
const evolveProposals = safeRequire('scripts/evolve/proposals.js');
|
|
33
|
+
|
|
34
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
function defaultMemoryDir() {
|
|
37
|
+
return process.env.KODELYTH_MEMORY_DIR || path.join(os.homedir(), '.kodelyth', 'memory');
|
|
38
|
+
}
|
|
39
|
+
function defaultEvolveDir() {
|
|
40
|
+
return process.env.KODELYTH_EVOLVE_DIR || path.join(os.homedir(), '.kodelyth', 'evolve');
|
|
41
|
+
}
|
|
42
|
+
function defaultBudgetDir() {
|
|
43
|
+
return process.env.KODELYTH_TOKEN_BUDGET_DIR || path.join(os.homedir(), '.kodelyth', 'token-budget');
|
|
44
|
+
}
|
|
45
|
+
function defaultCoordRoot() {
|
|
46
|
+
return process.env.KODELYTH_COORDINATION_ROOT || path.join(process.cwd(), '.orchestration');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function safeReadDir(p) {
|
|
50
|
+
try { return fs.readdirSync(p, { withFileTypes: true }); } catch { return []; }
|
|
51
|
+
}
|
|
52
|
+
function safeStat(p) {
|
|
53
|
+
try { return fs.statSync(p); } catch { return null; }
|
|
54
|
+
}
|
|
55
|
+
function safeReadJson(p, fallback) {
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
58
|
+
} catch { return fallback; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── overview ─────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
function overview({
|
|
64
|
+
repoRoot = ROOT,
|
|
65
|
+
memoryDir = defaultMemoryDir(),
|
|
66
|
+
evolveDir = defaultEvolveDir(),
|
|
67
|
+
coordRoot = defaultCoordRoot(),
|
|
68
|
+
} = {}) {
|
|
69
|
+
let catalogStats = { agents: 0, skills: 0, commands: 0, rules: 0, bundles: 0 };
|
|
70
|
+
if (catalog?.stats) {
|
|
71
|
+
try { catalogStats = catalog.stats(); } catch { /* swallow */ }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let memoryTotal = 0;
|
|
75
|
+
let memoryProjects = 0;
|
|
76
|
+
if (memoryStore?.stats) {
|
|
77
|
+
try {
|
|
78
|
+
const ms = memoryStore.stats();
|
|
79
|
+
memoryTotal = ms.total || 0;
|
|
80
|
+
memoryProjects = ms.projects || 0;
|
|
81
|
+
} catch { /* swallow */ }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let proposalsCounts = { pending: 0, accepted: 0, rejected: 0, applied: 0, total: 0 };
|
|
85
|
+
if (evolveProposals?.readLatest) {
|
|
86
|
+
try {
|
|
87
|
+
const all = evolveProposals.readLatest(evolveDir);
|
|
88
|
+
proposalsCounts.total = all.length;
|
|
89
|
+
for (const p of all) {
|
|
90
|
+
if (p.status && proposalsCounts[p.status] !== undefined) {
|
|
91
|
+
proposalsCounts[p.status] += 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch { /* swallow */ }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let routingMisses = 0;
|
|
98
|
+
if (evolveStats?.getRoutingMissStats) {
|
|
99
|
+
try { routingMisses = evolveStats.getRoutingMissStats(evolveDir).total_misses || 0; } catch { /* */ }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let surfaceTotal = 0;
|
|
103
|
+
if (evolveStats?.getReuseStats) {
|
|
104
|
+
try { surfaceTotal = evolveStats.getReuseStats(evolveDir).total_surfaces || 0; } catch { /* */ }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let sessionsCount = 0;
|
|
108
|
+
try {
|
|
109
|
+
if (fs.existsSync(coordRoot)) {
|
|
110
|
+
sessionsCount = safeReadDir(coordRoot).filter(e => e.isDirectory()).length;
|
|
111
|
+
}
|
|
112
|
+
} catch { /* */ }
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
package_root: repoRoot,
|
|
116
|
+
storage: {
|
|
117
|
+
memory_dir: memoryDir,
|
|
118
|
+
evolve_dir: evolveDir,
|
|
119
|
+
coord_root: coordRoot,
|
|
120
|
+
home: os.homedir(),
|
|
121
|
+
},
|
|
122
|
+
catalog: catalogStats,
|
|
123
|
+
memory: { total: memoryTotal, projects: memoryProjects },
|
|
124
|
+
evolve: {
|
|
125
|
+
proposals: proposalsCounts,
|
|
126
|
+
routing_misses: routingMisses,
|
|
127
|
+
surfaces_total: surfaceTotal,
|
|
128
|
+
},
|
|
129
|
+
swarm: {
|
|
130
|
+
sessions: sessionsCount,
|
|
131
|
+
},
|
|
132
|
+
generated_at: new Date().toISOString(),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── memory ───────────────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
// NOTE: the memory store reads `KODELYTH_MEMORY_DIR` once at require time
|
|
139
|
+
// and freezes its path. The dashboard therefore reads from whatever store
|
|
140
|
+
// path was configured at boot — there's no way to redirect mid-process.
|
|
141
|
+
// `memoryDir` here is reported back via /api/overview but cannot be
|
|
142
|
+
// overridden per-call.
|
|
143
|
+
function memoryStats() {
|
|
144
|
+
if (!memoryStore?.stats) return { total: 0, projects: 0, byLanguage: {}, topTags: [], storageDir: defaultMemoryDir() };
|
|
145
|
+
try { return memoryStore.stats(); }
|
|
146
|
+
catch { return { total: 0, projects: 0, byLanguage: {}, topTags: [], storageDir: defaultMemoryDir() }; }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function recentMemories({ limit = 20 } = {}) {
|
|
150
|
+
if (!memoryStore?.listAll) return [];
|
|
151
|
+
try {
|
|
152
|
+
const all = memoryStore.listAll();
|
|
153
|
+
all.sort((a, b) => String(b.captured_at || '').localeCompare(String(a.captured_at || '')));
|
|
154
|
+
return all.slice(0, Math.max(1, Math.min(200, limit))).map(m => ({
|
|
155
|
+
id: m.id,
|
|
156
|
+
captured_at: m.captured_at,
|
|
157
|
+
problem: m.problem,
|
|
158
|
+
approach: (m.approach || '').slice(0, 240),
|
|
159
|
+
tags: m.tags || [],
|
|
160
|
+
language: m.language || null,
|
|
161
|
+
project_path: m.project_path || null,
|
|
162
|
+
source: m.source || null,
|
|
163
|
+
}));
|
|
164
|
+
} catch { return []; }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function memorySearch({ query, limit = 10 } = {}) {
|
|
168
|
+
if (!memoryStore?.recall || !query) return [];
|
|
169
|
+
try {
|
|
170
|
+
const results = memoryStore.recall(query, { limit: Math.max(1, Math.min(50, limit)) });
|
|
171
|
+
return results.map(r => ({
|
|
172
|
+
id: r.id,
|
|
173
|
+
score: Number((r.score || 0).toFixed(3)),
|
|
174
|
+
problem: r.problem,
|
|
175
|
+
approach: (r.approach || '').slice(0, 240),
|
|
176
|
+
tags: r.tags || [],
|
|
177
|
+
language: r.language || null,
|
|
178
|
+
}));
|
|
179
|
+
} catch { return []; }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── evolve ───────────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
function evolveSnapshot({ evolveDir = defaultEvolveDir(), proposalLimit = 50 } = {}) {
|
|
185
|
+
proposalLimit = Math.max(1, Math.min(200, proposalLimit));
|
|
186
|
+
let reuse = { total_memories_tracked: 0, total_surfaces: 0, last_updated: null, entries: [] };
|
|
187
|
+
let miss = { total_misses: 0, unique_prompts: 0, entries: [] };
|
|
188
|
+
if (evolveStats?.getReuseStats) {
|
|
189
|
+
try { reuse = evolveStats.getReuseStats(evolveDir); } catch { /* */ }
|
|
190
|
+
}
|
|
191
|
+
if (evolveStats?.getRoutingMissStats) {
|
|
192
|
+
try { miss = evolveStats.getRoutingMissStats(evolveDir); } catch { /* */ }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let proposals = [];
|
|
196
|
+
if (evolveProposals?.readLatest) {
|
|
197
|
+
try {
|
|
198
|
+
proposals = evolveProposals.readLatest(evolveDir).slice(0, proposalLimit).map(p => ({
|
|
199
|
+
id: p.id,
|
|
200
|
+
type: p.type,
|
|
201
|
+
status: p.status,
|
|
202
|
+
created_at: p.created_at,
|
|
203
|
+
last_updated: p.last_updated || p.timestamp,
|
|
204
|
+
target_path: p.proposal?.target_path || null,
|
|
205
|
+
rationale: p.proposal?.rationale || null,
|
|
206
|
+
applied_path: p.applied_path || null,
|
|
207
|
+
note: p.note || null,
|
|
208
|
+
}));
|
|
209
|
+
} catch { /* */ }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { reuse, miss, proposals };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── catalog ──────────────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
function catalogList({ kind, query = '', limit = 50 } = {}) {
|
|
218
|
+
if (!catalog) return [];
|
|
219
|
+
const fnByKind = {
|
|
220
|
+
agents: catalog.loadAgents,
|
|
221
|
+
skills: catalog.loadSkills,
|
|
222
|
+
commands: catalog.loadCommands,
|
|
223
|
+
rules: catalog.loadRules,
|
|
224
|
+
bundles: catalog.loadBundles,
|
|
225
|
+
};
|
|
226
|
+
const fn = fnByKind[kind];
|
|
227
|
+
if (!fn) return [];
|
|
228
|
+
|
|
229
|
+
let items;
|
|
230
|
+
try { items = fn() || []; } catch { return []; }
|
|
231
|
+
|
|
232
|
+
const q = String(query || '').toLowerCase().trim();
|
|
233
|
+
if (q) {
|
|
234
|
+
items = items.filter(it => {
|
|
235
|
+
const hay = `${it.name || ''} ${it.description || ''} ${(it.tags || []).join(' ')}`.toLowerCase();
|
|
236
|
+
return hay.includes(q);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return items.slice(0, Math.max(1, Math.min(500, limit))).map(it => ({
|
|
240
|
+
name: it.name,
|
|
241
|
+
description: it.description || '',
|
|
242
|
+
tags: it.tags || [],
|
|
243
|
+
path: it.path || null,
|
|
244
|
+
origin: it.origin || null,
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function catalogCounts() {
|
|
249
|
+
if (!catalog?.stats) return { agents: 0, skills: 0, commands: 0, rules: 0, bundles: 0 };
|
|
250
|
+
try { return catalog.stats(); } catch { return { agents: 0, skills: 0, commands: 0, rules: 0, bundles: 0 }; }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── sessions (orchestration / swarm) ─────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
function sessionsList({ coordRoot = defaultCoordRoot(), limit = 30 } = {}) {
|
|
256
|
+
if (!fs.existsSync(coordRoot)) return [];
|
|
257
|
+
const dirs = safeReadDir(coordRoot).filter(e => e.isDirectory());
|
|
258
|
+
const out = [];
|
|
259
|
+
for (const d of dirs) {
|
|
260
|
+
const abs = path.join(coordRoot, d.name);
|
|
261
|
+
const st = safeStat(abs);
|
|
262
|
+
if (!st) continue;
|
|
263
|
+
const workers = safeReadDir(abs).filter(e => e.isDirectory()).map(e => e.name);
|
|
264
|
+
out.push({
|
|
265
|
+
session: d.name,
|
|
266
|
+
path: abs,
|
|
267
|
+
modified: st.mtime.toISOString(),
|
|
268
|
+
workers,
|
|
269
|
+
worker_count: workers.length,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
out.sort((a, b) => b.modified.localeCompare(a.modified));
|
|
273
|
+
return out.slice(0, Math.max(1, Math.min(200, limit)));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function sessionDetail({ session, coordRoot = defaultCoordRoot() } = {}) {
|
|
277
|
+
if (!session || session === '..' || session === '.') return null;
|
|
278
|
+
const dir = path.join(coordRoot, session);
|
|
279
|
+
// Containment check — ensure the resolved path stays within coordRoot.
|
|
280
|
+
if (!dir.startsWith(coordRoot + path.sep) && dir !== coordRoot) return null;
|
|
281
|
+
if (!fs.existsSync(dir)) return null;
|
|
282
|
+
const workers = safeReadDir(dir).filter(e => e.isDirectory());
|
|
283
|
+
return {
|
|
284
|
+
session,
|
|
285
|
+
path: dir,
|
|
286
|
+
workers: workers.map(w => {
|
|
287
|
+
const wdir = path.join(dir, w.name);
|
|
288
|
+
const taskPath = path.join(wdir, 'task.md');
|
|
289
|
+
const handoffPath = path.join(wdir, 'handoff.md');
|
|
290
|
+
const statusPath = path.join(wdir, 'status.md');
|
|
291
|
+
return {
|
|
292
|
+
slug: w.name,
|
|
293
|
+
has_task: fs.existsSync(taskPath),
|
|
294
|
+
has_handoff: fs.existsSync(handoffPath),
|
|
295
|
+
has_status: fs.existsSync(statusPath),
|
|
296
|
+
task_excerpt: safeReadExcerpt(taskPath, 800),
|
|
297
|
+
handoff_excerpt: safeReadExcerpt(handoffPath, 800),
|
|
298
|
+
status_excerpt: safeReadExcerpt(statusPath, 800),
|
|
299
|
+
};
|
|
300
|
+
}),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function safeReadExcerpt(p, max = 800) {
|
|
305
|
+
try {
|
|
306
|
+
const content = fs.readFileSync(p, 'utf8');
|
|
307
|
+
return content.length > max ? content.slice(0, max) + '\n…(truncated)' : content;
|
|
308
|
+
} catch { return null; }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ── token budget snapshot ────────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
function tokenBudgetSnapshot({ budgetDir = defaultBudgetDir() } = {}) {
|
|
314
|
+
if (!fs.existsSync(budgetDir)) return { sessions: [], total_tokens: 0 };
|
|
315
|
+
const files = safeReadDir(budgetDir).filter(e => e.isFile() && e.name.startsWith('budget-') && e.name.endsWith('.json'));
|
|
316
|
+
const sessions = [];
|
|
317
|
+
let total = 0;
|
|
318
|
+
for (const f of files) {
|
|
319
|
+
const data = safeReadJson(path.join(budgetDir, f.name), null);
|
|
320
|
+
if (!data) continue;
|
|
321
|
+
sessions.push({
|
|
322
|
+
session_id: data.session_id || f.name.replace(/^budget-|\.json$/g, ''),
|
|
323
|
+
tokens: Number(data.tokens) || 0,
|
|
324
|
+
budget: data.budget || null,
|
|
325
|
+
mode: data.mode || null,
|
|
326
|
+
last_event: data.last_event || data.updated_at || null,
|
|
327
|
+
});
|
|
328
|
+
total += Number(data.tokens) || 0;
|
|
329
|
+
}
|
|
330
|
+
sessions.sort((a, b) => (b.last_event || '').localeCompare(a.last_event || ''));
|
|
331
|
+
return { sessions: sessions.slice(0, 50), total_tokens: total };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = {
|
|
335
|
+
// overview
|
|
336
|
+
overview,
|
|
337
|
+
// memory
|
|
338
|
+
memoryStats,
|
|
339
|
+
recentMemories,
|
|
340
|
+
memorySearch,
|
|
341
|
+
// evolve
|
|
342
|
+
evolveSnapshot,
|
|
343
|
+
// catalog
|
|
344
|
+
catalogList,
|
|
345
|
+
catalogCounts,
|
|
346
|
+
// sessions
|
|
347
|
+
sessionsList,
|
|
348
|
+
sessionDetail,
|
|
349
|
+
// token budget
|
|
350
|
+
tokenBudgetSnapshot,
|
|
351
|
+
// exposed for tests
|
|
352
|
+
_internals: { defaultMemoryDir, defaultEvolveDir, defaultBudgetDir, defaultCoordRoot, safeReadDir, safeReadJson, safeReadExcerpt },
|
|
353
|
+
};
|