@gobing-ai/knowledge-kit 0.0.14 → 0.0.16

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 (26) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/package.json +1 -1
  3. package/plugins/generations/omni-voice-gen/README.md +9 -3
  4. package/plugins/generations/omni-voice-gen/profiles.json +5 -1
  5. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/backend.py +12 -1
  6. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/pipeline.py +18 -2
  7. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/profiles.py +8 -0
  8. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/voicescript.py +0 -12
  9. package/plugins/generations/omni-voice-gen/voices/robin-news.wav +0 -0
  10. package/plugins/kk/commands/workflow-run.md +54 -22
  11. package/plugins/kk/config.example.yaml +5 -0
  12. package/plugins/kk/plugin.json +1 -1
  13. package/plugins/kk/scripts/kk-workflow-stages.ts +1226 -0
  14. package/plugins/kk/{workflows → scripts}/validate-voicescript.ts +4 -4
  15. package/plugins/kk/{workflows → scripts}/wrap-voicescript-doc.ts +7 -6
  16. package/plugins/kk/skills/audio-authoring/SKILL.md +11 -12
  17. package/plugins/kk/skills/itc-generating/SKILL.md +2 -2
  18. package/plugins/kk/skills/itc-generating/references/generic-craft.md +1 -1
  19. package/plugins/kk/skills/itc-generating/references/platform-english.md +1 -1
  20. package/plugins/kk/skills/itc-generating/references/platform-wechat.md +1 -1
  21. package/plugins/kk/skills/storm-research/SKILL.md +5 -5
  22. package/plugins/kk/skills/topic/SKILL.md +7 -7
  23. package/plugins/kk/workflows/kk-daily-ai-voice.yaml +149 -163
  24. package/plugins/kk/workflows/kk-itc.yaml +12 -29
  25. package/plugins/kk/workflows/kk-solo-podcast.yaml +72 -131
  26. package/plugins/kk/workflows/kk-storm-research.yaml +12 -62
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "plugins": [
8
8
  {
9
- "version": "0.0.14",
9
+ "version": "0.0.16",
10
10
  "name": "kk",
11
11
  "source": "./plugins/kk",
12
12
  "description": "knowledge-kit — a set of toolkit for knowledge management"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/knowledge-kit",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "An ingest → create → publish content pipeline CLI (Bun).",
5
5
  "repository": {
6
6
  "type": "git",
@@ -2,14 +2,17 @@
2
2
 
3
3
  OmniVoice-backed voice generator plugin for [knowledge-kit](../../../README.md).
4
4
  Drop-in replacement for `voice-gen` — same `kk executor run` contract (`--in Doc[] --out Content`),
5
- same audio metadata shape, same QC pipeline. Swap the backend in `kk-daily-ai-voice` with:
5
+ same audio metadata shape, same QC pipeline. `kk-daily-ai-voice` selects it by default via the
6
+ `voice_generator` var; fall back to the Voicebox backend with:
6
7
 
7
8
  ```bash
8
9
  spur workflow run plugins/kk/workflows/kk-daily-ai-voice.yaml \
9
- --vars '{"voice_generator":"omni-voice-gen"}' # other vars per your kk config
10
+ --vars '{"voice_generator":"voice-gen"}' # other vars per your kk config
10
11
  ```
11
12
 
12
- The default `voice_generator` is `voice-gen` (production behavior unchanged; swap is operator opt-in).
13
+ > **Voice identity:** the shipped `robin-news` profile is an instruct placeholder. For a real
14
+ > operator voice, record a 3–10 s clip into a private registry (`OMNIVOICE_PROFILE_REGISTRY`)
15
+ > and it supersedes the committed entry.
13
16
 
14
17
  ## Setup
15
18
 
@@ -23,6 +26,9 @@ make sync # runs `uv sync` — creates .venv, installs omnivoice from P
23
26
  > (~2–4 GB depending on device). The Whisper ASR model used for transcription-fidelity QC loads
24
27
  > lazily on the first QC transcription (`OMNIVOICE_ASR_MODEL` overrides it). Subsequent runs
25
28
  > use the HF cache. `OMNIVOICE_MODEL` overrides the model id or points to a local checkout.
29
+ > If the cached-model revalidation hang blocks a run (observed on flaky networks), set
30
+ > `HF_HUB_OFFLINE=1` once the model is cached — the plugin only needs the hub for the
31
+ > first pull.
26
32
  > GPU (`cuda`) is auto-selected when available; MPS (Apple Silicon) and CPU are fallbacks.
27
33
  > Set `OMNIVOICE_DEVICE` to override (e.g. `cpu` for CI).
28
34
 
@@ -1,4 +1,8 @@
1
1
  {
2
+ "robin-news": {
3
+ "ref_audio": "voices/robin-news.wav",
4
+ "ref_text": "本届赛事由北京市人民政府、中央广播电视总台、世界机器人合作组织、亚太机器人世界杯国际理事会联合主办。来自六大洲 16 个国家的 666 支队伍、2056 台机器人参赛,队伍数量较首届增长 138%,机器人"
5
+ },
2
6
  "sample-clone": {
3
7
  "ref_audio": "voices/sample-ref.wav",
4
8
  "ref_text": "Replace with the transcript of your reference clip."
@@ -7,6 +11,6 @@
7
11
  "prompt": "voices/sample-prompt.pt"
8
12
  },
9
13
  "sample-narrator": {
10
- "instruct": "Speak in a calm, measured narrator voice."
14
+ "instruct": "male, middle-aged, low pitch"
11
15
  }
12
16
  }
@@ -19,6 +19,15 @@ from .profiles import ProfileEntry
19
19
 
20
20
  DEFAULT_MODEL = "k2-fsa/OmniVoice"
21
21
  SAMPLE_RATE = 24000 # OmniVoice output rate; ASR transcribe is fixed at 24000 (task R4)
22
+ _ASR_CHUNK_SECONDS = 28.0 # ponytail: Whisper errors on >30s inputs; fixed chunks, silence-aware split if boundary words hurt fidelity
23
+
24
+
25
+ def _chunk_for_asr(audio: np.ndarray) -> list[np.ndarray]:
26
+ """Split a waveform into <=30s slices the upstream Whisper pipe accepts."""
27
+ chunk = int(_ASR_CHUNK_SECONDS * SAMPLE_RATE)
28
+ if len(audio) <= chunk:
29
+ return [audio]
30
+ return [audio[i : i + chunk] for i in range(0, len(audio), chunk)]
22
31
 
23
32
 
24
33
  class SegmentSpec(TypedDict):
@@ -147,4 +156,6 @@ class OmnivoiceBackend:
147
156
  if not self._asr_loaded:
148
157
  self._model.load_asr_model(model_name=self._asr_model)
149
158
  self._asr_loaded = True
150
- return str(self._model.transcribe((_wav_to_float(wav), SAMPLE_RATE)))
159
+ audio = _wav_to_float(wav)
160
+ parts = [str(self._model.transcribe((part, SAMPLE_RATE))) for part in _chunk_for_asr(audio)]
161
+ return " ".join(part for part in parts if part)
@@ -39,7 +39,6 @@ from .voicescript import (
39
39
  WarnSink,
40
40
  merge_voice_scripts,
41
41
  parse_doc_to_voicescript,
42
- resolve_instruct,
43
42
  validate_voice_script,
44
43
  )
45
44
 
@@ -51,6 +50,10 @@ def _log(msg: str) -> None:
51
50
  print(f"omni-voice-gen: {msg}", file=sys.stderr)
52
51
 
53
52
 
53
+ # Emotions already warned about this run (one line per distinct value, not per segment).
54
+ _ignored_emotions: set[str] = set()
55
+
56
+
54
57
  def _delete(*paths: str) -> None:
55
58
  for path in paths:
56
59
  Path(path).unlink(missing_ok=True)
@@ -188,11 +191,24 @@ def _render(
188
191
  speaker = (script.speakers or {}).get(segment.speaker) if segment.speaker else None
189
192
  profile = resolve_profile_target(segment, speaker, script, env_default, docs)
190
193
  emotion = _first(segment.emotion, getattr(speaker, "emotion", None), script.default_emotion)
194
+ # OmniVoice instruct accepts a fixed vocabulary (accent/age/pitch) with no emotion items,
195
+ # so voicegen-era free-text emotion is unrepresentable: drop it loudly and let the
196
+ # resolved profile's instruct carry the voice. An explicit segment/speaker instruct is
197
+ # operator-authored whitelist text and passes through untouched.
198
+ segment_instruct = _first(segment.instruct, getattr(speaker, "instruct", None))
199
+ if segment_instruct is None and emotion is not None:
200
+ key = emotion.strip().lower()
201
+ if key not in _ignored_emotions:
202
+ _ignored_emotions.add(key)
203
+ _log(
204
+ f"ignoring emotion {emotion!r}: OmniVoice instruct vocabulary has no emotion items — "
205
+ "voice comes from the resolved profile"
206
+ )
191
207
  spec: SegmentSpec = {
192
208
  "text": segment.text,
193
209
  "language": _first(segment.language, getattr(speaker, "language", None), script.language, "en"),
194
210
  "profile": resolve_profile(profile, registry),
195
- "instruct": resolve_instruct(_first(segment.instruct, getattr(speaker, "instruct", None)), emotion),
211
+ "instruct": segment_instruct,
196
212
  "seed": None if segment.seed is None else int(segment.seed),
197
213
  "speed": None,
198
214
  }
@@ -83,6 +83,14 @@ def load_registry(path: Path | None = None) -> ProfileRegistry:
83
83
  f"profile registry at {path} must be a JSON object mapping profile names to entries"
84
84
  )
85
85
  entries = {name: _parse_entry(name, spec) for name, spec in data.items()}
86
+ # Resolve relative asset paths against the registry's directory so backend passes
87
+ # absolute paths to the engine regardless of process CWD.
88
+ base = path.resolve().parent
89
+ for entry in entries.values():
90
+ if entry.ref_audio and not Path(entry.ref_audio).is_absolute():
91
+ entry.ref_audio = str((base / entry.ref_audio).resolve())
92
+ if entry.prompt and not Path(entry.prompt).is_absolute():
93
+ entry.prompt = str((base / entry.prompt).resolve())
86
94
  lowered = [key.lower() for key in entries]
87
95
  if len(set(lowered)) != len(lowered):
88
96
  dupes = sorted({key for key in entries if lowered.count(key.lower()) > 1})
@@ -338,15 +338,3 @@ def validate_voice_script(script: VoiceScript, sink: WarnSink) -> None:
338
338
  raise ValueError("seed must be a non-negative integer")
339
339
 
340
340
  _collect_ignored(segment, f"segments[{idx}]", sink)
341
-
342
-
343
- def resolve_instruct(instruct: str | None = None, emotion: str | None = None) -> str | None:
344
- if instruct and emotion:
345
- if emotion.lower() in instruct.lower():
346
- return instruct
347
- return f"{instruct}. Emotion: {emotion}"
348
- if instruct:
349
- return instruct
350
- if emotion:
351
- return f"Speak in a {emotion} tone."
352
- return None
@@ -2,8 +2,8 @@
2
2
  name: workflow-run
3
3
  description: >-
4
4
  Run STORM daily research, IT content authoring, or solo-podcast end to end.
5
- Resolves the workflow YAML from the user override, KK_WORKFLOWS_SOURCE, the
6
- installed kk package root, or a repo checkout — runs in place from any cwd;
5
+ Resolves the workflow YAML from the user override, KK_WORKFLOWS_SOURCE, bun/npm
6
+ global installs, the kk package root, or a repo checkout — runs in place from any cwd;
7
7
  creates the config with defaults if missing; computes the workspace; shells
8
8
  spur workflow run against the resolved YAML.
9
9
  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]"
@@ -13,8 +13,9 @@ Thin runner around the **storm-research**, **itc-generating** / **topic**, and *
13
13
  capabilities. Read `plugins/kk/skills/storm-research/SKILL.md` or
14
14
  `plugins/kk/skills/itc-generating/SKILL.md` for craft; solo-podcast craft lives in the
15
15
  `audio-authoring` skill. This command owns the run procedure: config create,
16
- workflow YAML resolution (user override → `$KK_WORKFLOWS_SOURCE` → **installed package
17
- root** → repo checkout, runs in place from any cwd), workspace computation, and the
16
+ workflow YAML resolution (user override → `$KK_WORKFLOWS_SOURCE` → **bun/npm global
17
+ install** → **installed package root** → repo checkout, runs in place from any cwd), workspace
18
+ computation, and the
18
19
  `spur workflow run` of the resolved YAML. Design-time SSOT remains
19
20
  `plugins/kk/workflows/` in the package/repo. No new `kk` CLI noun (ADR-011) — everything here
20
21
  is shell + the workflow.
@@ -30,12 +31,13 @@ Parse per the frozen rule:
30
31
  `kk-storm-research` (default), `kk-itc`, `kk-solo-podcast`. Any other name after successful copy → exit 1 listing
31
32
  `kk-storm-research`, `kk-itc`, `kk-solo-podcast`.
32
33
  - `--in <file>` → file mode. `--force` → overwrite an existing dest YAML / existing `brief.md`.
33
- - Profile `kk-storm-research`: `--fixture` → force `fixture: "true"`.
34
- - Profile `kk-itc`: `--dir <path>` (default `./<kebab>`), `--playbook generic|english|wechat` (default `generic`),
34
+ - Profile `kk-storm-research`: `--dir <path>` (default `$works_dir/kk-storm-research/<topic-id>`),
35
+ `--fixture` → force `fixture: "true"`.
36
+ - Profile `kk-itc`: `--dir <path>` (default `$works_dir/kk-itc/<kebab>`), `--playbook generic|english|wechat` (default `generic`),
35
37
  `--research` (`true`|`false`, default `false`), `--judge` (`true`|`false`, default `false`),
36
38
  `--outline <a|b|c>` (default empty), `--writer itc-generating|topic` (default `itc-generating`).
37
39
  `--fixture` passed with `kk-itc` or `kk-solo-podcast` → exit 1 (`fixture is storm-only`).
38
- - Profile `kk-solo-podcast`: `--dir <path>` (default `$works_dir/<kebab>`), `--outline <a|b|c>`
40
+ - Profile `kk-solo-podcast`: `--dir <path>` (default `$works_dir/kk-solo-podcast/<date>`), `--outline <a|b|c>`
39
41
  (default empty), `--duration <min>` (default `8`), `--language <code>` (default `en`),
40
42
  `--script-approved` (sets `script_approved=true`; default off). `--fixture` is storm-only.
41
43
  - **XOR:** exactly one of `<topic>` / `--in <file>`. Both set or both empty → exit 1, stderr
@@ -90,6 +92,19 @@ while [ -L "$kbin" ]; do
90
92
  case "$link" in /*) kbin="$link";; *) kbin="$(dirname "$kbin")/$link";; esac
91
93
  done
92
94
  pkg=""; [ -n "$kbin" ] && pkg="$(cd "$(dirname "$kbin")/.." 2>/dev/null && pwd || true)"
95
+ # Global-install roots — the common no-config case (package installed with `bun install -g`
96
+ # or `npm i -g` and no `workflows_dir` written yet). Each probe is guarded, so a missing
97
+ # package manager never breaks the chain.
98
+ bun_global_workflows=""
99
+ if command -v bun >/dev/null 2>&1; then
100
+ bg="$(dirname "$(bun pm bin -g 2>/dev/null)")/install/global/node_modules/@gobing-ai/knowledge-kit/plugins/kk/workflows"
101
+ [ -d "$bg" ] && bun_global_workflows="$bg"
102
+ fi
103
+ npm_global_workflows=""
104
+ if command -v npm >/dev/null 2>&1; then
105
+ ng="$(dirname "$(npm root -g 2>/dev/null)")/node_modules/@gobing-ai/knowledge-kit/plugins/kk/workflows"
106
+ [ -d "$ng" ] && npm_global_workflows="$ng"
107
+ fi
93
108
  ```
94
109
 
95
110
  Resolution order — first hit wins:
@@ -97,26 +112,31 @@ Resolution order — first hit wins:
97
112
  1. **User override** `$workflows_dir/$NAME.yaml` (default `~/.config/kk/workflows/`) —
98
113
  when it exists it always wins, and nothing is copied.
99
114
  2. `$KK_WORKFLOWS_SOURCE/$NAME.yaml`
100
- 3. `$pkg/plugins/kk/workflows/$NAME.yaml` — the installed package copy
101
- 4. `plugins/kk/workflows/$NAME.yaml` — repo checkout (development)
115
+ 3. **bun global install** — `$(dirname $(bun pm bin -g))/install/global/node_modules/@gobing-ai/knowledge-kit/plugins/kk/workflows/$NAME.yaml`
116
+ 4. **npm global install** — `$(dirname $(npm root -g))/node_modules/@gobing-ai/knowledge-kit/plugins/kk/workflows/$NAME.yaml`
117
+ 5. `$pkg/plugins/kk/workflows/$NAME.yaml` — the installed package copy (resolved from the `kk` binary)
118
+ 6. `plugins/kk/workflows/$NAME.yaml` — repo checkout (development)
119
+
120
+ A corrupt `config.yaml` already exited 1 in step 2; none of these probes runs before that check,
121
+ so no fallback masks a config error.
102
122
 
103
123
  ```bash
104
124
  dest=""
105
125
  if [ -f "$workflows_dir/$NAME.yaml" ]; then
106
126
  dest="$workflows_dir/$NAME.yaml"
107
127
  else
108
- for d in "$KK_WORKFLOWS_SOURCE" ${pkg:+"$pkg/plugins/kk/workflows"} "plugins/kk/workflows"; do
128
+ for d in "$KK_WORKFLOWS_SOURCE" "$bun_global_workflows" "$npm_global_workflows" ${pkg:+"$pkg/plugins/kk/workflows"} "plugins/kk/workflows"; do
109
129
  [ -n "$d" ] && [ -f "$d/$NAME.yaml" ] && { dest="$d/$NAME.yaml"; break; }
110
130
  done
111
- [ -z "$dest" ] && { echo "no source workflow for $NAME (searched $workflows_dir, KK_WORKFLOWS_SOURCE, kk package root, plugins/kk/workflows)" >&2; exit 1; }
131
+ [ -z "$dest" ] && { echo "no source workflow for $NAME (searched $workflows_dir, KK_WORKFLOWS_SOURCE, bun/npm global installs, kk package root, plugins/kk/workflows)" >&2; exit 1; }
112
132
  fi
113
133
  # --force: refresh the user override from the best available source, then run the override.
114
134
  if [ "$FORCE" = true ]; then
115
135
  src=""
116
- for d in "$KK_WORKFLOWS_SOURCE" ${pkg:+"$pkg/plugins/kk/workflows"} "plugins/kk/workflows"; do
136
+ for d in "$KK_WORKFLOWS_SOURCE" "$bun_global_workflows" "$npm_global_workflows" ${pkg:+"$pkg/plugins/kk/workflows"} "plugins/kk/workflows"; do
117
137
  [ -n "$d" ] && [ -f "$d/$NAME.yaml" ] && { src="$d/$NAME.yaml"; break; }
118
138
  done
119
- [ -z "$src" ] && { echo "no source workflow for $NAME to force-install (searched KK_WORKFLOWS_SOURCE, kk package root, plugins/kk/workflows)" >&2; exit 1; }
139
+ [ -z "$src" ] && { echo "no source workflow for $NAME to force-install (searched KK_WORKFLOWS_SOURCE, bun/npm global installs, kk package root, plugins/kk/workflows)" >&2; exit 1; }
120
140
  install -d "$workflows_dir" && cp "$src" "$workflows_dir/$NAME.yaml"
121
141
  dest="$workflows_dir/$NAME.yaml"
122
142
  fi
@@ -126,9 +146,11 @@ fi
126
146
  `--force` to place a fresh copy there) — that override wins on every later run.
127
147
  - No auto-copy, no drift warnings: the package copy is versioned with the CLI and
128
148
  never goes stale; overrides are explicit.
129
- - Sidecars (`validate-voicescript.ts`, `wrap-voicescript-doc.ts`) are **not copied** —
130
- the workflow's own fallback chain resolves them via `$KK_WORKFLOWS_DIR`, the package
131
- root, then `$HOME/.config/kk/workflows`.
149
+ - Sidecars (`plugins/kk/scripts/kk-workflow-stages.ts`, plus the
150
+ `validate-voicescript.ts` / `wrap-voicescript-doc.ts` it delegates to) are **not copied** —
151
+ the workflow resolves the stage runner from the package root, and the runner resolves each
152
+ sidecar via the explicit var, `$KK_WORKFLOWS_DIR`, the package copy, then
153
+ `$HOME/.config/kk/workflows`.
132
154
 
133
155
  ## 4. Compute workspace
134
156
 
@@ -143,7 +165,7 @@ slug=$(printf '%s' "$normalized" | tr -cs 'a-z0-9' '-' | sed 's/^-*//; s/-*$//'
143
165
  [ -z "$slug" ] && slug="topic"
144
166
  hash=$(printf '%s' "$normalized" | shasum -a 256 | cut -c1-8)
145
167
  topic_id="${slug}-${hash}"
146
- work_dir="$works_dir/$topic_id"
168
+ work_dir="${DIR:-$works_dir/kk-storm-research/$topic_id}"
147
169
  ```
148
170
 
149
171
  - `raw` derives empty (no H1 and empty basename) → exit 1 with the derived value.
@@ -157,15 +179,24 @@ mkdir -p "$work_dir"
157
179
 
158
180
  `raw` = `TOPIC` or file-mode first ATX H1 else basename without extension.
159
181
  `kebab` = lower-case, `[^a-z0-9]+` → `-`, trimmed dashes (no length cap, no sha256 hash).
160
- `work_dir="${DIR:-./$kebab}"`. Empty derived `kebab` → exit 1.
182
+ `work_dir="${DIR:-$works_dir/kk-itc/$kebab}"`. Empty derived `kebab` → exit 1. An explicit
183
+ `--dir` still wins, and the default never writes into the caller's cwd.
161
184
 
162
- ### Profile `kk-solo-podcast` / `kk-daily-ai-voice`
185
+ ### Profile `kk-solo-podcast`
163
186
 
164
187
  `date` = today's date in `YYYY-MM-DD` (`$(date +%Y-%m-%d)`).
165
188
  `raw` = `TOPIC` or file-mode first ATX H1 else basename without extension.
166
189
  `kebab` = lower-case, `[^a-z0-9]+` → `-`, trimmed dashes (no length cap, no sha256 hash).
167
190
  `work_dir="${DIR:-$works_dir/kk-solo-podcast/$date}"`.
168
191
 
192
+ ### Profile `kk-daily-ai-voice`
193
+
194
+ Same `date` / `raw` / `kebab` derivation as `kk-solo-podcast`.
195
+ `work_dir="${DIR:-$works_dir/kk-daily-ai-voice/$date}"`.
196
+
197
+ Every profile's default puts its workspace in **its own** `$works_dir/<workflow-name>/` namespace;
198
+ an explicit `--dir` overrides all four defaults.
199
+
169
200
  ## 5. Run the workflow
170
201
 
171
202
  All values are strings.
@@ -216,9 +247,10 @@ spur workflow run "$dest" --vars \
216
247
  and `$work_dir/content.json` (machine contract). Skim `content.md` citations against `docs.json`
217
248
  ids before presenting.
218
249
  - **`kk-itc`**: print resolved `work_dir`, then `$work_dir/brief.md`, `$work_dir/2-outline/outline-approved.md`,
219
- and `$work_dir/3-draft/draft-article.md`. If `judge=true`, also print the verdict path.
220
- - **`kk-solo-podcast`**: print resolved `work_dir`, then `$work_dir/3-script/voicescript.yaml`,
221
- `$work_dir/4-audio/content.json`, and `$work_dir/4-audio/content.wav`.
250
+ and `$work_dir/3-draft-draft-article.md`. If `judge=true`, also print the verdict path.
251
+ - **`kk-solo-podcast`**: print resolved `work_dir`, then `$work_dir/1-briefing-briefing.md`,
252
+ `$work_dir/3-script-voicescript.yaml`, `$work_dir/4-audio/content.json`, and
253
+ `$work_dir/4-audio/content.wav`.
222
254
 
223
255
  If the run ended `failed`, show the failing state (`spur workflow trace <run-id> --json`) and the
224
256
  fail-loud recovery line.
@@ -1,3 +1,8 @@
1
+ # kk operator config. `works_dir` is the run-workspace root — each workflow run defaults
2
+ # to <works_dir>/<workflow-name>/<run-id> unless --dir is given (existing `.works/`-era
3
+ # trees are not migrated). `workflows_dir` is the user-override root for workflow YAMLs;
4
+ # when a workflow is not found there, /workflow-run falls back to KK_WORKFLOWS_SOURCE,
5
+ # bun/npm global installs, the kk package root, then the repo checkout.
1
6
  works_dir: "~/.config/kk/works"
2
7
  workflows_dir: "~/.config/kk/workflows"
3
8
  defaults:
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "kk",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "knowledge-kit Claude Code plugin: skills, commands, subagents, hooks, and rules"
5
5
  }