@gobing-ai/knowledge-kit 0.0.7 → 0.0.8

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 (48) hide show
  1. package/dist/index.js +21639 -11931
  2. package/package.json +4 -1
  3. package/plugins/generations/content-gen/package.json +2 -1
  4. package/plugins/generations/content-gen/src/index.ts +9 -8
  5. package/plugins/generations/content-gen/src/storm.ts +12 -4
  6. package/plugins/generations/voice-gen/package.json +17 -0
  7. package/plugins/generations/voice-gen/plugin.json +6 -0
  8. package/plugins/generations/voice-gen/src/concat.ts +218 -0
  9. package/plugins/generations/voice-gen/src/index.ts +213 -0
  10. package/plugins/generations/voice-gen/src/voicebox-client.ts +223 -0
  11. package/plugins/generations/voice-gen/src/voicescript.ts +365 -0
  12. package/plugins/generations/voice-gen/tsconfig.json +8 -0
  13. package/plugins/ingestions/karakeep-local/package.json +17 -0
  14. package/plugins/ingestions/karakeep-local/src/index.ts +31 -26
  15. package/plugins/ingestions/karakeep-local/tsconfig.json +4 -0
  16. package/plugins/ingestions/web-search/package.json +4 -1
  17. package/plugins/ingestions/web-search/src/index.ts +137 -15
  18. package/plugins/kk/README.md +9 -3
  19. package/plugins/kk/commands/workflow-run.md +100 -28
  20. package/plugins/kk/config.example.yaml +34 -0
  21. package/plugins/kk/scripts/render-md.ts +8 -3
  22. package/plugins/kk/skills/{judge → content-judge}/SKILL.md +13 -14
  23. package/plugins/kk/skills/{judge → content-judge}/references/workflow-integration.md +13 -11
  24. package/plugins/kk/skills/itc-generating/SKILL.md +147 -0
  25. package/plugins/kk/skills/itc-generating/references/generic-craft.md +80 -0
  26. package/plugins/kk/skills/itc-generating/references/platform-english.md +72 -0
  27. package/plugins/kk/skills/itc-generating/references/platform-wechat.md +60 -0
  28. package/plugins/kk/skills/itc-generating/references/skill-authoring.md +62 -0
  29. package/plugins/kk/skills/storm-research/SKILL.md +10 -3
  30. package/plugins/kk/workflows/judge-gated-publish-example.yaml +101 -0
  31. package/plugins/kk/workflows/kk-ingest-generate-publish.yaml +72 -0
  32. package/plugins/kk/workflows/kk-itc.yaml +285 -0
  33. package/plugins/kk/workflows/kk-solo-podcast.yaml +374 -0
  34. package/plugins/kk/workflows/validate-voicescript.ts +226 -0
  35. package/plugins/publishings/emdash-pub/package.json +17 -0
  36. package/plugins/publishings/emdash-pub/plugin.json +7 -0
  37. package/plugins/publishings/emdash-pub/src/index.ts +450 -0
  38. package/plugins/publishings/emdash-pub/tsconfig.json +4 -0
  39. package/plugins/publishings/qiita-pub/package.json +2 -1
  40. package/plugins/publishings/qiita-pub/src/index.ts +9 -9
  41. package/plugins/publishings/surfdash-pub/package.json +2 -1
  42. package/plugins/publishings/surfdash-pub/src/index.ts +16 -11
  43. package/plugins/publishings/zenn-pub/package.json +2 -1
  44. package/plugins/publishings/zenn-pub/src/index.ts +11 -11
  45. package/plugins/kk/agents/judge-compliance.md +0 -37
  46. package/plugins/kk/agents/judge-tech.md +0 -35
  47. package/plugins/kk/agents/judge-tone.md +0 -37
  48. /package/plugins/kk/skills/{judge → content-judge}/references/rubrics.md +0 -0
@@ -1,8 +1,12 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
3
- import { dirname, join, relative } from 'node:path';
2
+ import { dirname, relative } from 'node:path';
4
3
  import { parseArgs } from 'node:util';
5
4
  import type { Doc } from '@gobing-ai/kk-core';
5
+ import { getLogger, initializeLogger } from '@gobing-ai/ts-infra';
6
+ import { createNodeFileSystem, walkDir } from '@gobing-ai/ts-runtime';
7
+ import { echoError } from '@gobing-ai/ts-utils';
8
+
9
+ const log = getLogger('kk.ingestion.karakeep-local');
6
10
 
