kodelyth-ecc 1.5.10 → 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.
Files changed (123) hide show
  1. package/.github/workflows/publish.yml +19 -0
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +447 -0
  4. package/CLAUDE.md +48 -16
  5. package/README.md +265 -81
  6. package/VERSION +1 -1
  7. package/actions/ecc-review/README.md +243 -0
  8. package/actions/ecc-review/action.yml +158 -0
  9. package/actions/ecc-review/post-comment.js +171 -0
  10. package/actions/ecc-review/run-review.js +285 -0
  11. package/agents/backdoor-hunter.md +260 -0
  12. package/agents/chaos-engineer.md +251 -0
  13. package/agents/code-stealer-detector.md +228 -0
  14. package/agents/jailbreak-tester.md +222 -0
  15. package/agents/license-violation-finder.md +212 -0
  16. package/agents/prompt-injection-hunter.md +126 -0
  17. package/agents/secret-hunter.md +209 -0
  18. package/agents/supply-chain-auditor.md +195 -0
  19. package/bin/kodelyth-ecc.js +875 -1
  20. package/bundles/enterprise.md +172 -0
  21. package/bundles/indie-hacker.md +106 -0
  22. package/bundles/red-team.md +138 -0
  23. package/cat +0 -0
  24. package/commands/dashboard.md +67 -0
  25. package/commands/devil-mode.md +121 -0
  26. package/commands/memory-evolve.md +71 -0
  27. package/commands/replay.md +61 -0
  28. package/commands/route-model.md +48 -0
  29. package/commands/swarm.md +68 -0
  30. package/commands/verify-supply-chain.md +59 -0
  31. package/docs/dashboard.md +211 -0
  32. package/docs/evolve.md +303 -0
  33. package/docs/mcp-clients.md +167 -0
  34. package/docs/mcp.md +178 -0
  35. package/docs/replay.md +244 -0
  36. package/docs/supply-chain.md +207 -0
  37. package/docs/swarm.md +243 -0
  38. package/hooks/hooks.json +52 -0
  39. package/hooks/memory/auto-recall.js +29 -1
  40. package/hooks/safety/README.md +124 -0
  41. package/hooks/safety/lib/patterns.js +179 -0
  42. package/hooks/safety/prompt-injection-guard.js +179 -0
  43. package/hooks/safety/token-budget.js +229 -0
  44. package/install.ps1 +199 -2
  45. package/install.sh +285 -3
  46. package/package.json +30 -4
  47. package/rules/common/agent-intent-routing.md +182 -0
  48. package/rules/common/cost-aware-model-routing.md +152 -0
  49. package/scripts/dashboard/data.js +353 -0
  50. package/scripts/dashboard/server.js +324 -0
  51. package/scripts/dashboard/static/index.html +645 -0
  52. package/scripts/evolve/analyze.js +303 -0
  53. package/scripts/evolve/proposals.js +162 -0
  54. package/scripts/evolve/stats.js +219 -0
  55. package/scripts/mcp/catalog.js +244 -0
  56. package/scripts/mcp/client.js +181 -0
  57. package/scripts/mcp/prompts.js +133 -0
  58. package/scripts/mcp/resources.js +94 -0
  59. package/scripts/mcp/server.js +153 -0
  60. package/scripts/mcp/tools.js +465 -0
  61. package/scripts/replay/bundle.js +191 -0
  62. package/scripts/replay/replay.js +107 -0
  63. package/scripts/router/classify.js +232 -0
  64. package/scripts/supply-chain/manifest.js +154 -0
  65. package/scripts/supply-chain/sbom.js +202 -0
  66. package/scripts/supply-chain/verify.js +102 -0
  67. package/scripts/swarm/build-plan.js +193 -0
  68. package/skills/cost-aware-model-routing/SKILL.md +153 -0
  69. package/skills/kodelyth-quickstart/SKILL.md +7 -0
  70. package/skills/observability-dashboard/SKILL.md +119 -0
  71. package/skills/self-evolving-memory/SKILL.md +175 -0
  72. package/skills/session-replay/SKILL.md +199 -0
  73. package/skills/supply-chain-verification/SKILL.md +201 -0
  74. package/skills/swarm-orchestrator/SKILL.md +177 -0
  75. package/social/card-install.svg +1 -1
  76. package/social/facebook-group/POST.md +121 -0
  77. package/social/facebook-group/fb-1-3am-debug.png +0 -0
  78. package/social/facebook-group/fb-1-3am-debug.svg +97 -0
  79. package/social/facebook-group/fb-2-cpu-upgrade.png +0 -0
  80. package/social/facebook-group/fb-2-cpu-upgrade.svg +132 -0
  81. package/social/facebook-group/fb-3-before-after.png +0 -0
  82. package/social/facebook-group/fb-3-before-after.svg +94 -0
  83. package/social/facebook-v150.svg +5 -5
  84. package/social/github-social-preview.svg +119 -100
  85. package/social/readme-hero.svg +11 -11
  86. package/social/section-agents.svg +57 -0
  87. package/social/section-author.svg +54 -0
  88. package/social/section-dashboard.svg +59 -0
  89. package/social/section-devil.svg +54 -0
  90. package/social/section-hooks.svg +51 -0
  91. package/social/section-install.svg +42 -0
  92. package/social/section-learning.svg +52 -0
  93. package/social/section-mcp.svg +46 -0
  94. package/social/section-memory.svg +57 -0
  95. package/social/section-parallel.svg +72 -0
  96. package/social/section-routing.svg +51 -0
  97. package/social/x-card-agents-grid.svg +6 -6
  98. package/tests/dashboard/data.test.js +235 -0
  99. package/tests/dashboard/server.test.js +240 -0
  100. package/tests/evolve/analyze.test.js +169 -0
  101. package/tests/evolve/proposals.test.js +173 -0
  102. package/tests/evolve/stats.test.js +159 -0
  103. package/tests/mcp/catalog.test.js +98 -0
  104. package/tests/mcp/client.test.js +109 -0
  105. package/tests/mcp/resources-prompts.test.js +70 -0
  106. package/tests/mcp/tools.test.js +159 -0
  107. package/tests/replay/bundle.test.js +181 -0
  108. package/tests/replay/replay.test.js +131 -0
  109. package/tests/router/classify.test.js +164 -0
  110. package/tests/safety/patterns.test.js +94 -0
  111. package/tests/safety/prompt-injection-guard.test.js +111 -0
  112. package/tests/safety/token-budget.test.js +119 -0
  113. package/tests/supply-chain/manifest.test.js +147 -0
  114. package/tests/supply-chain/sbom.test.js +170 -0
  115. package/tests/supply-chain/verify.test.js +146 -0
  116. package/tests/swarm/build-plan.test.js +188 -0
  117. package/wiki/Agent-Reference.md +58 -7
  118. package/wiki/FAQ.md +204 -7
  119. package/wiki/Home.md +104 -28
  120. package/wiki/Hook-Reference.md +1 -1
  121. package/wiki/Installation-Guide.md +109 -6
  122. package/wiki/Platform-Support.md +239 -25
  123. package/wiki/Skill-Reference.md +101 -6
