@gobing-ai/knowledge-kit 0.0.10 → 0.0.12

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 (46) hide show
  1. package/dist/index.js +363 -138
  2. package/package.json +1 -1
  3. package/plugins/generations/content-gen/src/storm.ts +99 -50
  4. package/plugins/generations/core-facts-gen/package.json +17 -0
  5. package/plugins/generations/core-facts-gen/plugin.json +7 -0
  6. package/plugins/generations/core-facts-gen/src/index.ts +116 -0
  7. package/plugins/generations/core-facts-gen/tsconfig.json +4 -0
  8. package/plugins/generations/daily-article-gen/package.json +17 -0
  9. package/plugins/generations/daily-article-gen/plugin.json +7 -0
  10. package/plugins/generations/daily-article-gen/src/index.ts +91 -0
  11. package/plugins/generations/daily-article-gen/tsconfig.json +4 -0
  12. package/plugins/generations/dailynews-gen/src/index.ts +11 -0
  13. package/plugins/generations/dailynews-gen/src/script-builder.ts +1 -1
  14. package/plugins/generations/episode-plan-gen/package.json +17 -0
  15. package/plugins/generations/episode-plan-gen/plugin.json +7 -0
  16. package/plugins/generations/episode-plan-gen/src/index.ts +726 -0
  17. package/plugins/generations/episode-plan-gen/tsconfig.json +4 -0
  18. package/plugins/generations/voice-gen/src/index.ts +102 -11
  19. package/plugins/generations/voice-gen/src/qc.ts +154 -9
  20. package/plugins/ingestions/aihot-ingest/plugin.json +1 -1
  21. package/plugins/ingestions/aihot-ingest/src/index.ts +72 -13
  22. package/plugins/ingestions/aihot-ingest/src/mapper.ts +1 -0
  23. package/plugins/ingestions/aihot-ingest/src/rss.ts +151 -0
  24. package/plugins/ingestions/horizon-ingest/package.json +17 -0
  25. package/plugins/ingestions/horizon-ingest/plugin.json +7 -0
  26. package/plugins/ingestions/horizon-ingest/src/index.ts +205 -0
  27. package/plugins/ingestions/horizon-ingest/tsconfig.json +4 -0
  28. package/plugins/ingestions/last30days-ingest/package.json +17 -0
  29. package/plugins/ingestions/last30days-ingest/plugin.json +7 -0
  30. package/plugins/ingestions/last30days-ingest/src/index.ts +148 -0
  31. package/plugins/ingestions/last30days-ingest/tsconfig.json +4 -0
  32. package/plugins/kk/README.md +2 -2
  33. package/plugins/kk/commands/tell-me.md +16 -0
  34. package/plugins/kk/skills/explain-things/SKILL.md +58 -0
  35. package/plugins/kk/skills/explain-things/references/views.md +95 -0
  36. package/plugins/kk/skills/taste-unslop/SKILL.md +98 -0
  37. package/plugins/kk/skills/taste-unslop/references/pattern-guide.md +148 -0
  38. package/plugins/kk/workflows/kk-daily-ai-voice.yaml +432 -19
  39. package/plugins/publishings/podcast-pub/package.json +17 -0
  40. package/plugins/publishings/podcast-pub/plugin.json +7 -0
  41. package/plugins/publishings/podcast-pub/src/index.ts +538 -0
  42. package/plugins/publishings/podcast-pub/src/map.ts +165 -0
  43. package/plugins/publishings/podcast-pub/src/microfeed-client.ts +196 -0
  44. package/plugins/publishings/podcast-pub/src/show-notes.ts +132 -0
  45. package/plugins/publishings/podcast-pub/tsconfig.json +4 -0
  46. package/plugins/publishings/surfdash-pub/src/index.ts +328 -62
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "horizon-ingest",
3
+ "kind": "ingestion",
4
+ "entry": "./src/index.ts",
5
+ "version": "1.0.0",
6
+ "description": "Wraps the Horizon CLI (uv run horizon) into Doc[] — one Doc per digest item block"
7
+ }
@@ -0,0 +1,205 @@
1
+ import { homedir } from 'node:os';
2
+ import { dirname, join } from 'node:path';
3
+ import { parseArgs } from 'node:util';
4
+ import { type Doc, DocListSchema } from '@gobing-ai/kk-core';
5
+ import { atomicWriteJson, createNodeFileSystem, readJsonFile } from '@gobing-ai/ts-runtime';
6
+ import { echoError } from '@gobing-ai/ts-utils';
7
+ import { z } from 'zod';
8
+
9
+ /**
10
+ * Ingestion adapter over the Horizon CLI (task 0089 contract).
11
+ * Runs `uv run horizon` (cwd = the Horizon repo), then parses the digest
12
+ * `<data-dir>/summaries/horizon-<date>-<lang>.md` written during this run —
13
+ * one Doc per `### [title](url)` item block.
14
+ */
15
+
16
+ const InputConfigSchema = z.object({
17
+ data_dir: z.string().trim().min(1).optional(),
18
+ config: z.string().trim().min(1).optional(),
19
+ hours: z.number().int().min(1).optional(),
20
+ languages: z.array(z.string().trim().min(1)).optional(),
21
+ horizon_dir: z.string().trim().min(1).optional(),
22
+ });
23
+
24
+ export interface HorizonRunResult {
25
+ exitCode: number;
26
+ stderr: string;
27
+ }
28
+
29
+ /** Subprocess seam — tests inject a fake that pre-seeds a summaries file. */
30
+ export type RunHorizon = (config: z.infer<typeof InputConfigSchema>, horizonDir: string) => Promise<HorizonRunResult>;
31
+
32
+ export const defaultRunner: RunHorizon = async (config, horizonDir) => {
33
+ const argv = [process.env.HORIZON_UV ?? 'uv', 'run', 'horizon'];
34
+ if (config.hours !== undefined) argv.push('--hours', String(config.hours));
35
+ if (config.data_dir !== undefined) argv.push('-d', config.data_dir);
36
+ if (config.config !== undefined) argv.push('-c', config.config);
37
+ const proc = Bun.spawnSync(argv, { cwd: horizonDir, stdout: 'pipe', stderr: 'pipe' });
38
+ return { exitCode: proc.exitCode ?? 1, stderr: proc.stderr.toString() };
39
+ };
40
+
41
+ const HEADING_RE = /^### \[(.+)\]\((.+)\) ⭐️ ([\d.]+)\/10\s*$/m;
42
+ const ANCHOR_RE = /<a id="item-([a-z0-9-]+)-(\d+)"><\/a>/;
43
+
44
+ interface ParsedItem {
45
+ anchorProfile: string;
46
+ anchorN: string;
47
+ title: string;
48
+ url: string;
49
+ score: number;
50
+ block: string;
51
+ }
52
+
53
+ /** Split a digest markdown file into per-item blocks keyed off the item anchors. */
54
+ export function splitItemBlocks(markdown: string): ParsedItem[] {
55
+ const items: ParsedItem[] = [];
56
+ const segments = markdown.split(/(?=<a id="item-)/);
57
+ for (const segment of segments) {
58
+ const anchor = segment.match(ANCHOR_RE);
59
+ const heading = segment.match(HEADING_RE);
60
+ if (!anchor || !heading) continue;
61
+ items.push({
62
+ anchorProfile: anchor[1] ?? '',
63
+ anchorN: anchor[2] ?? '',
64
+ title: heading[1] ?? '',
65
+ url: heading[2] ?? '',
66
+ score: Number(heading[3]),
67
+ block: segment,
68
+ });
69
+ }
70
+ return items;
71
+ }
72
+
73
+ /** Map one parsed item block to a Doc. */
74
+ export function itemToDoc(item: ParsedItem, date: string, lang: string): Doc {
75
+ const lines = item.block.split('\n');
76
+ const bodyParts: string[] = [];
77
+ const references: string[] = [];
78
+ let sourceLine = '';
79
+ let tags: string[] = [];
80
+ let inDetails = false;
81
+ for (const line of lines.slice(1)) {
82
+ if (line.startsWith('<details')) inDetails = true;
83
+ if (line.startsWith('</details>')) {
84
+ inDetails = false;
85
+ continue;
86
+ }
87
+ if (inDetails) {
88
+ const href = line.match(/<a href="([^"]+)"/);
89
+ if (href?.[1]) references.push(href[1]);
90
+ continue;
91
+ }
92
+ const tagMatch = line.match(/^\*\*Tags\*\*:\s*(.+)$/);
93
+ if (tagMatch?.[1]) {
94
+ tags = [...tagMatch[1].matchAll(/#([^`,\s]+)/g)].map((m) => m[1] ?? '');
95
+ continue;
96
+ }
97
+ if (/^---\s*$/.test(line)) continue;
98
+ if (/^[a-z0-9_-]+ · /i.test(line) && !sourceLine) {
99
+ sourceLine = line.trim();
100
+ continue;
101
+ }
102
+ if (line.trim()) bodyParts.push(line);
103
+ }
104
+ const doc: Doc = {
105
+ id: `horizon:${date}-${item.anchorProfile}-${item.anchorN}`,
106
+ title: item.title,
107
+ body: bodyParts.join('\n').trim(),
108
+ sourceUri: item.url,
109
+ mediaType: 'text/markdown',
110
+ metadata: {
111
+ lang,
112
+ date,
113
+ profile: item.anchorProfile,
114
+ score: item.score,
115
+ source: sourceLine.split(' · ')[0] ?? '',
116
+ source_line: sourceLine,
117
+ tags,
118
+ references,
119
+ },
120
+ };
121
+ return doc;
122
+ }
123
+
124
+ /** Parse a full digest file into Doc[]. */
125
+ export function digestToDocs(markdown: string, date: string, lang: string): Doc[] {
126
+ return DocListSchema.parse(splitItemBlocks(markdown).map((item) => itemToDoc(item, date, lang)));
127
+ }
128
+
129
+ export interface IngestionOptions {
130
+ out: string;
131
+ }
132
+
133
+ export async function processIngestionIO(
134
+ options: IngestionOptions,
135
+ config: z.infer<typeof InputConfigSchema>,
136
+ runner: RunHorizon = defaultRunner,
137
+ now: Date = new Date(),
138
+ ): Promise<{ docs: Doc[] }> {
139
+ const fs = createNodeFileSystem();
140
+ const horizonDir = config.horizon_dir ?? process.env.HORIZON_DIR ?? join(homedir(), 'tools', 'Horizon');
141
+ const dataDir = config.data_dir ?? join(horizonDir, 'data');
142
+
143
+ const runStartedAt = now.getTime();
144
+ const run = await runner(config, horizonDir);
145
+ if (run.exitCode !== 0) {
146
+ throw new Error(`horizon exited ${run.exitCode}: ${run.stderr.trim() || 'no stderr'}`);
147
+ }
148
+
149
+ // Collect digests written (or refreshed) during this run. The date comes from the
150
+ // FILENAME, not the run clock — Horizon names the digest for the date it finishes,
151
+ // which crosses UTC midnight when a long crawl runs late in the day (dogfood
152
+ // daily-ai-news-20260903: run started 23:35 UTC, digest written 00:03 UTC next day).
153
+ const langs = new Set(config.languages ?? ['zh', 'en']);
154
+ const summariesDir = join(dataDir, 'summaries');
155
+ const docs: Doc[] = [];
156
+ if (!(await fs.exists(summariesDir))) {
157
+ echoError(`horizon-ingest: no summaries dir at ${summariesDir} — emitting []`);
158
+ }
159
+ const digestRe = /^horizon-(\d{4}-\d{2}-\d{2})-([a-z-]+)\.md$/;
160
+ const entries = (await fs.exists(summariesDir)) ? await fs.readDir(summariesDir) : [];
161
+ for (const entry of entries) {
162
+ const match = entry.match(digestRe);
163
+ if (!match?.[1] || !match?.[2] || !langs.has(match[2])) continue;
164
+ const file = join(summariesDir, entry);
165
+ const stat = await fs.stat(file);
166
+ if (!stat || stat.mtimeMs < runStartedAt - 1000) continue; // stale file from an earlier run
167
+ docs.push(...digestToDocs(await fs.readFile(file), match[1], match[2]));
168
+ }
169
+ if (docs.length === 0) {
170
+ echoError(
171
+ `horizon-ingest: no fresh digest in ${summariesDir} (languages: ${[...langs].join(', ')}) — emitting []`,
172
+ );
173
+ }
174
+
175
+ const outDir = dirname(options.out);
176
+ if (outDir && outDir !== '.') await fs.ensureDir(outDir);
177
+ await atomicWriteJson(options.out, DocListSchema.parse(docs), fs);
178
+ return { docs };
179
+ }
180
+
181
+ export async function main(runner?: RunHorizon): Promise<number> {
182
+ let values: { in?: string; out?: string };
183
+ try {
184
+ ({ values } = parseArgs({ options: { in: { type: 'string' }, out: { type: 'string' } } }));
185
+ } catch (err: unknown) {
186
+ echoError(`horizon-ingest failed: ${err instanceof Error ? err.message : String(err)}`);
187
+ return 1;
188
+ }
189
+ if (!values.in || !values.out) {
190
+ echoError('horizon-ingest failed: Missing required arguments: --in <config.json> --out <docs.json>');
191
+ return 1;
192
+ }
193
+ try {
194
+ const config = InputConfigSchema.parse(await readJsonFile(values.in));
195
+ await processIngestionIO({ out: values.out }, config, runner);
196
+ return 0;
197
+ } catch (err: unknown) {
198
+ echoError(`horizon-ingest failed: ${err instanceof Error ? err.message : String(err)}`);
199
+ return 1;
200
+ }
201
+ }
202
+
203
+ if (import.meta.main) {
204
+ process.exit(await main());
205
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../../tooling/typescript/base.json",
3
+ "include": ["src", "tests"]
4
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@gobing-ai/last30days-ingest",
3
+ "type": "module",
4
+ "private": true,
5
+ "scripts": {
6
+ "typecheck": "tsc --noEmit"
7
+ },
8
+ "dependencies": {
9
+ "@gobing-ai/kk-core": "workspace:*",
10
+ "@gobing-ai/ts-runtime": "catalog:",
11
+ "@gobing-ai/ts-utils": "catalog:",
12
+ "zod": "4.4.3"
13
+ },
14
+ "devDependencies": {
15
+ "@types/bun": "1.3.14"
16
+ }
17
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "last30days-ingest",
3
+ "kind": "ingestion",
4
+ "entry": "./src/index.ts",
5
+ "version": "1.0.0",
6
+ "description": "Wraps the last30days skill CLI (agent JSON contract) into Doc[] — one Doc per results[] item"
7
+ }
@@ -0,0 +1,148 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ import { parseArgs } from 'node:util';
5
+ import { type Doc, DocListSchema } from '@gobing-ai/kk-core';
6
+ import { atomicWriteJson, createNodeFileSystem, readJsonFile } from '@gobing-ai/ts-runtime';
7
+ import { echoError } from '@gobing-ai/ts-utils';
8
+ import { z } from 'zod';
9
+
10
+ /**
11
+ * Ingestion adapter over the last30days skill CLI (task 0089 contract).
12
+ * Shells out to `last30days.py --emit=json --json-profile=agent`, captures the
13
+ * agent JSON from `--output`, and maps `results[]` → `Doc[]` (one Doc per item).
14
+ * Without `SETUP_COMPLETE` the tool self-degrades to keyless web search — that
15
+ * degraded `Doc[]` is still valid output (exit 0).
16
+ */
17
+
18
+ const InputConfigSchema = z.object({
19
+ topic: z.string().trim().min(1),
20
+ days: z.number().int().min(1).max(90).optional(),
21
+ web_backend: z.string().trim().min(1).optional(),
22
+ save_dir: z.string().trim().min(1).optional(),
23
+ });
24
+
25
+ export interface Last30DaysRunResult {
26
+ exitCode: number;
27
+ outputPath: string;
28
+ stderr: string;
29
+ }
30
+
31
+ /** Subprocess seam — tests inject a fake that writes a canned agent JSON. */
32
+ export type RunLast30Days = (
33
+ config: z.infer<typeof InputConfigSchema>,
34
+ outputPath: string,
35
+ ) => Promise<Last30DaysRunResult>;
36
+
37
+ export const defaultRunner: RunLast30Days = async (config, outputPath) => {
38
+ const skillDir = process.env.LAST30DAYS_SKILL_DIR ?? join(homedir(), '.agents', 'skills', 'last30days');
39
+ const argv = [
40
+ process.env.LAST30DAYS_PYTHON ?? 'python3',
41
+ join(skillDir, 'scripts', 'last30days.py'),
42
+ config.topic,
43
+ '--emit=json',
44
+ '--json-profile=agent',
45
+ '--output',
46
+ outputPath,
47
+ ];
48
+ if (config.days !== undefined) argv.push('--days', String(config.days));
49
+ if (config.web_backend !== undefined) argv.push('--web-backend', config.web_backend);
50
+ if (config.save_dir !== undefined) argv.push('--save-dir', config.save_dir);
51
+ const proc = Bun.spawnSync(argv, { cwd: skillDir, stdout: 'pipe', stderr: 'pipe' });
52
+ return { exitCode: proc.exitCode ?? 1, outputPath, stderr: proc.stderr.toString() };
53
+ };
54
+
55
+ const AgentJsonSchema = z.object({
56
+ schema_version: z.string().optional(),
57
+ window_days: z.number().optional(),
58
+ query: z.string().optional(),
59
+ results: z.array(z.record(z.string(), z.unknown())),
60
+ });
61
+
62
+ export function mapResultsToDocs(agentJson: unknown, topic: string): Doc[] {
63
+ const parsed = AgentJsonSchema.parse(agentJson);
64
+ const docs: Doc[] = [];
65
+ for (const item of parsed.results) {
66
+ try {
67
+ // Degraded/keyless mode uses the full URL as candidate_id — a 90-char URL id is
68
+ // brittle for downstream STORM citation (outline agents miscite them; dogfood
69
+ // run ff10095b failed assertKnownEvidence with 53 unknown IDs). Hash those.
70
+ const rawId =
71
+ typeof item.candidate_id === 'string' && item.candidate_id ? item.candidate_id : String(item.url ?? '');
72
+ const idSeed = rawId.startsWith('http')
73
+ ? createHash('sha256').update(rawId).digest('hex').slice(0, 16)
74
+ : rawId;
75
+ const doc: Doc = {
76
+ id: `last30days:${idSeed}`,
77
+ body: String(item.summary ?? ''),
78
+ };
79
+ if (typeof item.title === 'string' && item.title) doc.title = item.title;
80
+ if (typeof item.url === 'string' && item.url) doc.sourceUri = item.url;
81
+ doc.mediaType = 'text/markdown';
82
+ doc.metadata = {
83
+ topic,
84
+ window_days: parsed.window_days,
85
+ cluster: item.cluster,
86
+ source: item.source,
87
+ engagement: item.engagement,
88
+ relevance_score: item.relevance_score,
89
+ published_at: item.published_at,
90
+ schema_version: parsed.schema_version,
91
+ };
92
+ docs.push(doc);
93
+ } catch (err: unknown) {
94
+ echoError(
95
+ `last30days-ingest: skipping unparseable item (${err instanceof Error ? err.message : String(err)})`,
96
+ );
97
+ }
98
+ }
99
+ return DocListSchema.parse(docs);
100
+ }
101
+
102
+ export interface IngestionOptions {
103
+ out: string;
104
+ }
105
+
106
+ export async function processIngestionIO(
107
+ options: IngestionOptions,
108
+ config: z.infer<typeof InputConfigSchema>,
109
+ runner: RunLast30Days = defaultRunner,
110
+ ): Promise<{ docs: Doc[] }> {
111
+ const fs = createNodeFileSystem();
112
+ const outDir = dirname(options.out);
113
+ if (outDir && outDir !== '.') await fs.ensureDir(outDir);
114
+ const capturePath = join(outDir || '.', '.last30days-agent.json');
115
+ const run = await runner(config, capturePath);
116
+ if (run.exitCode !== 0) {
117
+ throw new Error(`last30days exited ${run.exitCode}: ${run.stderr.trim() || 'no stderr'}`);
118
+ }
119
+ const docs = mapResultsToDocs(await readJsonFile(capturePath), config.topic);
120
+ await atomicWriteJson(options.out, docs, fs);
121
+ return { docs };
122
+ }
123
+
124
+ export async function main(runner?: RunLast30Days): Promise<number> {
125
+ let values: { in?: string; out?: string };
126
+ try {
127
+ ({ values } = parseArgs({ options: { in: { type: 'string' }, out: { type: 'string' } } }));
128
+ } catch (err: unknown) {
129
+ echoError(`last30days-ingest failed: ${err instanceof Error ? err.message : String(err)}`);
130
+ return 1;
131
+ }
132
+ if (!values.in || !values.out) {
133
+ echoError('last30days-ingest failed: Missing required arguments: --in <config.json> --out <docs.json>');
134
+ return 1;
135
+ }
136
+ try {
137
+ const config = InputConfigSchema.parse(await readJsonFile(values.in));
138
+ await processIngestionIO({ out: values.out }, config, runner);
139
+ return 0;
140
+ } catch (err: unknown) {
141
+ echoError(`last30days-ingest failed: ${err instanceof Error ? err.message : String(err)}`);
142
+ return 1;
143
+ }
144
+ }
145
+
146
+ if (import.meta.main) {
147
+ process.exit(await main());
148
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../../tooling/typescript/base.json",
3
+ "include": ["src", "tests"]
4
+ }
@@ -4,7 +4,7 @@ This directory is the **only** home for knowledge-kit agent capabilities (ADR-00
4
4
  It is a Claude Code / Superskill plugin, **not** a kk-core product plugin.
5
5
 
6
6
  | Path | Holds |
7
- |------|--------|
7
+ | ------ | -------- |
8
8
  | `skills/` | Fat skills (`SKILL.md`) |
9
9
  | `commands/` | Thin slash-command wrappers |
10
10
  | `agents/` | Thin subagent wrappers (currently empty — see below) |
@@ -24,5 +24,5 @@ Install: `superskill install kk`.
24
24
 
25
25
  Capability files **omit a leading `kk-`**. `superskill install` prefixes the plugin name, so
26
26
  `skills/content-judge` installs as `kk:content-judge` (not `kk:kk-content-judge`). Same for `topic`,
27
- `storm-research`, `itc-generating`, and `/workflow-run`. Product workflow YAML (`kk-storm-research.yaml`)
27
+ `storm-research`, `itc-generating`, `explain-things` (via `/tell-me`), and `/workflow-run`. Product workflow YAML (`kk-storm-research.yaml`)
28
28
  keeps its existing name — that is a workflow stem, not an installable agent capability.
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: tell-me
3
+ description: Explain the supplied or current topic with the smallest useful view; use ELI5 only when requested.
4
+ argument-hint: "[topic] [--eli5]"
5
+ allowed-tools: ["Skill"]
6
+ ---
7
+
8
+ # tell-me
9
+
10
+ Thin wrapper for the `explain-things` skill — all explanation and view-selection logic lives in
11
+ the skill. Forward `$ARGUMENTS` unchanged, including empty arguments (the skill owns
12
+ conversation-topic fallback).
13
+
14
+ ```text
15
+ Skill(skill="kk:explain-things", args="$ARGUMENTS")
16
+ ```
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: explain-things
3
+ description: >-
4
+ Explain a topic, mechanism, or change with the smallest useful view — pseudocode,
5
+ call tree, component tree, file tree, Mermaid, diff, copyable block, or a focused
6
+ HTML artifact as a last resort. Use when the user says "explain", "tell me",
7
+ "how does X work", "ELI5", "like I'm 5", or "show me". Normal explanations are
8
+ concise inline views; the ELI5 register is opt-in. This is not for content authoring,
9
+ research, judging, publishing, or package/code review workflows.
10
+ ---
11
+
12
+ # explain-things
13
+
14
+ Explain the topic with the smallest useful view and only the prose needed to read it.
15
+ Skip the preamble.
16
+
17
+ ## 1. Resolve the topic
18
+
19
+ `$ARGUMENTS` carries an optional standalone `--eli5` modifier; strip it if present, and the
20
+ remaining text is the explicit topic.
21
+
22
+ - Explicit topic present → use it.
23
+ - No explicit topic → use the current conversation topic.
24
+ - Neither → stop before creating any artifact and say exactly:
25
+
26
+ > Provide a topic, for example `/tell-me <topic>`, or ask again while a topic is active.
27
+
28
+ ## 2. Choose the register
29
+
30
+ - **Normal** (default): concise supporting prose beside one smallest useful inline view.
31
+ - **ELI5**: picture-first, few words — use only when `--eli5` was passed or the user explicitly
32
+ asks for ELI5 / "like I'm 5". Lead with a familiar analogy; define unavoidable jargon.
33
+ ELI5 changes vocabulary and presentation, not the view catalog, and never forces HTML.
34
+
35
+ ## 3. Pick the smallest view
36
+
37
+ Read `references/views.md` for the full decision catalog plus syntax examples. The default
38
+ mapping:
39
+
40
+ | Question shape | Smallest default view |
41
+ | --- | --- |
42
+ | Decision logic or algorithm | Pseudocode |
43
+ | In-process call order | Call tree |
44
+ | Multiple actors, timing, or data flow | Mermaid sequence/flow diagram |
45
+ | UI composition, state, or ownership | Component tree |
46
+ | File/module responsibility | Shallow file tree |
47
+ | Change to an existing known shape | Focused `diff` block |
48
+ | Mostly new, copyable target | Complete code/config block |
49
+ | Dense spatial layout/comparison inline forms cannot express | One focused HTML artifact |
50
+
51
+ Honor an explicitly requested format when feasible. Add a second view only when it answers a
52
+ distinct missing question.
53
+
54
+ ## 4. Render
55
+
56
+ Place the view directly beside only the prose it needs. If inline text, trees, code, diffs, and
57
+ Mermaid cannot make the point, write one focused HTML file (match the product's look, real labels,
58
+ desktop + mobile), then open it and report its path.
@@ -0,0 +1,95 @@
1
+ # View catalog
2
+
3
+ Decision table first; syntax sketches below. Pick **one** view that makes the point (a second
4
+ only for a distinct missing question). Provenance: distilled from `vendors/misc/eli5/SKILL.md`
5
+ (picture-first few-words register) and `vendors/misc/show-me/SKILL.md` (view forms) — read them
6
+ only to trace history; this file is the operative catalog.
7
+
8
+ | Question shape | View |
9
+ | --- | --- |
10
+ | Decision logic or algorithm | Pseudocode |
11
+ | In-process call order | Call tree |
12
+ | Multiple actors, timing, or data flow | Mermaid sequence/flow diagram |
13
+ | UI composition, state, or ownership | Component tree |
14
+ | File/module responsibility, broad refactor | Shallow file tree |
15
+ | Change to an existing known shape | Focused `diff` block |
16
+ | Mostly new, copyable target | Complete code/config block |
17
+ | Dense spatial layout/comparison the inline forms cannot express | One focused HTML artifact |
18
+
19
+ ## Syntax sketches
20
+
21
+ Pseudocode — logic, not language:
22
+
23
+ ```text
24
+ on(save)
25
+ if content is unchanged
26
+ return cached result
27
+ write new content
28
+ return fresh result
29
+ ```
30
+
31
+ Call tree — who calls whom:
32
+
33
+ ```text
34
+ submitForm
35
+ createSession
36
+ persistPrompt
37
+ launchAgent
38
+ navigateToSession
39
+ ```
40
+
41
+ Component tree — composition, with state/module boundaries that matter:
42
+
43
+ ```tsx
44
+ <SessionPage> (apps/example/src/routes/session.tsx)
45
+ useSessionEvents()
46
+ <SessionToolbar>
47
+ <RunSkillButton> (packages/ui)
48
+ ```
49
+
50
+ File tree — responsibilities at one level:
51
+
52
+ ```text
53
+ src/
54
+ ├── commands/ # parses user actions
55
+ ├── sessions/ # owns session state
56
+ └── transport/ # sends API requests
57
+ ```
58
+
59
+ Mermaid — interaction/timing across actors:
60
+
61
+ ```mermaid
62
+ sequenceDiagram
63
+ participant User
64
+ participant UI
65
+ participant Daemon
66
+ User->>UI: choose command
67
+ UI->>Daemon: send expanded prompt
68
+ Daemon-->>UI: stream result
69
+ ```
70
+
71
+ Diff — match its shape to the topic: component change, file-layout change, call-tree change, or
72
+ state/control-flow change:
73
+
74
+ ```diff
75
+ on(save)
76
+ - write content
77
+ + if content is unchanged
78
+ + return cached result
79
+ + write new content
80
+ + invalidate cache
81
+ ```
82
+
83
+ Copyable block — most of it is new, omitted context would hide ownership/order, or the user needs
84
+ a target shape to paste:
85
+
86
+ ```ts
87
+ function expandSkill(command: string): string {
88
+ const skillName = command.slice(1);
89
+ return `use the ${skillName} skill`;
90
+ }
91
+ ```
92
+
93
+ HTML artifact — last resort only. For a visual too dense for Mermaid (layout walk-through,
94
+ before/after comparison, short slide deck): one focused HTML file matching the product's colors,
95
+ type, spacing, and components; real labels and data; desktop + mobile. Open it and report the path.