7
11
  interface ParsedMeta {
8
12
  title?: string;
@@ -17,27 +21,26 @@ function computeHash(str: string): string {
17
21
  }
18
22
 
19
23
  async function collectFiles(dir: string): Promise<string[]> {
20
- const entries = await readdir(dir, { recursive: true });
21
- const files: string[] = [];
22
- for (const entry of entries) {
23
- const fullPath = join(dir, entry);
24
- const st = await stat(fullPath);
25
- if (st.isFile()) {
26
- files.push(fullPath);
27
- }
28
- }
29
- return files.sort();
24
+ // walkDir is cycle-safe and root-confined: a directory symlink pointing back at
25
+ // an ancestor is walked at most once, and one escaping `dir` is skipped.
26
+ return (await walkDir(dir, createNodeFileSystem())).sort();
30
27
  }
31
28
 
32
29
  export async function processKarakeepFolder(inDir: string): Promise<Doc[]> {
30
+ const fs = createNodeFileSystem();
33
31
  try {
34
- const inStat = await stat(inDir);
32
+ // The FileSystem seam returns null for a missing path where node's stat throws;
33
+ // both cases must still surface as "Inaccessible source path" below.
34
+ const inStat = await fs.stat(inDir);
35
+ if (inStat === null) {
36
+ throw new Error(`Source path does not exist: ${inDir}`);
37
+ }
35
38
  if (!inStat.isDirectory()) {
36
39
  throw new Error(`Source path is not a directory: ${inDir}`);
37
40
  }
38
41
  } catch (err: unknown) {
39
42
  const msg = `Inaccessible source path: ${inDir}`;
40
- console.error(`Error: ${msg}`, err);
43
+ echoError(`Error: ${msg}`);
41
44
  throw new Error(msg, { cause: err });
42
45
  }
43
46
 
@@ -62,14 +65,14 @@ export async function processKarakeepFolder(inDir: string): Promise<Doc[]> {
62
65
 
63
66
  if (contentMd) {
64
67
  processedFiles.add(contentMd);
65
- const mdText = await readFile(contentMd, 'utf-8');
68
+ const mdText = await fs.readFile(contentMd);
66
69
  let metaObj: ParsedMeta = {};
67
70
  if (metaJson) {
68
71
  processedFiles.add(metaJson);
69
72
  try {
70
- metaObj = JSON.parse(await readFile(metaJson, 'utf-8')) as ParsedMeta;
73
+ metaObj = JSON.parse(await fs.readFile(metaJson)) as ParsedMeta;
71
74
  } catch (err: unknown) {
72
- console.error(`Warning: corrupt/unparsable meta file at ${metaJson}`, err);
75
+ log.warn(`Warning: corrupt/unparsable meta file at ${metaJson}`, { error: err });
73
76
  }
74
77
  }
75
78
 
@@ -99,7 +102,7 @@ export async function processKarakeepFolder(inDir: string): Promise<Doc[]> {
99
102
 
100
103
  if (f.endsWith('.json')) {
101
104
  try {
102
- const jsonText = await readFile(f, 'utf-8');
105
+ const jsonText = await fs.readFile(f);
103
106
  const jsonObj = JSON.parse(jsonText) as ParsedMeta;
104
107
  const { title, sourceUri, mediaType, body, ...extraMeta } = jsonObj;
105
108
  if (typeof body === 'string') {
@@ -112,13 +115,13 @@ export async function processKarakeepFolder(inDir: string): Promise<Doc[]> {
112
115
  metadata: Object.keys(extraMeta).length > 0 ? extraMeta : undefined,
113
116
  });
114
117
  } else {
115
- console.error(`Warning: JSON entry missing string body at ${f}`);
118
+ log.warn(`Warning: JSON entry missing string body at ${f}`);
116
119
  }
117
120
  } catch (err: unknown) {
118
- console.error(`Warning: corrupt/unparsable JSON file at ${f}`, err);
121
+ log.warn(`Warning: corrupt/unparsable JSON file at ${f}`, { error: err });
119
122
  }
120
123
  } else if (f.endsWith('.md')) {
121
- const mdText = await readFile(f, 'utf-8');
124
+ const mdText = await fs.readFile(f);
122
125
  docs.push({
123
126
  id,
124
127
  body: mdText,
@@ -140,31 +143,33 @@ export async function main() {
140
143
  });
141
144
 
142
145
  if (!values.in || !values.out) {
143
- console.error('Error: Missing required arguments --in or --out');
146
+ echoError('Error: Missing required arguments --in or --out');
144
147
  process.exit(1);
145
148
  }
146
149
 
150
+ const fs = createNodeFileSystem();
147
151
  try {
148
152
  const candidateFiles = await collectFiles(values.in);
149
153
  const docs = await processKarakeepFolder(values.in);
150
154
 
151
155
  if (docs.length === 0 && candidateFiles.length > 0) {
152
- console.error(`Fatal: source yielded zero ingestible documents at ${values.in}`);
156
+ echoError(`Fatal: source yielded zero ingestible documents at ${values.in}`);
153
157
  process.exit(1);
154
158
  }
155
159
 
156
160
  const outDir = dirname(values.out);
157
161
  if (outDir && outDir !== '.') {
158
- await mkdir(outDir, { recursive: true });
162
+ await fs.ensureDir(outDir);
159
163
  }
160
164
 
161
- await writeFile(values.out, JSON.stringify(docs, null, 2), 'utf-8');
165
+ await fs.writeFile(values.out, JSON.stringify(docs, null, 2));
162
166
  } catch (err: unknown) {
163
- console.error('Fatal error during karakeep-local ingestion:', err);
167
+ echoError(`Fatal error during karakeep-local ingestion: ${err instanceof Error ? err.message : String(err)}`);
164
168
  process.exit(1);
165
169
  }
166
170
  }
167
171
 
168
172
  if (import.meta.main) {
173
+ await initializeLogger({ console: process.env.NODE_ENV !== 'test', json: true, level: 'warn' });
169
174
  main();
170
175
  }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../../tooling/typescript/base.json",
3
+ "include": ["src", "tests"]
4
+ }
@@ -6,7 +6,10 @@
6
6
  "typecheck": "tsc --noEmit"
7
7
  },
8
8
  "dependencies": {
9
- "@gobing-ai/kk-core": "workspace:*"
9
+ "@gobing-ai/kk-core": "workspace:*",
10
+ "@gobing-ai/ts-infra": "catalog:",
11
+ "@gobing-ai/ts-runtime": "catalog:",
12
+ "@gobing-ai/ts-utils": "catalog:"
10
13
  },
11
14
  "devDependencies": {
12
15
  "@types/bun": "1.3.14"
@@ -1,8 +1,12 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
- import { dirname } from 'node:path';
2
+ import { dirname, join } from 'node:path';
4
3
  import { parseArgs } from 'node:util';
5
4
  import { type Doc, DocListSchema } from '@gobing-ai/kk-core';
5
+ import { getLogger, initializeLogger } from '@gobing-ai/ts-infra';
6
+ import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
7
+ import { echoError } from '@gobing-ai/ts-utils';
8
+
9
+ const log = getLogger('kk.ingestion.web-search');
6
10
 
7
11
  /** Validated `--in` input per the 0054 contract (`topic` required; `maxResults`/`fixture` optional). */
8
12
  interface InInput {
@@ -49,13 +53,49 @@ export function computeId(uri: string): string {
49
53
  /** Fetch signature the live path accepts, so tests can inject a fake (CI never hits the network). */
50
54
  export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
51
55
 
52
- /** Live-path dependencies — fetch, env, and clock are all injectable for tests. */
56
+ /** markitdown normalizer — HTML in, markdown out (ADR-013 D3). Throws on non-zero exit or
57
+ * empty output; callers treat a throw as unusable content (per-URL fallback, D5). */
58
+ export type MarkitdownRunner = (html: string) => Promise<string>;
59
+
60
+ /** Live-path dependencies — fetch, env, clock, scrape, and markitdown are all injectable for tests. */
53
61
  export interface SearchDeps {
54
62
  fetch: FetchLike;
55
63
  env: Record<string, string | undefined>;
56
64
  now: () => Date;
65
+ /** Scrape transport; defaults to `deps.fetch` so tests can route both Firecrawl calls separately. */
66
+ scrape?: FetchLike;
67
+ /** markitdown runner; when absent in live mode, `main` probes PATH and fails loud (R6). */
68
+ markitdown?: MarkitdownRunner;
57
69
  }
58
70
 
71
+ /** Real markitdown runner: `markitdown -x .html`, HTML on stdin, markdown on stdout. The
72
+ * `-x .html` extension hint is required — bare stdin passes HTML through unconverted.
73
+ * Spawns the absolute path resolved via Bun.which (with an explicit PATH option — plain
74
+ * argv[0] lookup can resolve against a startup PATH snapshot, missing runtime edits). */
75
+ export const realMarkitdown: MarkitdownRunner = async (html: string): Promise<string> => {
76
+ const bin = Bun.which('markitdown', { PATH: process.env.PATH });
77
+ if (!bin) {
78
+ throw new Error('markitdown is not on PATH');
79
+ }
80
+ const proc = Bun.spawn([bin, '-x', '.html'], { stdin: 'pipe', stdout: 'pipe', stderr: 'pipe' });
81
+ proc.stdin.write(html);
82
+ await proc.stdin.end();
83
+ const [stdout, stderrText, exitCode] = await Promise.all([
84
+ new Response(proc.stdout).text(),
85
+ new Response(proc.stderr).text(),
86
+ proc.exited,
87
+ ]);
88
+ if (exitCode !== 0) {
89
+ const detail = stderrText.trim();
90
+ throw new Error(`markitdown exited ${exitCode}${detail ? `: ${detail}` : ''}`);
91
+ }
92
+ const markdown = stdout.trim();
93
+ if (markdown.length === 0) {
94
+ throw new Error('markitdown produced empty output');
95
+ }
96
+ return markdown;
97
+ };
98
+
59
99
  /** One Firecrawl `data.web[]` item — the subset of fields 0054 Block 3/4 consume. */
60
100
  interface FirecrawlWebItem {
61
101
  url?: unknown;
@@ -70,6 +110,12 @@ interface FirecrawlSearchResponse {
70
110
  data?: { web?: FirecrawlWebItem[] | null } | null;
71
111
  }
72
112
 
113
+ /** One Firecrawl `/v2/scrape` body — the subset the cache pass consumes. */
114
+ interface FirecrawlScrapeResponse {
115
+ success?: unknown;
116
+ data?: { html?: unknown; markdown?: unknown } | null;
117
+ }
118
+
73
119
  /**
74
120
  * Live Firecrawl search → Doc[] (0054 Blocks 2–5). Throws an Error whose message is the
75
121
  * fail-loud diagnostic; `main` prefixes it with `web-search failed: ` and returns 1.
@@ -152,7 +198,7 @@ export async function searchFirecrawl(input: InInput, deps: SearchDeps): Promise
152
198
  if (!url || body.length === 0) {
153
199
  // Row 7 — partial item failure: skip + stderr warn, continue.
154
200
  skipped += 1;
155
- console.warn(`web-search: skipping result without usable markdown (url=${url ?? 'missing'})`);
201
+ log.warn(`web-search: skipping result without usable markdown (url=${url ?? 'missing'})`);
156
202
  continue;
157
203
  }
158
204
  docs.push({
@@ -195,6 +241,72 @@ function extractError(bodyText: string): string {
195
241
  return '';
196
242
  }
197
243
 
244
+ /** Scrape + normalize + cache pass over the search results (ADR-013 D1–D6). Sequential
245
+ * per Doc — deliberate, no parallel fan-out (rate-limit friendly, deterministic). Cache hit
246
+ * (`references/<doc-id>.md` exists) reuses the file with no new /v2/scrape call (R3/R4);
247
+ * otherwise scrape → normalize (html via markitdown, markdown as-is) → write cache → body
248
+ * swap. Per-URL failure or unusable content warns and keeps the snippet body (R5/R11) — never
249
+ * fatal. Returns a new Doc[]; input order and non-body fields are untouched. */
250
+ export async function scrapeAndCacheDocs(docs: Doc[], deps: SearchDeps, outPath: string): Promise<Doc[]> {
251
+ const fs = createNodeFileSystem();
252
+ const cacheDir = join(dirname(outPath), 'references');
253
+ const scrapeFetch = deps.scrape ?? deps.fetch;
254
+ const out: Doc[] = [];
255
+ for (const doc of docs) {
256
+ const uri = doc.sourceUri;
257
+ if (!uri) {
258
+ out.push(doc);
259
+ continue;
260
+ }
261
+ const cacheFile = join(cacheDir, `${doc.id}.md`);
262
+ try {
263
+ const cached = await fs.readFile(cacheFile);
264
+ if (cached.trim().length > 0) {
265
+ out.push({ ...doc, body: cached });
266
+ continue;
267
+ }
268
+ } catch {
269
+ // Cache miss — fall through to the live scrape.
270
+ }
271
+ try {
272
+ const key = deps.env.FIRECRAWL_API_KEY;
273
+ if (!key) throw new Error('FIRECRAWL_API_KEY is unset');
274
+ const res = await scrapeFetch('https://api.firecrawl.dev/v2/scrape', {
275
+ method: 'POST',
276
+ headers: {
277
+ 'Content-Type': 'application/json',
278
+ Authorization: `Bearer ${key}`,
279
+ },
280
+ body: JSON.stringify({ url: uri, formats: ['html', 'markdown'] }),
281
+ });
282
+ if (!res.ok || res.status !== 200) {
283
+ throw new Error(`Firecrawl HTTP ${res.status}`);
284
+ }
285
+ const parsed = JSON.parse(await res.text()) as FirecrawlScrapeResponse;
286
+ if (parsed.success !== true || parsed.data === null || parsed.data === undefined) {
287
+ throw new Error('Firecrawl malformed scrape response');
288
+ }
289
+ const html = typeof parsed.data.html === 'string' ? parsed.data.html : undefined;
290
+ const markdown = typeof parsed.data.markdown === 'string' ? parsed.data.markdown : undefined;
291
+ let normalized: string;
292
+ if (html && html.trim().length > 0) {
293
+ normalized = await (deps.markitdown ?? realMarkitdown)(html);
294
+ } else if (markdown && markdown.trim().length > 0) {
295
+ normalized = markdown.trim();
296
+ } else {
297
+ throw new Error('scrape returned no usable content');
298
+ }
299
+ await fs.ensureDir(cacheDir);
300
+ await fs.writeFile(cacheFile, normalized);
301
+ out.push({ ...doc, body: normalized });
302
+ } catch (err: unknown) {
303
+ log.warn(`web-search: scrape failed (url=${uri}): ${err instanceof Error ? err.message : String(err)}`);
304
+ out.push(doc);
305
+ }
306
+ }
307
+ return out;
308
+ }
309
+
198
310
  /**
199
311
  * Build the fixture `Doc[]` for a topic (0054 Block 4 mapping). Always emits
200
312
  * canned docs — the v1 plugin has no live Firecrawl path.
@@ -245,6 +357,7 @@ export function parseInput(raw: string): InInput {
245
357
  }
246
358
 
247
359
  export async function main(): Promise<number> {
360
+ const fs = createNodeFileSystem();
248
361
  const { values } = parseArgs({
249
362
  options: {
250
363
  in: { type: 'string' },
@@ -253,34 +366,42 @@ export async function main(): Promise<number> {
253
366
  });
254
367
 
255
368
  if (!values.in || !values.out) {
256
- console.error('Error: Missing required arguments --in and --out');
369
+ echoError('Error: Missing required arguments --in and --out');
257
370
  return 1;
258
371
  }
259
372
 
260
373
  let input: InInput;
261
374
  try {
262
- input = parseInput(await readFile(values.in, 'utf-8'));
375
+ input = parseInput(await fs.readFile(values.in));
263
376
  } catch (err: unknown) {
264
- console.error(`web-search failed: ${err instanceof Error ? err.message : String(err)}`);
377
+ echoError(`web-search failed: ${err instanceof Error ? err.message : String(err)}`);
265
378
  return 1;
266
379
  }
267
380
 
268
381
  if (!input.fixture) {
269
382
  // 0054 Block 1/Block 8: `fixture` absent/false = LIVE Firecrawl path.
270
383
  try {
271
- const docs = await searchFirecrawl(input, {
384
+ const deps: SearchDeps = {
272
385
  fetch: globalThis.fetch,
273
386
  env: process.env,
274
387
  now: () => new Date(),
275
- });
388
+ };
389
+ // Bun.which with an explicit PATH — without the option it can ignore runtime
390
+ // process.env.PATH changes (observed on Bun 1.3.14).
391
+ if (!deps.markitdown && !Bun.which('markitdown', { PATH: deps.env.PATH })) {
392
+ // R6 — live mode requires the normalizer; fail before any network call.
393
+ throw new Error('markitdown is not on PATH');
394
+ }
395
+ const docs = await searchFirecrawl(input, deps);
396
+ const cached = await scrapeAndCacheDocs(docs, deps, values.out);
276
397
  const outDir = dirname(values.out);
277
398
  if (outDir && outDir !== '.') {
278
- await mkdir(outDir, { recursive: true });
399
+ await fs.ensureDir(outDir);
279
400
  }
280
- await writeFile(values.out, JSON.stringify(DocListSchema.parse(docs), null, 2), 'utf-8');
401
+ await fs.writeFile(values.out, JSON.stringify(DocListSchema.parse(cached), null, 2));
281
402
  return 0;
282
403
  } catch (err: unknown) {
283
- console.error(`web-search failed: ${err instanceof Error ? err.message : String(err)}`);
404
+ echoError(`web-search failed: ${err instanceof Error ? err.message : String(err)}`);
284
405
  return 1;
285
406
  }
286
407
  }
@@ -289,16 +410,17 @@ export async function main(): Promise<number> {
289
410
  const docs = DocListSchema.parse(buildFixtureDocs(input.topic, input.maxResults));
290
411
  const outDir = dirname(values.out);
291
412
  if (outDir && outDir !== '.') {
292
- await mkdir(outDir, { recursive: true });
413
+ await fs.ensureDir(outDir);
293
414
  }
294
- await writeFile(values.out, JSON.stringify(docs, null, 2), 'utf-8');
415
+ await fs.writeFile(values.out, JSON.stringify(docs, null, 2));
295
416
  return 0;
296
417
  } catch (err: unknown) {
297
- console.error(`web-search failed: ${err instanceof Error ? err.message : String(err)}`);
418
+ echoError(`web-search failed: ${err instanceof Error ? err.message : String(err)}`);
298
419
  return 1;
299
420
  }
300
421
  }
301
422
 
302
423
  if (import.meta.main) {
424
+ await initializeLogger({ console: process.env.NODE_ENV !== 'test', json: true, level: 'warn' });
303
425
  process.exit(await main());
304
426
  }
@@ -7,9 +7,15 @@ It is a Claude Code / Superskill plugin, **not** a kk-core product plugin.
7
7
  |------|--------|
8
8
  | `skills/` | Fat skills (`SKILL.md`) |
9
9
  | `commands/` | Thin slash-command wrappers |
10
- | `agents/` | Thin subagent wrappers |
10
+ | `agents/` | Thin subagent wrappers (currently empty — see below) |
11
11
  | `hooks/` | Hook definitions |
12
12
  | `rules/` | Agent rules |
13
+ | `workflows/` | Design-time product YAML SSOT (ADR-009). Runtime dest is `$HOME/.config/kk/workflows` via `/workflow-run` |
14
+
15
+ `agents/` is reserved for thin wrappers of fat skills. It is empty: `judge-tech` /
16
+ `judge-tone` / `judge-compliance` were retired (2026-08-19). Rubric selection lives on
17
+ `content-judge` (`agent.run` prompt or `--judge` on `itc-generating`). Do not re-add a
18
+ persona wrapper unless a new skill owns a distinct run procedure.
13
19
 
14
20
  `discoverPlugins` skips this directory. Product plugins (`ingestion` / `generator` /
15
21
  `publish`) live under `plugins/ingestions|generations|publishings/<name>/`.
@@ -17,6 +23,6 @@ It is a Claude Code / Superskill plugin, **not** a kk-core product plugin.
17
23
  Install: `superskill install kk`.
18
24
 
19
25
  Capability files **omit a leading `kk-`**. `superskill install` prefixes the plugin name, so
20
- `skills/judge` installs as `kk:judge` (not `kk:kk-judge`). Same for `topic`, `storm-research`,
21
- `/workflow-run`, and the `judge-*` subagents. Product workflow YAML (`kk-storm-research.yaml`)
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`)
22
28
  keeps its existing name — that is a workflow stem, not an installable agent capability.
@@ -1,18 +1,20 @@
1
1
  ---
2
2
  name: workflow-run
3
3
  description: >-
4
- Run STORM daily research end to end — a topic sentence XOR a markdown file becomes a
5
- grounded content.md report. Installs the workflow YAML on first run (0056 copy rule),
6
- creates the config with defaults if missing, computes the topic workspace, and shells
7
- spur workflow run against the installed workflow.
8
- argument-hint: "[name] <topic|--in file> [--fixture] [--force]"
4
+ Run STORM daily research or IT content authoring end to end. Installs the workflow
5
+ YAML on first run (two-root copy rule), creates the config with defaults if missing,
6
+ computes the workspace, and shells spur workflow run against the installed workflow.
7
+ argument-hint: "[name] <topic|--in file> [--dir <path>] [--playbook generic|english|wechat] [--research] [--judge] [--outline <a|b|c>] [--writer itc-generating|topic] [--duration <min>] [--language <code>] [--script-approved] [--fixture] [--force]"
9
8
  ---
10
9
 
11
- Thin wrapper around the **storm-research** skill. Read
12
- `plugins/kk/skills/storm-research/SKILL.md` first — it is the craft SSOT (query framing,
13
- evidence verification, better-report practices, fail-loud rows). This command owns the run
14
- procedure: config create, YAML install, topic-id computation, and the `spur workflow run`.
15
- No new `kk` CLI noun (ADR-011) — everything here is shell + the workflow.
10
+ Thin runner around the **storm-research**, **itc-generating** / **topic**, and **kk-solo-podcast**
11
+ capabilities. Read `plugins/kk/skills/storm-research/SKILL.md` or
12
+ `plugins/kk/skills/itc-generating/SKILL.md` for craft; solo-podcast craft is inlined in the
13
+ workflow `agent.run` prompt (no fat skill). This command owns the run procedure: config create,
14
+ YAML install (two-root copy rule) into `$HOME/.config/kk/workflows`, workspace computation, and
15
+ the `spur workflow run` of that **runtime** dest. Design-time SSOT remains
16
+ `plugins/kk/workflows/` in the package/repo. No new `kk` CLI noun (ADR-011) — everything here
17
+ is shell + the workflow.
16
18
 
17
19
  **Args:** `$ARGUMENTS`
18
20
 
@@ -21,14 +23,25 @@ No new `kk` CLI noun (ADR-011) — everything here is shell + the workflow.
21
23
  Parse per the frozen rule:
22
24
 
23
25
  - First token is the workflow **name** when it matches `^[a-z0-9][a-z0-9-]*$` and is not `--in`;
24
- otherwise `name=kk-storm-research` and that token is the topic.
25
- - `--in <file>` → file mode. `--fixture` → force `fixture: "true"`. `--force` → overwrite an
26
- existing dest YAML.
26
+ otherwise `name=kk-storm-research` and that token is the topic. Supported profile names:
27
+ `kk-storm-research` (default), `kk-itc`, `kk-solo-podcast`. Any other name after successful copy → exit 1 listing
28
+ `kk-storm-research`, `kk-itc`, `kk-solo-podcast`.
29
+ - `--in <file>` → file mode. `--force` → overwrite an existing dest YAML / existing `brief.md`.
30
+ - Profile `kk-storm-research`: `--fixture` → force `fixture: "true"`.
31
+ - Profile `kk-itc`: `--dir <path>` (default `./<kebab>`), `--playbook generic|english|wechat` (default `generic`),
32
+ `--research` (`true`|`false`, default `false`), `--judge` (`true`|`false`, default `false`),
33
+ `--outline <a|b|c>` (default empty), `--writer itc-generating|topic` (default `itc-generating`).
34
+ `--fixture` passed with `kk-itc` or `kk-solo-podcast` → exit 1 (`fixture is storm-only`).
35
+ - Profile `kk-solo-podcast`: `--dir <path>` (default `$works_dir/<kebab>`), `--outline <a|b|c>`
36
+ (default empty), `--duration <min>` (default `8`), `--language <code>` (default `en`),
37
+ `--script-approved` (sets `script_approved=true`; default off). `--fixture` is storm-only.
27
38
  - **XOR:** exactly one of `<topic>` / `--in <file>`. Both set or both empty → exit 1, stderr
28
39
  states the XOR rule, no writes.
29
40
 
30
41
  Resolve these before step 2: `NAME`, `TOPIC`, `INPUT_FILE` (empty in sentence mode), `FIXTURE`
31
- (`true`|`false`), `FORCE`.
42
+ (`true`|`false`), `FORCE` (`true`|`false`), `DIR`, `PLAYBOOK`, `RESEARCH`, `JUDGE`, `OUTLINE`,
43
+ `WRITER`, `DURATION` (`8`), `LANGUAGE` (`en`), `SCRIPT_APPROVED` (`true`|`false`),
44
+ `VOICE_PROFILE` (from `VOICEBOX_DEFAULT_PROFILE`, default empty).
32
45
 
33
46
  ## 2. Resolve the config
34
47
 
@@ -68,28 +81,43 @@ from config — env-only.
68
81
 
69
82
  1. `$KK_WORKFLOWS_SOURCE/$NAME.yaml`
70
83
  2. `plugins/kk/workflows/$NAME.yaml`
71
- 3. `.spur/workflows/$NAME.yaml`
72
84
 
73
- - `dest` missing → copy from the first hit. Source missing from all three → exit 1, stderr
74
- lists the roots searched.
85
+ - `dest` missing → copy from the first hit. Source missing from both roots → exit 1, stderr
86
+ lists the roots searched: `no source workflow for $NAME (searched KK_WORKFLOWS_SOURCE, plugins/kk/workflows)`.
75
87
  - `dest` exists + `--force` → overwrite from source.
76
88
  - `dest` exists + identical bytes → no-op.
77
89
  - `dest` exists + different bytes → **warn and leave** (run proceeds with the user's copy).
78
90
 
79
91
  ```bash
80
92
  src=""
81
- for d in "$KK_WORKFLOWS_SOURCE" "plugins/kk/workflows" ".spur/workflows"; do
93
+ for d in "$KK_WORKFLOWS_SOURCE" "plugins/kk/workflows"; do
82
94
  [ -n "$d" ] && [ -f "$d/$NAME.yaml" ] && { src="$d/$NAME.yaml"; break; }
83
95
  done
84
- [ -z "$src" ] && { echo "no source workflow for $NAME (searched KK_WORKFLOWS_SOURCE, plugins/kk/workflows, .spur/workflows)" >&2; exit 1; }
96
+ [ -z "$src" ] && { echo "no source workflow for $NAME (searched KK_WORKFLOWS_SOURCE, plugins/kk/workflows)" >&2; exit 1; }
85
97
  if [ ! -f "$dest" ] || [ "$FORCE" = true ]; then
86
98
  install -d "$(dirname "$dest")" && cp "$src" "$dest"
87
99
  elif ! cmp -s "$src" "$dest"; then
88
100
  echo "warning: $dest differs from install source; leaving user copy (pass --force to replace)" >&2
89
101
  fi
102
+ # Runtime dest is $workflows_dir (default ~/.config/kk/workflows). Copy YAML sidecars
103
+ # that the machine shells (kk-solo-podcast: validate-voicescript.ts).
104
+ if [ "$NAME" = "kk-solo-podcast" ] && [ -n "$src" ]; then
105
+ side="validate-voicescript.ts"
106
+ sdir=$(dirname "$src")
107
+ ddir=$(dirname "$dest")
108
+ if [ -f "$sdir/$side" ]; then
109
+ if [ ! -f "$ddir/$side" ] || [ "$FORCE" = true ]; then
110
+ cp "$sdir/$side" "$ddir/$side"
111
+ elif ! cmp -s "$sdir/$side" "$ddir/$side"; then
112
+ echo "warning: $ddir/$side differs from install source; leaving user copy (pass --force to replace)" >&2
113
+ fi
114
+ fi
115
+ fi
90
116
  ```
91
117
 
92
- ## 4. Compute the topic workspace (0056 `topicId`)
118
+ ## 4. Compute workspace
119
+
120
+ ### Profile `kk-storm-research` (0056 `topicId`)
93
121
 
94
122
  `raw` = `TOPIC` (sentence mode) or the first ATX H1 (`/^#\s+(.+)$/m`) else basename without
95
123
  extension (`--in` mode). Then:
@@ -110,21 +138,65 @@ work_dir="$works_dir/$topic_id"
110
138
  mkdir -p "$work_dir"
111
139
  ```
112
140
 
141
+ ### Profile `kk-itc` (authoring workspace)
142
+
143
+ `raw` = `TOPIC` or file-mode first ATX H1 else basename without extension.
144
+ `kebab` = lower-case, `[^a-z0-9]+` → `-`, trimmed dashes (no length cap, no sha256 hash).
145
+ `work_dir="${DIR:-./$kebab}"`. Empty derived `kebab` → exit 1.
146
+
147
+ ### Profile `kk-solo-podcast`
148
+
149
+ `raw` = `TOPIC` or file-mode first ATX H1 else basename without extension.
150
+ `kebab` = lower-case, `[^a-z0-9]+` → `-`, trimmed dashes (no length cap, no sha256 hash).
151
+ `work_dir="${DIR:-$works_dir/$kebab}"`. Empty derived `kebab` → exit 1.
152
+
113
153
  ## 5. Run the workflow
114
154
 
115
- All values are strings. Bind the vars first: `maxResults` and `fixture` from step 2;
116
- `plugins_path` defaults to `./plugins` in a checkout, else the installed package `plugins/`
117
- path or `KK_PLUGIN_PATH`; `render_script` stays `plugins/kk/scripts/render-md.ts`.
155
+ All values are strings.
156
+
157
+ ### Profile `kk-storm-research`
158
+
159
+ Bind vars: `maxResults` and `fixture` from step 2; `plugins_path` defaults to `./plugins`
160
+ in a checkout, else installed package `plugins/` or `KK_PLUGIN_PATH`; `render_script` is
161
+ `plugins/kk/scripts/render-md.ts`.
118
162
 
119
163
  ```bash
120
164
  spur workflow run "$dest" --vars \
121
165
  "{\"topic\":\"$TOPIC\",\"input_file\":\"$INPUT_FILE\",\"maxResults\":\"$maxResults\",\"fixture\":\"$FIXTURE\",\"work_dir\":\"$work_dir\",\"plugins_path\":\"$plugins_path\",\"render_script\":\"$render_script\"}"
122
166
  ```
123
167
 
168
+ ### Profile `kk-itc`
169
+
170
+ Bind vars: `topic`, `input_file`, `work_dir`, `writer`, `playbook`, `research`, `judge`, `outline`,
171
+ `force`, `rubric` (`tech-accuracy`), `verdictFile` (`.spur/run/${vars.__runId}-itc-verdict.json`), `agent`.
172
+
173
+ ```bash
174
+ spur workflow run "$dest" --vars \
175
+ "{\"topic\":\"$TOPIC\",\"input_file\":\"$INPUT_FILE\",\"work_dir\":\"$work_dir\",\"writer\":\"$WRITER\",\"playbook\":\"$PLAYBOOK\",\"research\":\"$RESEARCH\",\"judge\":\"$JUDGE\",\"outline\":\"$OUTLINE\",\"force\":\"$FORCE\",\"rubric\":\"tech-accuracy\",\"verdictFile\":\".spur/run/\${vars.__runId}-itc-verdict.json\",\"agent\":\"$AGENT\"}"
176
+ ```
177
+
178
+ ### Profile `kk-solo-podcast`
179
+
180
+ Bind vars: `topic`, `input_file`, `work_dir`, `outline`, `script_approved`, `force`,
181
+ `target_duration_min`, `language`, `voice_profile` (from `VOICEBOX_DEFAULT_PROFILE` or empty),
182
+ `validate_script` (`$workflows_dir/validate-voicescript.ts`), `plugins_path` (empty → ADR-012
183
+ default discovery), `agent`. **Always** `spur workflow run "$dest"` — `$dest` is
184
+ `$workflows_dir/kk-solo-podcast.yaml`, never the design-time `plugins/kk/workflows/` path.
185
+
186
+ ```bash
187
+ spur workflow run "$dest" --vars \
188
+ "{\"topic\":\"$TOPIC\",\"input_file\":\"$INPUT_FILE\",\"work_dir\":\"$work_dir\",\"outline\":\"$OUTLINE\",\"script_approved\":\"$SCRIPT_APPROVED\",\"force\":\"$FORCE\",\"target_duration_min\":\"$DURATION\",\"language\":\"$LANGUAGE\",\"voice_profile\":\"$VOICE_PROFILE\",\"validate_script\":\"$workflows_dir/validate-voicescript.ts\",\"plugins_path\":\"\",\"agent\":\"$AGENT\"}"
189
+ ```
190
+
124
191
  ## 6. Report
125
192
 
126
- Print the resolved `work_dir`, then the two artifacts to hand the operator:
127
- `$work_dir/content.md` (report sidecar) and `$work_dir/content.json` (machine contract).
128
- Per the skill's better-report practice (d), skim `content.md` citations against `docs.json`
129
- ids before presenting. If the run ended `failed`, show the failing state
130
- (`spur workflow trace <run-id> --json`) and the skill's fail-loud recovery row.
193
+ - **`kk-storm-research`**: print resolved `work_dir`, then `$work_dir/content.md` (report sidecar)
194
+ and `$work_dir/content.json` (machine contract). Skim `content.md` citations against `docs.json`
195
+ ids before presenting.
196
+ - **`kk-itc`**: print resolved `work_dir`, then `$work_dir/brief.md`, `$work_dir/2-outline/outline-approved.md`,
197
+ and `$work_dir/3-draft/draft-article.md`. If `judge=true`, also print the verdict path.
198
+ - **`kk-solo-podcast`**: print resolved `work_dir`, then `$work_dir/3-script/voicescript.yaml`,
199
+ `$work_dir/4-audio/content.json`, and `$work_dir/4-audio/content.wav`.
200
+
201
+ If the run ended `failed`, show the failing state (`spur workflow trace <run-id> --json`) and the
202
+ fail-loud recovery line.
@@ -0,0 +1,34 @@
1
+ works_dir: "~/.config/kk/works"
2
+ workflows_dir: "~/.config/kk/workflows"
3
+ defaults:
4
+ maxResults: 8
5
+ fixture: false
6
+
7
+ # Plugin credentials — paste values between the quotes.
8
+ # Path: plugins/<collection>/<name>/env — each entry is injected into that plugin's
9
+ # process environment at exec time. A real environment variable always wins
10
+ # over a value here (flag > env > config > compiled default).
11
+ plugins:
12
+ ingestions:
13
+ web-search:
14
+ env:
15
+ FIRECRAWL_API_KEY: "" # firecrawl.dev — required for live research
16
+ generations:
17
+ voice-gen:
18
+ env:
19
+ VOICEBOX_URL: "http://127.0.0.1:17493" # local Voicebox REST origin
20
+ VOICEBOX_DEFAULT_PROFILE: "Robin" # GET /profiles name or id — required for plain-text Docs
21
+ VOICEBOX_TIMEOUT_MS: "600000" # per-generation poll budget (ms)
22
+ VOICEBOX_POLL_MS: "1000" # GET /history/{id} interval
23
+ VOICEBOX_MAX_CHUNK_CHARS: "800" # Voicebox auto-chunk size (100–5000)
24
+ VOICEBOX_CROSSFADE_MS: "50" # Voicebox chunk crossfade (0–500)
25
+ publishings:
26
+ qiita-pub:
27
+ env:
28
+ QIITA_TOKEN: "" # Qiita API v2 token (read_qiita/write_qiita) — required
29
+ zenn-pub:
30
+ env:
31
+ ZENN_REPO: "" # GitHub repo for Zenn CLI deploy — used by zenn-pub
32
+ surfdash-pub:
33
+ env:
34
+ POSTSURFING_BIN: "" # optional — delete if unused; defaults to `postsurfing` on PATH