@@ -0,0 +1,202 @@
1
+ // scripts/supply-chain/sbom.js
2
+ //
3
+ // CycloneDX 1.5 Software Bill of Materials generator for kodelyth-ecc.
4
+ //
5
+ // Pure functions — given { rootDir }, produces a CycloneDX-shaped JS object.
6
+ // No network, no external deps. Reads package.json + package-lock.json (v3).
7
+ //
8
+ // Consumers should JSON.stringify with deterministic key order
9
+ // (we already build keys in fixed order so default JSON.stringify is fine).
10
+ //
11
+ // Exports:
12
+ // - SBOM_SPEC_VERSION
13
+ // - generateSBOM({ rootDir, timestamp? }) → CycloneDX object
14
+ // - readSBOMSources({ rootDir }) → { pkg, lock } (mostly for tests)
15
+ // - buildComponentFromLockEntry(name, entry) → CycloneDX component
16
+ //
17
+ 'use strict';
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ const SBOM_SPEC_VERSION = '1.5';
23
+ const SBOM_FORMAT = 'CycloneDX';
24
+
25
+ // ── helpers ──────────────────────────────────────────────────────────────────
26
+
27
+ function readJSON(p) {
28
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
29
+ }
30
+
31
+ function safeReadJSON(p) {
32
+ try { return readJSON(p); } catch { return null; }
33
+ }
34
+
35
+ function readSBOMSources({ rootDir }) {
36
+ const pkgPath = path.join(rootDir, 'package.json');
37
+ const lockPath = path.join(rootDir, 'package-lock.json');
38
+ const pkg = readJSON(pkgPath);
39
+ const lock = safeReadJSON(lockPath);
40
+ return { pkg, lock };
41
+ }
42
+
43
+ // purl (package URL) per spec: pkg:npm/<name>@<version>
44
+ // Scoped packages keep the leading @ but the slash is encoded.
45
+ function npmPurl(name, version) {
46
+ if (!name || !version) return null;
47
+ // scoped: @scope/name → @scope/name (slash kept, leading @ kept)
48
+ // CycloneDX uses raw form; npm purl spec keeps "@scope/name"
49
+ return `pkg:npm/${name}@${version}`;
50
+ }
51
+
52
+ function bomRefFor(name, version) {
53
+ return `${name}@${version}`;
54
+ }
55
+
56
+ function buildComponentFromLockEntry(installPath, entry) {
57
+ // installPath is the lockfile key like "node_modules/foo" or "" (root)
58
+ // entry has: { name?, version, dev?, optional?, devOptional?, license?, integrity?, resolved? }
59
+ const name = entry.name || pkgNameFromPath(installPath);
60
+ if (!name || !entry.version) return null;
61
+
62
+ const component = {
63
+ type: 'library',
64
+ 'bom-ref': bomRefFor(name, entry.version),
65
+ name,
66
+ version: entry.version,
67
+ purl: npmPurl(name, entry.version),
68
+ scope: entry.dev ? 'optional' : 'required',
69
+ };
70
+
71
+ if (entry.license) component.licenses = normalizeLicense(entry.license);
72
+ if (entry.integrity) component.hashes = parseIntegrity(entry.integrity);
73
+
74
+ return component;
75
+ }
76
+
77
+ function pkgNameFromPath(installPath) {
78
+ // "node_modules/foo" → "foo"
79
+ // "node_modules/@scope/bar" → "@scope/bar"
80
+ // "node_modules/foo/node_modules/@scope/bar" → "@scope/bar" (deepest)
81
+ if (!installPath) return null;
82
+ const parts = installPath.split('node_modules/').filter(Boolean);
83
+ if (parts.length === 0) return null;
84
+ return parts[parts.length - 1].replace(/\/$/, '');
85
+ }
86
+
87
+ function normalizeLicense(license) {
88
+ // license may be a string ("MIT") or an array
89
+ if (typeof license === 'string') {
90
+ return [{ license: { id: license } }];
91
+ }
92
+ if (Array.isArray(license)) {
93
+ return license.map(l => typeof l === 'string' ? { license: { id: l } } : { license: l });
94
+ }
95
+ if (license && typeof license === 'object') {
96
+ return [{ license }];
97
+ }
98
+ return undefined;
99
+ }
100
+
101
+ function parseIntegrity(integrity) {
102
+ // npm SRI strings look like "sha512-base64==" — possibly multiple, space-separated.
103
+ return integrity.split(/\s+/).filter(Boolean).map(sri => {
104
+ const idx = sri.indexOf('-');
105
+ if (idx === -1) return null;
106
+ const alg = sri.slice(0, idx).toLowerCase();
107
+ const b64 = sri.slice(idx + 1);
108
+ // CycloneDX uses hex; convert base64 to hex.
109
+ let hex;
110
+ try { hex = Buffer.from(b64, 'base64').toString('hex'); }
111
+ catch { return null; }
112
+ const algMap = { sha256: 'SHA-256', sha384: 'SHA-384', sha512: 'SHA-512' };
113
+ return algMap[alg] ? { alg: algMap[alg], content: hex } : null;
114
+ }).filter(Boolean);
115
+ }
116
+
117
+ // ── main entry ────────────────────────────────────────────────────────────────
118
+
119
+ function generateSBOM({ rootDir, timestamp } = {}) {
120
+ if (!rootDir) throw new Error('generateSBOM: rootDir is required');
121
+ const { pkg, lock } = readSBOMSources({ rootDir });
122
+
123
+ const ts = timestamp || new Date().toISOString();
124
+
125
+ const rootComponent = {
126
+ type: 'application',
127
+ 'bom-ref': bomRefFor(pkg.name, pkg.version),
128
+ name: pkg.name,
129
+ version: pkg.version,
130
+ purl: npmPurl(pkg.name, pkg.version),
131
+ description: pkg.description || undefined,
132
+ };
133
+ if (pkg.license) rootComponent.licenses = normalizeLicense(pkg.license);
134
+
135
+ const components = [];
136
+ const dependencies = [];
137
+
138
+ if (lock && lock.packages) {
139
+ // Walk every entry except the root ("")
140
+ for (const [installPath, entry] of Object.entries(lock.packages)) {
141
+ if (!installPath) continue; // root handled as metadata.component
142
+ const c = buildComponentFromLockEntry(installPath, entry);
143
+ if (c) components.push(c);
144
+ }
145
+
146
+ // Dependency graph for the root only — direct dependencies + optionalDependencies.
147
+ const rootEntry = lock.packages[''] || {};
148
+ const directDeps = []
149
+ .concat(Object.keys(rootEntry.dependencies || {}))
150
+ .concat(Object.keys(rootEntry.optionalDependencies || {}));
151
+ if (directDeps.length) {
152
+ const seen = new Map();
153
+ for (const c of components) seen.set(c.name, c['bom-ref']);
154
+ const dependsOn = directDeps
155
+ .map(n => seen.get(n))
156
+ .filter(Boolean);
157
+ dependencies.push({
158
+ ref: rootComponent['bom-ref'],
159
+ dependsOn,
160
+ });
161
+ }
162
+ }
163
+
164
+ const bom = {
165
+ bomFormat: SBOM_FORMAT,
166
+ specVersion: SBOM_SPEC_VERSION,
167
+ serialNumber: `urn:uuid:${deriveSerial(pkg, ts)}`,
168
+ version: 1,
169
+ metadata: {
170
+ timestamp: ts,
171
+ tools: [{
172
+ vendor: 'kodelyth-ecc',
173
+ name: 'kodelyth-ecc-sbom',
174
+ version: pkg.version,
175
+ }],
176
+ component: rootComponent,
177
+ },
178
+ components,
179
+ dependencies,
180
+ };
181
+
182
+ return bom;
183
+ }
184
+
185
+ // Derive a stable, deterministic UUID-ish serial from package + timestamp.
186
+ // Not a cryptographic UUID; it's stable across re-runs at same time.
187
+ function deriveSerial(pkg, ts) {
188
+ const crypto = require('crypto');
189
+ const h = crypto.createHash('sha256').update(`${pkg.name}@${pkg.version}|${ts}`).digest('hex');
190
+ // 8-4-4-4-12 layout
191
+ return `${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20,32)}`;
192
+ }
193
+
194
+ module.exports = {
195
+ SBOM_SPEC_VERSION,
196
+ SBOM_FORMAT,
197
+ generateSBOM,
198
+ readSBOMSources,
199
+ buildComponentFromLockEntry,
200
+ // exposed for testing only
201
+ _internals: { npmPurl, parseIntegrity, normalizeLicense, pkgNameFromPath, deriveSerial },
202
+ };
@@ -0,0 +1,102 @@
1
+ // scripts/supply-chain/verify.js
2
+ //
3
+ // Verify an installed copy of kodelyth-ecc against a shipped content manifest.
4
+ //
5
+ // Pure functions — given { rootDir, manifest }, returns a structured report
6
+ // with categorized differences. No process.exit calls; CLI layer decides.
7
+ //
8
+ // Categories:
9
+ // ok — file exists with matching size + sha256
10
+ // modified — file exists but sha256 differs
11
+ // missing — file in manifest but not on disk
12
+ // extra — file on disk under shipped dirs but not in manifest (advisory)
13
+ //
14
+ // Exports:
15
+ // - verifyAgainstManifest({ rootDir, manifest }) → { ok, summary, details }
16
+ // - VERIFY_RESULT_KEYS
17
+ //
18
+ 'use strict';
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const { hashFile, walkFiles, MANIFEST_SCHEMA } = require('./manifest.js');
23
+
24
+ const VERIFY_RESULT_KEYS = ['ok', 'modified', 'missing', 'extra'];
25
+
26
+ function verifyAgainstManifest({ rootDir, manifest } = {}) {
27
+ if (!rootDir) throw new Error('verifyAgainstManifest: rootDir is required');
28
+ if (!manifest) throw new Error('verifyAgainstManifest: manifest is required');
29
+ if (manifest.schema !== MANIFEST_SCHEMA) {
30
+ throw new Error(
31
+ `verifyAgainstManifest: unsupported manifest schema "${manifest.schema}" (expected "${MANIFEST_SCHEMA}")`
32
+ );
33
+ }
34
+
35
+ const okFiles = [];
36
+ const modifiedFiles = [];
37
+ const missingFiles = [];
38
+ const extraFiles = [];
39
+
40
+ const expectedByPath = new Map();
41
+ for (const entry of manifest.files || []) {
42
+ expectedByPath.set(entry.path, entry);
43
+ }
44
+
45
+ // Pass 1: every manifest file → check disk.
46
+ for (const entry of expectedByPath.values()) {
47
+ const abs = path.join(rootDir, entry.path);
48
+ if (!fs.existsSync(abs)) {
49
+ missingFiles.push({ path: entry.path, expected_sha256: entry.sha256 });
50
+ continue;
51
+ }
52
+ let actualSha;
53
+ try { actualSha = hashFile(abs); }
54
+ catch (err) {
55
+ missingFiles.push({ path: entry.path, expected_sha256: entry.sha256, error: err.message });
56
+ continue;
57
+ }
58
+ if (actualSha === entry.sha256) {
59
+ okFiles.push(entry.path);
60
+ } else {
61
+ modifiedFiles.push({
62
+ path: entry.path,
63
+ expected_sha256: entry.sha256,
64
+ actual_sha256: actualSha,
65
+ });
66
+ }
67
+ }
68
+
69
+ // Pass 2: walk disk → flag files NOT in manifest. Advisory only.
70
+ const onDisk = walkFiles(rootDir);
71
+ for (const abs of onDisk) {
72
+ const rel = path.relative(rootDir, abs).split(path.sep).join('/');
73
+ if (!expectedByPath.has(rel)) extraFiles.push(rel);
74
+ }
75
+
76
+ const summary = {
77
+ total_in_manifest: expectedByPath.size,
78
+ ok: okFiles.length,
79
+ modified: modifiedFiles.length,
80
+ missing: missingFiles.length,
81
+ extra: extraFiles.length,
82
+ };
83
+
84
+ // verify is "ok" if no missing and no modified files. Extras are non-fatal.
85
+ const ok = summary.missing === 0 && summary.modified === 0;
86
+
87
+ return {
88
+ ok,
89
+ summary,
90
+ details: {
91
+ ok: okFiles,
92
+ modified: modifiedFiles,
93
+ missing: missingFiles,
94
+ extra: extraFiles,
95
+ },
96
+ };
97
+ }
98
+
99
+ module.exports = {
100
+ VERIFY_RESULT_KEYS,
101
+ verifyAgainstManifest,
102
+ };
@@ -0,0 +1,193 @@
1
+ // =============================================================================
2
+ // Kodelyth ECC — Swarm Plan Builder (Phase 2.7)
3
+ //
4
+ // Promotes the existing `tmux-worktree-orchestrator` infrastructure to a first-
5
+ // class CLI surface. Takes simple flags (--task, --agents N, --harness <h>, ...)
6
+ // and produces a full plan compatible with scripts/lib/tmux-worktree-orchestrator.js.
7
+ //
8
+ // Pure-function builder. Zero side effects. Tested without spawning tmux/git.
9
+ //
10
+ // Two modes:
11
+ // 1. Generic swarm: N agents work in parallel on the SAME task in isolated
12
+ // worktrees, then their handoff files are merged for review.
13
+ // 2. Specialist swarm: a list of named ECC agents (architect, code-reviewer,
14
+ // security-reviewer, …) each run with an agent-shaped prompt over the
15
+ // same task. The swarm IS a generalized /devil-mode.
16
+ // =============================================================================
17
+
18
+ 'use strict';
19
+
20
+ const path = require('path');
21
+
22
+ // ── Smart-default specialist rotations (4 / 6 / 8 agents) ────────────────────
23
+ // Picked for breadth — code, security, architecture, test, perf, UX, API, docs.
24
+ const DEFAULT_ROTATIONS = {
25
+ 4: ['code-reviewer', 'security-reviewer', 'pair-programmer', 'tdd-guide'],
26
+ 6: ['code-reviewer', 'security-reviewer', 'pair-programmer', 'tdd-guide', 'performance-optimizer', 'api-guardian'],
27
+ 8: ['code-reviewer', 'security-reviewer', 'pair-programmer', 'tdd-guide', 'performance-optimizer', 'api-guardian', 'ux-reviewer', 'doc-updater'],
28
+ };
29
+
30
+ // ── Task-keyword signal weights for smart agent rotation ─────────────────────
31
+ const SIGNAL_AGENTS = [
32
+ { regex: /\b(security|auth|vuln|cve|secrets?|injection|owasp)\b/i, agent: 'security-reviewer' },
33
+ { regex: /\b(perf(?:ormance)?|slow|p99|latency|throughput|bottleneck)\b/i, agent: 'performance-optimizer' },
34
+ { regex: /\b(load[\s-]?test|stress[\s-]?test|capacity|k6|locust|artillery)\b/i, agent: 'load-tester' },
35
+ { regex: /\b(architect|design|adr|rfc|trade[\s-]?off|system\s+design)\b/i, agent: 'architect' },
36
+ { regex: /\b(api|endpoint|contract|breaking\s+change|rest|graphql)\b/i, agent: 'api-guardian' },
37
+ { regex: /\b(test|tdd|coverage|spec|unit\s+test)\b/i, agent: 'tdd-guide' },
38
+ { regex: /\b(refactor|cleanup|tech\s+debt|code\s+smell|simplify)\b/i, agent: 'refactor-cleaner' },
39
+ { regex: /\b(ux|accessibility|wcag|a11y|usability|ui)\b/i, agent: 'ux-reviewer' },
40
+ { regex: /\b(doc(?:s|umentation)?|readme|guide|tutorial|comment)\b/i, agent: 'doc-updater' },
41
+ { regex: /\b(release|ship|tag|semver|rollback)\b/i, agent: 'release-captain' },
42
+ { regex: /\b(incident|outage|down|p[01]|sev[12]|on[\s-]?call)\b/i, agent: 'incident-commander' },
43
+ { regex: /\b(debug|why\s+is|stack\s+trace|silent\s+fail)\b/i, agent: 'debug-detective' },
44
+ { regex: /\b(database|sql|postgres|mysql|index|n\+1)\b/i, agent: 'database-reviewer' },
45
+ { regex: /\b(devil[\s-]?mode|adversarial|red[\s-]?team)\b/i, agent: 'prompt-injection-hunter' },
46
+ ];
47
+
48
+ // Always-included anchors when count >= N.
49
+ const BASELINE_ANCHORS = ['code-reviewer', 'pair-programmer'];
50
+
51
+ // ── Harness launcher templates ───────────────────────────────────────────────
52
+ // Placeholders: {worker_name} {worker_slug} {repo_root} {worktree_path}
53
+ // {branch_name} {task_file} {handoff_file} {status_file}
54
+ // {session_name}
55
+ const HARNESS_LAUNCHERS = {
56
+ claude: 'claude --print --dangerously-skip-permissions "$(cat {task_file_sh})" 2>&1 | tee -a {handoff_file_sh}; printf "\\nstate: completed\\n" >> {status_file_sh}',
57
+ codex: `bash {repo_root_sh}/scripts/orchestrate-codex-worker.sh {task_file_sh} {handoff_file_sh} {status_file_sh}`,
58
+ opencode: 'opencode run --task-file {task_file_sh} --output {handoff_file_sh} --status {status_file_sh}',
59
+ windsurf: 'windsurf-cli run --task-file {task_file_sh} --output {handoff_file_sh}',
60
+ echo: `printf 'demo worker {worker_slug}\\n' >> {handoff_file_sh}; printf 'state: completed\\n' >> {status_file_sh}`,
61
+ };
62
+
63
+ // ── Core: pick agents for a swarm of size N ──────────────────────────────────
64
+ function pickAgents({ count, task, explicit }) {
65
+ if (Array.isArray(explicit) && explicit.length > 0) {
66
+ return explicit.map(String);
67
+ }
68
+ const n = Math.max(1, Math.min(Number(count) || 4, 12));
69
+ const text = String(task || '');
70
+ const picked = new Set();
71
+
72
+ // 1. Signal-driven picks first (highest priority).
73
+ for (const { regex, agent } of SIGNAL_AGENTS) {
74
+ if (regex.test(text)) picked.add(agent);
75
+ if (picked.size >= n) break;
76
+ }
77
+
78
+ // 2. Baseline anchors next.
79
+ for (const anchor of BASELINE_ANCHORS) {
80
+ if (picked.size >= n) break;
81
+ picked.add(anchor);
82
+ }
83
+
84
+ // 3. Fill from rotation if we still need more.
85
+ const rotationKey = n <= 4 ? 4 : (n <= 6 ? 6 : 8);
86
+ for (const a of DEFAULT_ROTATIONS[rotationKey]) {
87
+ if (picked.size >= n) break;
88
+ picked.add(a);
89
+ }
90
+
91
+ // 4. Fallback fill (shouldn't normally hit).
92
+ const fallback = DEFAULT_ROTATIONS[8];
93
+ for (const a of fallback) {
94
+ if (picked.size >= n) break;
95
+ picked.add(a);
96
+ }
97
+
98
+ return [...picked].slice(0, n);
99
+ }
100
+
101
+ // ── Core: build a worker plan-config for a single agent ──────────────────────
102
+ function buildWorker({ agent, task, harness, launcherCommand }) {
103
+ const launcher = launcherCommand || HARNESS_LAUNCHERS[harness];
104
+ if (!launcher) {
105
+ throw new Error(`No launcher template for harness "${harness}". Pass --launcher-cmd or use one of: ${Object.keys(HARNESS_LAUNCHERS).join(', ')}.`);
106
+ }
107
+ return {
108
+ name: agent,
109
+ task: shapeTaskForAgent(agent, task),
110
+ launcherCommand: launcher,
111
+ };
112
+ }
113
+
114
+ function shapeTaskForAgent(agent, task) {
115
+ return [
116
+ `# ${agent} — swarm task`,
117
+ '',
118
+ `You are running as the ECC \`${agent}\` specialist in a parallel swarm. ` +
119
+ 'Other agents are running the same shared task in sibling worktrees. ' +
120
+ 'Stay strictly inside your specialty — do not duplicate what other agents will cover. ' +
121
+ 'Produce a focused handoff that names exactly which findings are yours and yours alone.',
122
+ '',
123
+ '## Shared Task',
124
+ '',
125
+ String(task || '').trim() || '(no task description provided)',
126
+ '',
127
+ '## Required handoff sections',
128
+ '1. Summary',
129
+ '2. Files Changed',
130
+ '3. Validation',
131
+ '4. Remaining Risks',
132
+ ].join('\n');
133
+ }
134
+
135
+ // ── Public API: full plan ────────────────────────────────────────────────────
136
+ function buildSwarmPlan({
137
+ task,
138
+ agents,
139
+ count = 4,
140
+ harness = 'claude',
141
+ launcherCmd,
142
+ sessionName,
143
+ repoRoot = process.cwd(),
144
+ worktreeRoot,
145
+ coordinationRoot,
146
+ baseRef = 'HEAD',
147
+ seedPaths = [],
148
+ replaceExisting = false,
149
+ } = {}) {
150
+ if (!task || typeof task !== 'string' || task.trim().length === 0) {
151
+ throw new Error('buildSwarmPlan: --task is required');
152
+ }
153
+ const explicit = Array.isArray(agents) ? agents : null;
154
+ const picked = pickAgents({ count, task, explicit });
155
+
156
+ const session = sessionName || `swarm-${new Date().toISOString().slice(0,10)}-${picked.length}a`;
157
+
158
+ const workers = picked.map(agent => buildWorker({
159
+ agent,
160
+ task,
161
+ harness,
162
+ launcherCommand: launcherCmd,
163
+ }));
164
+
165
+ return {
166
+ sessionName: session,
167
+ repoRoot,
168
+ worktreeRoot,
169
+ coordinationRoot,
170
+ baseRef,
171
+ replaceExisting: !!replaceExisting,
172
+ seedPaths,
173
+ workers,
174
+ // For debugging / dashboards:
175
+ _meta: {
176
+ task,
177
+ harness,
178
+ pickedAgents: picked,
179
+ count: picked.length,
180
+ },
181
+ };
182
+ }
183
+
184
+ module.exports = {
185
+ HARNESS_LAUNCHERS,
186
+ DEFAULT_ROTATIONS,
187
+ BASELINE_ANCHORS,
188
+ SIGNAL_AGENTS,
189
+ pickAgents,
190
+ shapeTaskForAgent,
191
+ buildWorker,
192
+ buildSwarmPlan,
193
+ };
@@ -0,0 +1,153 @@
1
+ ---
2
+ name: cost-aware-model-routing
3
+ description: Pick the right model tier (trivial/standard/hard) for each ECC session task. Companion to the cost-aware-model-routing rule and the token-budget safety hook. Use when the user asks "what model should I use" or when a session is burning frontier-model tokens on routine work.
4
+ origin: ECC
5
+ ---
6
+
7
+ # Cost-Aware Model Routing — Explicit Skill
8
+
9
+ The companion skill to the always-on routing rule. Invoke this when the user explicitly asks "what model should I use for this task" or when you (the agent) want to surface a routing recommendation deliberately rather than via the implicit rule.
10
+
11
+ > Pairs with `rules/common/cost-aware-model-routing.md` (the always-on rule), `hooks/safety/token-budget.js` (spend control), and the `cost-aware-llm-pipeline` skill (patterns for user code).
12
+
13
+ ---
14
+
15
+ ## When to invoke
16
+
17
+ Trigger this skill when:
18
+
19
+ - The user asks "which model should I use", "is opus overkill for this", "can haiku handle this", "what's the cheapest tier for this".
20
+ - A session is approaching budget limits and the next task looks trivial.
21
+ - The user is on a frontier model but the next task is mechanical (rename, format, doc).
22
+ - You want to teach the user how to route deliberately for the next 10 turns instead of just this one.
23
+
24
+ Do NOT invoke when:
25
+
26
+ - The user is mid-task on something hard (architecture, incident, security audit) — keep them on the high tier.
27
+ - The user has explicitly chosen a model in this session — respect their choice.
28
+
29
+ ---
30
+
31
+ ## Workflow
32
+
33
+ ### 1. Classify the current task
34
+
35
+ Use the three-tier framework from the rule:
36
+
37
+ | Tier | Examples |
38
+ |---|---|
39
+ | **trivial** | Rename, format, fix typo, add JSDoc, summarize, list files, version bump, kebab→camel |
40
+ | **standard** | Code review of a PR, write a unit test, refactor one file, fix a non-critical bug, write a doc section |
41
+ | **hard** | Architecture decision, debug-blitz of intermittent prod bug, security audit, multi-file refactor (3+ files), incident postmortem, devil-mode, agent harness changes |
42
+
43
+ Weight signals together. Single signals are noisy; three or four together are reliable.
44
+
45
+ ### 2. Read the team's config
46
+
47
+ Check for `.kodelyth/router.json` at the project root and these env vars:
48
+
49
+ ```
50
+ KODELYTH_ROUTER off | (unset)
51
+ KODELYTH_ROUTER_TRIVIAL override trivial-tier model id
52
+ KODELYTH_ROUTER_STANDARD override standard-tier model id
53
+ KODELYTH_ROUTER_HARD override hard-tier model id
54
+ KODELYTH_ROUTER_DEFAULT trivial | standard | hard (fallback for ambiguous tasks)
55
+ ```
56
+
57
+ If `KODELYTH_ROUTER=off`, do NOT emit recommendations — the team has chosen to opt out.
58
+
59
+ ### 3. Emit a single, structured recommendation
60
+
61
+ Use this exact format so the user can scan it instantly:
62
+
63
+ ```
64
+ [model-router] task=<tier> · suggested=<model-id> · current=<active or "unknown">
65
+ why: <one short sentence on the dominant signals>
66
+ next: <how to switch — provider-specific>
67
+ ```
68
+
69
+ Only emit when the active model is mismatched with the suggested tier. If the user is already on the right tier, route silently and do not interrupt.
70
+
71
+ ### 4. If the user wants ad-hoc routing for a future task
72
+
73
+ Tell them:
74
+
75
+ ```
76
+ npx kodelyth-ecc route "<task description>" # quick CLI hint
77
+ npx kodelyth-ecc route "<task>" --files 5 --agent debug-detective
78
+ ```
79
+
80
+ Or for an in-session check, they can `use cost-aware-model-routing` and describe the task.
81
+
82
+ ---
83
+
84
+ ## Hard rules (never break these)
85
+
86
+ 1. **Security / incident / adversarial work never downgrades.** If the active agent is `security-reviewer`, `incident-commander`, `debug-detective`, or any devil-mode agent (`prompt-injection-hunter`, `supply-chain-auditor`, `secret-hunter`, `backdoor-hunter`, `chaos-engineer`, `jailbreak-tester`, `code-stealer-detector`, `license-violation-finder`) — recommend **hard** tier even if the surface task looks trivial.
87
+ 2. **Production framing wins.** "Down", "leaking", "outage", "incident", "P0", "P1" → hard.
88
+ 3. **Multi-file refactors (3+) never go to trivial.**
89
+ 4. **Respect explicit user choice.** If the user said "use haiku", don't override unless a new turn introduces conflicting signals.
90
+ 5. **Budget-aware:** if `token-budget` reports usage > 70% of budget, bias one tier downward when ambiguous to extend the session.
91
+
92
+ ---
93
+
94
+ ## Examples
95
+
96
+ ### Example 1 — clear downgrade opportunity
97
+
98
+ ```
99
+ User: "rename getUserName to getUserDisplayName everywhere"
100
+ [active model: claude-opus-4-1]
101
+
102
+ Recommendation:
103
+ [model-router] task=trivial · suggested=claude-haiku-4-5-20251001 · current=claude-opus-4-1
104
+ why: single mechanical rename, no behavior change, no tests need updating.
105
+ next: switch to /model claude-haiku for this turn — saves ~80% in tokens with no quality loss.
106
+ ```
107
+
108
+ ### Example 2 — never downgrade incident work
109
+
110
+ ```
111
+ User: "production is down and the auth service is throwing 502s, help"
112
+ [active model: claude-haiku-4-5-20251001]
113
+
114
+ Recommendation:
115
+ [model-router] task=hard · suggested=claude-opus-4-1 · current=claude-haiku-4-5-20251001
116
+ why: production incident with security-relevant subsystem.
117
+ next: switch to /model claude-opus before continuing — this is incident-commander territory.
118
+ ```
119
+
120
+ ### Example 3 — match, no recommendation
121
+
122
+ ```
123
+ User: "review this 40-line PR for typos and obvious bugs"
124
+ [active model: claude-sonnet-4-6]
125
+
126
+ Recommendation: (silent — already on the right tier)
127
+ ```
128
+
129
+ ### Example 4 — devil-mode never downgrades
130
+
131
+ ```
132
+ User: "/devil-mode --all"
133
+ [active model: claude-haiku-4-5-20251001]
134
+
135
+ Recommendation:
136
+ [model-router] task=hard · suggested=claude-opus-4-1 · current=claude-haiku-4-5-20251001
137
+ why: devil-mode adversarial sweep — security/quality assertions need frontier reasoning.
138
+ next: switch to /model claude-opus or your equivalent frontier model.
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Implementation references
144
+
145
+ - Rule: `rules/common/cost-aware-model-routing.md`
146
+ - Classifier: `scripts/router/classify.js` (pure-function, deterministic)
147
+ - CLI: `npx kodelyth-ecc route <task>`
148
+ - Companion safety hook: `hooks/safety/token-budget.js` (spend cap)
149
+ - Skill for user-code patterns (different scope): `cost-aware-llm-pipeline`
150
+
151
+ ---
152
+
153
+ Built into [Kodelyth ECC](../../README.md). MIT licensed.
@@ -166,6 +166,12 @@ use debug-detective
166
166
  ```
167
167
  Tell the agent the symptom. It will trace the bug to its root cause.
168
168
 
169
+ **Bonus: See your AI workspace at a glance**
170
+ ```
171
+ npx kodelyth-ecc dashboard
172
+ ```
173
+ Opens the local observability dashboard at `http://127.0.0.1:5747`. Inspect memory captures, BM25 search across past solutions, evolve proposals, the full skill and agent catalog, swarm sessions, and token-budget snapshots — all local, zero telemetry.
174
+
169
175
  ---
170
176
 
171
177
  ## Common Questions
@@ -200,6 +206,7 @@ BUILD BROKEN? → use build-error-resolver or /build-fix
200
206
  UX REVIEW? → use ux-reviewer
201
207
  LOAD LANGUAGE PATTERNS? → /python-patterns, /golang-patterns, etc.
202
208
  GENERAL CODE STANDARDS? → /coding-standards
209
+ OBSERVABILITY DASHBOARD? → npx kodelyth-ecc dashboard (localhost:5747)
203
210
  ```
204
211
 
205
212
  ---