@gobing-ai/knowledge-kit 0.0.9 → 0.0.11

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.
@@ -3,7 +3,8 @@
3
3
  name: kk-solo-podcast
4
4
  kind: state-machine
5
5
  description: >-
6
- Solo podcast authoring pipeline using VoiceScript authoring, Spur HITL gates, and voice-gen audio generation
6
+ Solo podcast authoring pipeline over the kk:audio-authoring skill, Spur HITL gates,
7
+ and voice-gen audio generation
7
8
  initialState: prepare
8
9
  terminalStates:
9
10
  - done
@@ -26,7 +27,7 @@ vars:
26
27
  plugins_path: ""
27
28
  validate_script: ""
28
29
  agent: ""
29
- __runId: "solo"
30
+ wrap_script: ""
30
31
  __hitlAnswer: ""
31
32
  __hitlInput: ""
32
33
 
@@ -36,32 +37,33 @@ states:
36
37
  onEnter:
37
38
  - kind: shell
38
39
  options:
39
- command: >-
40
- set -e;
41
- if [ -z "${vars.work_dir}" ]; then
42
- echo "missing work_dir" >&2; exit 1;
43
- fi;
44
- mkdir -p "${vars.work_dir}" "${vars.work_dir}/1-briefing" "${vars.work_dir}/2-outline" "${vars.work_dir}/3-script" "${vars.work_dir}/4-audio";
45
- if [ -n "${vars.topic}" ] && [ -n "${vars.input_file}" ]; then
46
- echo "XOR violation: both topic and input_file set" >&2; exit 1;
47
- fi;
48
- if [ -z "${vars.topic}" ] && [ -z "${vars.input_file}" ]; then
49
- echo "XOR violation: neither topic nor input_file set" >&2; exit 1;
50
- fi;
51
- if [ -z "${vars.voice_profile}" ] && [ -z "$VOICEBOX_DEFAULT_PROFILE" ]; then
52
- echo "missing voice_profile (set vars.voice_profile or VOICEBOX_DEFAULT_PROFILE)" >&2; exit 1;
53
- fi;
54
- if [ -f "${vars.work_dir}/brief.md" ] && [ "${vars.force}" != "true" ]; then
55
- echo "initialization target already exists at ${vars.work_dir}/brief.md; pass force=true to replace" >&2; exit 1;
56
- fi;
57
- if [ -n "${vars.topic}" ]; then
58
- printf '%s\n' "${vars.topic}" > "${vars.work_dir}/brief.md";
59
- else
60
- if [ ! -f "${vars.input_file}" ]; then
61
- echo "input file not found: ${vars.input_file}" >&2; exit 1;
62
- fi;
63
- cp "${vars.input_file}" "${vars.work_dir}/brief.md";
40
+ command: >-
41
+ set -e;
42
+ if [ -z "${vars.work_dir}" ]; then
43
+ echo "missing work_dir" >&2; exit 1;
44
+ fi;
45
+ mkdir -p "${vars.work_dir}" "${vars.work_dir}/1-briefing" "${vars.work_dir}/2-outline" "${vars.work_dir}/3-script" "${vars.work_dir}/4-audio";
46
+ if [ -n "${vars.topic}" ] && [ -n "${vars.input_file}" ]; then
47
+ echo "XOR violation: both topic and input_file set" >&2; exit 1;
48
+ fi;
49
+ if [ -z "${vars.topic}" ] && [ -z "${vars.input_file}" ]; then
50
+ echo "XOR violation: neither topic nor input_file set" >&2; exit 1;
51
+ fi;
52
+ if [ -z "${vars.voice_profile}" ] && [ -z "$VOICEBOX_DEFAULT_PROFILE" ]; then
53
+ echo "missing voice_profile (set vars.voice_profile or VOICEBOX_DEFAULT_PROFILE)" >&2; exit 1;
54
+ fi;
55
+ if [ -f "${vars.work_dir}/brief.md" ] && [ "${vars.force}" != "true" ]; then
56
+ echo "initialization target already exists at ${vars.work_dir}/brief.md; pass force=true to replace" >&2; exit 1;
57
+ fi;
58
+ if [ -n "${vars.topic}" ]; then
59
+ printf '%s\n' "${vars.topic}" > "${vars.work_dir}/brief.md";
60
+ else
61
+ if [ ! -f "${vars.input_file}" ]; then
62
+ echo "input file not found: ${vars.input_file}" >&2; exit 1;
64
63
  fi;
64
+ cp "${vars.input_file}" "${vars.work_dir}/brief.md";
65
+ fi;
66
+ echo 0 > "${vars.work_dir}/.revise-count";
65
67
 
66
68
  - id: understand
67
69
  description: "Analyze brief and generate podcast briefing (agent.run)"
@@ -71,9 +73,11 @@ states:
71
73
  role: scribe
72
74
  agent: ${vars.agent}
73
75
  input: |
76
+ Invoke the kk:audio-authoring skill: operation `briefing`, mode `solo`.
74
77
  Read the source brief at ${vars.work_dir}/brief.md.
75
78
  Write a concise podcast briefing at ${vars.work_dir}/1-briefing/briefing.md.
76
79
  Identify core thesis, key listener takeaways, audience profile, target spoken duration (${vars.target_duration_min} minutes), tone, and what material from the brief to emphasize or cut.
80
+ Flags: --dir ${vars.work_dir} --duration ${vars.target_duration_min}
77
81
  expectFile: ${vars.work_dir}/1-briefing/briefing.md
78
82
 
79
83
  - id: outline-write
@@ -84,6 +88,7 @@ states:
84
88
  role: scribe
85
89
  agent: ${vars.agent}
86
90
  input: |
91
+ Invoke the kk:audio-authoring skill: operation `outline`, mode `solo`.
87
92
  Read the briefing at ${vars.work_dir}/1-briefing/briefing.md and brief at ${vars.work_dir}/brief.md.
88
93
  Generate spoken podcast outline options in ${vars.work_dir}/2-outline/:
89
94
  - outline-option-a.md (e.g. narrative / storytelling arc)
@@ -91,11 +96,12 @@ states:
91
96
  - optional outline-option-c.md
92
97
  Target total episode spoken duration is ${vars.target_duration_min} minutes.
93
98
  Do not wait for operator selection; write the option files.
99
+ Flags: --dir ${vars.work_dir} --duration ${vars.target_duration_min}
94
100
  expectFile: ${vars.work_dir}/2-outline/outline-option-a.md
95
101
  - kind: shell
96
102
  options:
97
- command: >-
98
- test -f "${vars.work_dir}/2-outline/outline-option-b.md" || { echo "missing required outline-option-b.md" >&2; exit 1; }
103
+ command: >-
104
+ test -f "${vars.work_dir}/2-outline/outline-option-b.md" || { echo "missing required outline-option-b.md" >&2; exit 1; }
99
105
 
100
106
  - id: outline-select
101
107
  description: "Operator selects preferred outline option"
@@ -115,15 +121,15 @@ states:
115
121
  onEnter:
116
122
  - kind: shell
117
123
  options:
118
- command: >-
119
- sel="${vars.outline}";
120
- if [ -z "$sel" ]; then sel="${vars.__hitlAnswer}"; fi;
121
- if [ -z "$sel" ]; then sel="a"; fi;
122
- src="${vars.work_dir}/2-outline/outline-option-$sel.md";
123
- if [ ! -f "$src" ]; then
124
- echo "selected outline file not found: $src" >&2; exit 1;
125
- fi;
126
- cp "$src" "${vars.work_dir}/2-outline/outline-approved.md";
124
+ command: >-
125
+ sel="${vars.outline}";
126
+ if [ -z "$sel" ]; then sel="${vars.__hitlAnswer}"; fi;
127
+ if [ -z "$sel" ]; then sel="a"; fi;
128
+ src="${vars.work_dir}/2-outline/outline-option-$sel.md";
129
+ if [ ! -f "$src" ]; then
130
+ echo "selected outline file not found: $src" >&2; exit 1;
131
+ fi;
132
+ cp "$src" "${vars.work_dir}/2-outline/outline-approved.md";
127
133
 
128
134
  - id: script-write
129
135
  description: "Author complete VoiceScript YAML from approved outline (agent.run)"
@@ -133,6 +139,7 @@ states:
133
139
  role: scribe
134
140
  agent: ${vars.agent}
135
141
  input: |
142
+ Invoke the kk:audio-authoring skill: operation `script`, mode `solo`.
136
143
  Read the approved outline at ${vars.work_dir}/2-outline/outline-approved.md and briefing at ${vars.work_dir}/1-briefing/briefing.md.
137
144
  Target duration is ${vars.target_duration_min} minutes.
138
145
  Spoken budget: if language (${vars.language}) is zh, ja, or ko, target characters = ${vars.target_duration_min} * ${vars.cjk_chars_per_min}; otherwise target words = ${vars.target_duration_min} * ${vars.words_per_min}. Land inside ±20%, spoken prose, one speaker.
@@ -147,6 +154,8 @@ states:
147
154
  gap_ms: 400 # optional, >= 0, silence before next clip
148
155
  instruct: "warm" # optional, <= 500 characters
149
156
  Do not invent fields. Revision feedback: ${vars.__hitlInput}
157
+ Duration retries: if ${vars.work_dir}/.duration-feedback exists, invoke with `--force` (authorized rewrite) and treat its contents as the revision feedback.
158
+ Flags: --dir ${vars.work_dir} --duration ${vars.target_duration_min} --language ${vars.language} --profile ${vars.voice_profile} --revise "${vars.__hitlInput}"
150
159
  expectFile: ${vars.work_dir}/3-script/voicescript.yaml
151
160
 
152
161
  - id: script-validate
@@ -154,18 +163,26 @@ states:
154
163
  onEnter:
155
164
  - kind: shell
156
165
  options:
157
- command: >-
158
- set -e;
159
- vs="${vars.validate_script}";
160
- if [ -z "$vs" ]; then
161
- if [ -n "$KK_WORKFLOWS_DIR" ]; then vs="$KK_WORKFLOWS_DIR/validate-voicescript.ts";
162
- else vs="$HOME/.config/kk/workflows/validate-voicescript.ts"; fi;
163
- fi;
164
- if [ ! -f "$vs" ]; then
165
- echo "validate_script not found: $vs (install with /workflow-run kk-solo-podcast into $HOME/.config/kk/workflows)" >&2;
166
- exit 1;
167
- fi;
168
- bun "$vs" --in "${vars.work_dir}/3-script/voicescript.yaml"
166
+ command: >-
167
+ set -e;
168
+ vs="${vars.validate_script}";
169
+ if [ ! -f "$vs" ]; then
170
+ kbin="$(command -v kk 2>/dev/null || true)";
171
+ while [ -L "$kbin" ]; do
172
+ link="$(readlink "$kbin")";
173
+ case "$link" in /*) kbin="$link";; *) kbin="$(dirname "$kbin")/$link";; esac;
174
+ done;
175
+ pkg=""; [ -n "$kbin" ] && pkg="$(cd "$(dirname "$kbin")/.." 2>/dev/null && pwd || true)";
176
+ vs="";
177
+ if [ -n "$KK_WORKFLOWS_DIR" ] && [ -f "$KK_WORKFLOWS_DIR/validate-voicescript.ts" ]; then vs="$KK_WORKFLOWS_DIR/validate-voicescript.ts";
178
+ elif [ -n "$pkg" ] && [ -f "$pkg/plugins/kk/workflows/validate-voicescript.ts" ]; then vs="$pkg/plugins/kk/workflows/validate-voicescript.ts";
179
+ elif [ -f "$HOME/.config/kk/workflows/validate-voicescript.ts" ]; then vs="$HOME/.config/kk/workflows/validate-voicescript.ts"; fi;
180
+ fi;
181
+ if [ -z "$vs" ]; then
182
+ echo "validate_script not found (searched: \$KK_WORKFLOWS_DIR, kk package root via \`command -v kk\`, \$HOME/.config/kk/workflows) — install the kk package or place an override with /workflow-run kk-solo-podcast --force" >&2;
183
+ exit 1;
184
+ fi;
185
+ bun "$vs" --in "${vars.work_dir}/3-script/voicescript.yaml"
169
186
 
170
187
  - id: script-review
171
188
  description: "Operator reviews and approves VoiceScript before live TTS generation"
@@ -182,13 +199,10 @@ states:
182
199
  onEnter:
183
200
  - kind: shell
184
201
  options:
185
- command: >-
186
- mkdir -p .spur/run;
187
- run_id="${vars.__runId}";
188
- if [ -z "$run_id" ]; then run_id="solo"; fi;
189
- count_file=".spur/run/$run_id-solo-revise-count";
190
- curr=$(cat "$count_file" 2>/dev/null || echo 0);
191
- echo $((curr + 1)) > "$count_file";
202
+ command: >-
203
+ rm -f "${vars.work_dir}/.duration-feedback";
204
+ n=$(cat "${vars.work_dir}/.revise-count" 2>/dev/null || echo 0);
205
+ echo $((n + 1)) > "${vars.work_dir}/.revise-count";
192
206
  - kind: hitl.input
193
207
  options:
194
208
  prompt: "Provide specific revision feedback for ${vars.work_dir}/3-script/voicescript.yaml:"
@@ -199,41 +213,65 @@ states:
199
213
  onEnter:
200
214
  - kind: shell
201
215
  options:
202
- command: >-
203
- bun -e '
204
- import { readFileSync, writeFileSync } from "node:fs";
205
- import { createHash } from "node:crypto";
206
- import { resolve } from "node:path";
207
- const scriptPath = resolve(process.argv[1]);
208
- const outPath = resolve(process.argv[2]);
209
- const voiceProfile = process.argv[3] || process.env.VOICEBOX_DEFAULT_PROFILE || "";
210
- const body = readFileSync(scriptPath, "utf-8");
211
- const sourceUri = "file://" + scriptPath;
212
- const id = createHash("sha256").update(sourceUri).digest("hex").slice(0, 16);
213
- const doc = {
214
- id,
215
- title: "Podcast VoiceScript",
216
- sourceUri,
217
- body,
218
- mediaType: "application/yaml",
219
- metadata: voiceProfile ? { voiceProfile } : {},
220
- };
221
- writeFileSync(outPath, JSON.stringify([doc], null, 2));
222
- ' "${vars.work_dir}/3-script/voicescript.yaml" "${vars.work_dir}/4-audio/docs.json" "${vars.voice_profile}"
216
+ command: >-
217
+ set -e;
218
+ ws="${vars.wrap_script}";
219
+ if [ ! -f "$ws" ]; then
220
+ kbin="$(command -v kk 2>/dev/null || true)";
221
+ while [ -L "$kbin" ]; do
222
+ link="$(readlink "$kbin")";
223
+ case "$link" in /*) kbin="$link";; *) kbin="$(dirname "$kbin")/$link";; esac;
224
+ done;
225
+ pkg=""; [ -n "$kbin" ] && pkg="$(cd "$(dirname "$kbin")/.." 2>/dev/null && pwd || true)";
226
+ ws="";
227
+ if [ -n "$KK_WORKFLOWS_DIR" ] && [ -f "$KK_WORKFLOWS_DIR/wrap-voicescript-doc.ts" ]; then ws="$KK_WORKFLOWS_DIR/wrap-voicescript-doc.ts";
228
+ elif [ -n "$pkg" ] && [ -f "$pkg/plugins/kk/workflows/wrap-voicescript-doc.ts" ]; then ws="$pkg/plugins/kk/workflows/wrap-voicescript-doc.ts";
229
+ elif [ -f "$HOME/.config/kk/workflows/wrap-voicescript-doc.ts" ]; then ws="$HOME/.config/kk/workflows/wrap-voicescript-doc.ts"; fi;
230
+ fi;
231
+ if [ -z "$ws" ]; then
232
+ echo "wrap_script not found (searched: \$KK_WORKFLOWS_DIR, kk package root via \`command -v kk\`, \$HOME/.config/kk/workflows) — install the kk package or place an override with /workflow-run kk-solo-podcast --force" >&2;
233
+ exit 1;
234
+ fi;
235
+ bun "$ws" "${vars.work_dir}/3-script/voicescript.yaml" "${vars.work_dir}/4-audio/docs.json" "${vars.voice_profile}"
223
236
 
224
237
  - id: generate
225
238
  description: "Invoke voice-gen generator plugin to generate audio"
226
239
  onEnter:
227
240
  - kind: shell
228
241
  options:
229
- command: >-
230
- set -e;
231
- extra="";
232
- if [ -n "${vars.plugins_path}" ]; then extra="--plugins-path ${vars.plugins_path}"; fi;
233
- kk executor run voice-gen --in "${vars.work_dir}/4-audio/docs.json" --out "${vars.work_dir}/4-audio/content.json" $extra
242
+ command: >-
243
+ set -e;
244
+ extra="";
245
+ if [ -n "${vars.plugins_path}" ]; then extra="--plugins-path ${vars.plugins_path}"; fi;
246
+ kk executor run voice-gen --in "${vars.work_dir}/4-audio/docs.json" --out "${vars.work_dir}/4-audio/content.json" $extra
234
247
 
235
248
  - id: duration-check
236
- description: "Validate generated audio duration against target budget"
249
+ description: "Validate generated audio duration against target budget; miss writes .duration-feedback for the retry edge"
250
+ onEnter:
251
+ - kind: shell
252
+ options:
253
+ command: >-
254
+ rm -f "${vars.work_dir}/4-audio/.duration-ok";
255
+ if [ ! -f "${vars.work_dir}/4-audio/content.json" ]; then
256
+ exit 0;
257
+ fi;
258
+ dur=$(jq -r '.metadata.duration // empty' "${vars.work_dir}/4-audio/content.json" 2>/dev/null);
259
+ if [ -z "$dur" ]; then
260
+ printf 'Regenerate: audio metadata.duration missing or not a number.\n' > "${vars.work_dir}/.duration-feedback";
261
+ n=$(cat "${vars.work_dir}/.revise-count" 2>/dev/null || echo 0);
262
+ echo $((n+1)) > "${vars.work_dir}/.revise-count";
263
+ exit 0;
264
+ fi;
265
+ target_s=$((${vars.target_duration_min} * 60));
266
+ min_s=$(awk -v t="$target_s" 'BEGIN {print t * 0.5}');
267
+ max_s=$(awk -v t="$target_s" 'BEGIN {print t * 2.0}');
268
+ if awk -v d="$dur" -v min="$min_s" -v max="$max_s" 'BEGIN { exit !(d >= min && d <= max) }'; then
269
+ touch "${vars.work_dir}/4-audio/.duration-ok";
270
+ else
271
+ printf 'Regenerate: last audio duration %ss outside 0.5x–2x of %ss; adjust segment lengths toward the target.\n' "$dur" "$target_s" > "${vars.work_dir}/.duration-feedback";
272
+ n=$(cat "${vars.work_dir}/.revise-count" 2>/dev/null || echo 0);
273
+ echo $((n+1)) > "${vars.work_dir}/.revise-count";
274
+ fi
237
275
 
238
276
  - id: done
239
277
  description: "Terminal — Solo podcast VoiceScript and audio generated successfully"
@@ -314,7 +352,7 @@ transitions:
314
352
  guard:
315
353
  kind: shell
316
354
  options:
317
- command: 'test "${vars.__hitlAnswer}" = "no" && { run_id="${vars.__runId}"; if [ -z "$run_id" ]; then run_id="solo"; fi; test "$(cat .spur/run/$run_id-solo-revise-count 2>/dev/null || echo 0)" -lt 3; }'
355
+ command: 'test "${vars.__hitlAnswer}" = "no" && test "$(cat "${vars.work_dir}/.revise-count" 2>/dev/null || echo 0)" -lt 3'
318
356
 
319
357
  - from: script-review
320
358
  to: failed
@@ -322,7 +360,7 @@ transitions:
322
360
  guard:
323
361
  kind: shell
324
362
  options:
325
- command: 'test "${vars.__hitlAnswer}" = "no" && { run_id="${vars.__runId}"; if [ -z "$run_id" ]; then run_id="solo"; fi; test "$(cat .spur/run/$run_id-solo-revise-count 2>/dev/null || echo 0)" -ge 3; }'
363
+ command: 'test "${vars.__hitlAnswer}" = "no" && test "$(cat "${vars.work_dir}/.revise-count" 2>/dev/null || echo 0)" -ge 3'
326
364
 
327
365
  - from: script-revise
328
366
  to: script-write
@@ -348,27 +386,20 @@ transitions:
348
386
  guard:
349
387
  kind: shell
350
388
  options:
351
- command: >-
352
- test ! -f "${vars.work_dir}/4-audio/content.json" || {
353
- dur=$(jq -r '.metadata.duration // empty' "${vars.work_dir}/4-audio/content.json" 2>/dev/null);
354
- test -n "$dur" && target_s=$((${vars.target_duration_min} * 60)) && min_s=$(awk -v t="$target_s" 'BEGIN {print t * 0.5}') && max_s=$(awk -v t="$target_s" 'BEGIN {print t * 2.0}') && awk -v d="$dur" -v min="$min_s" -v max="$max_s" 'BEGIN { if (d >= min && d <= max) exit 0; else exit 1 }';
355
- }
389
+ command: 'test ! -f "${vars.work_dir}/4-audio/content.json" || test -f "${vars.work_dir}/4-audio/.duration-ok"'
390
+
391
+ - from: duration-check
392
+ to: script-write
393
+ description: "Audio duration outside band with revision budget remaining -> regenerate script from feedback"
394
+ guard:
395
+ kind: shell
396
+ options:
397
+ command: 'test -f "${vars.work_dir}/4-audio/content.json" && test ! -f "${vars.work_dir}/4-audio/.duration-ok" && test "$(cat "${vars.work_dir}/.revise-count" 2>/dev/null || echo 0)" -lt 3'
356
398
 
357
399
  - from: duration-check
358
400
  to: failed
359
- description: "Audio duration missing or outside [0.5x, 2.0x] target range -> fail"
401
+ description: "Audio duration outside band with revision budget exhausted -> fail"
360
402
  guard:
361
403
  kind: shell
362
404
  options:
363
- command: >-
364
- test -f "${vars.work_dir}/4-audio/content.json" && {
365
- dur=$(jq -r '.metadata.duration // empty' "${vars.work_dir}/4-audio/content.json" 2>/dev/null);
366
- if [ -z "$dur" ]; then
367
- echo "duration-check failed: metadata.duration missing or not a number" >&2;
368
- exit 0;
369
- fi;
370
- target_s=$((${vars.target_duration_min} * 60));
371
- min_s=$(awk -v t="$target_s" 'BEGIN {print t * 0.5}');
372
- max_s=$(awk -v t="$target_s" 'BEGIN {print t * 2.0}');
373
- awk -v d="$dur" -v min="$min_s" -v max="$max_s" -v ts="$target_s" 'BEGIN { if (d < min || d > max) { printf "duration-check failed: duration %ss outside 0.5x–2x of %ss\n", d, ts > "/dev/stderr"; exit 0 } else exit 1 }';
374
- }
405
+ command: 'test -f "${vars.work_dir}/4-audio/content.json" && test ! -f "${vars.work_dir}/4-audio/.duration-ok" && test "$(cat "${vars.work_dir}/.revise-count" 2>/dev/null || echo 0)" -ge 3'
@@ -37,10 +37,10 @@ vars:
37
37
  maxResults: "8" # written into topic.json as int (0054 maxResults)
38
38
  fixture: "false" # "true" selects the stub; CI/dry-run must pass "true"
39
39
  work_dir: "./work" # RESOLVED topic dir ($works_root/<topic-id>)
40
- plugins_path: "./plugins" # --plugins-path for kk executor run
40
+ plugins_path: "" # empty -> ADR-012 default discovery (package-shipped built-ins); set for checkout dev
41
41
  ingestion_plugin: "web-search"
42
42
  generator_plugin: "content-gen"
43
- render_script: "plugins/kk/scripts/render-md.ts" # 0058 shipped runner
43
+ render_script: "" # empty -> fallback chain: kk package root -> checkout plugins/kk/scripts/render-md.ts
44
44
 
45
45
  states:
46
46
  - id: prepare
@@ -103,9 +103,11 @@ states:
103
103
  - kind: shell
104
104
  options:
105
105
  command: >-
106
+ set --;
107
+ if [ -n "${vars.plugins_path}" ]; then set -- --plugins-path "${vars.plugins_path}"; fi;
106
108
  kk executor run "${vars.ingestion_plugin}" --in
107
109
  "${vars.work_dir}/topic.json" --out "${vars.work_dir}/docs.json"
108
- --plugins-path "${vars.plugins_path}"
110
+ "$@"
109
111
 
110
112
  - id: generate
111
113
  description: >-
@@ -123,9 +125,11 @@ states:
123
125
  - kind: shell
124
126
  options:
125
127
  command: >-
128
+ set --;
129
+ if [ -n "${vars.plugins_path}" ]; then set -- --plugins-path "${vars.plugins_path}"; fi;
126
130
  kk executor run "${vars.generator_plugin}" --in
127
131
  "${vars.work_dir}/docs.json" --out "${vars.work_dir}/content.json"
128
- --plugins-path "${vars.plugins_path}"
132
+ "$@"
129
133
 
130
134
  - id: render
131
135
  description: >-
@@ -135,7 +139,24 @@ states:
135
139
  - kind: shell
136
140
  options:
137
141
  command: >-
138
- bun "${vars.render_script}" --in "${vars.work_dir}/content.json"
142
+ set -e;
143
+ rs="${vars.render_script}";
144
+ if [ ! -f "$rs" ]; then
145
+ kbin="$(command -v kk 2>/dev/null || true)";
146
+ while [ -L "$kbin" ]; do
147
+ link="$(readlink "$kbin")";
148
+ case "$link" in /*) kbin="$link";; *) kbin="$(dirname "$kbin")/$link";; esac;
149
+ done;
150
+ pkg=""; [ -n "$kbin" ] && pkg="$(cd "$(dirname "$kbin")/.." 2>/dev/null && pwd || true)";
151
+ rs="";
152
+ if [ -n "$pkg" ] && [ -f "$pkg/plugins/kk/scripts/render-md.ts" ]; then rs="$pkg/plugins/kk/scripts/render-md.ts";
153
+ elif [ -f "plugins/kk/scripts/render-md.ts" ]; then rs="plugins/kk/scripts/render-md.ts"; fi;
154
+ fi;
155
+ if [ -z "$rs" ]; then
156
+ echo "render_script not found (searched: var, kk package root via \`command -v kk\`, ./plugins/kk/scripts)" >&2;
157
+ exit 1;
158
+ fi;
159
+ bun "$rs" --in "${vars.work_dir}/content.json"
139
160
  --out "${vars.work_dir}/content.md"
140
161
 
141
162
  - id: done
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Runtime VoiceScript→Doc[] wrapper for kk-solo-podcast.
4
+ *
5
+ * Copied with the workflow YAML into $KK_WORKFLOWS_DIR (default
6
+ * ~/.config/kk/workflows). Node/Bun builtins only — no @gobing-ai/* and no
7
+ * relative imports (those paths do not exist next to the runtime copy).
8
+ *
9
+ * SYNC: plugins/kk/workflows/kk-solo-podcast.yaml (wrap-docs state shells this
10
+ * script with scriptPath, outPath, voiceProfile argv). The wrap shape must stay
11
+ * a single-Doc array with sha256(file://scriptPath)[0:16] id — voice-gen and
12
+ * tests depend on it.
13
+ */
14
+ import { createHash } from 'node:crypto';
15
+ import { readFileSync, writeFileSync } from 'node:fs';
16
+ import { resolve } from 'node:path';
17
+
18
+ const [scriptPathArg, outPathArg, voiceProfileArg] = process.argv.slice(2);
19
+
20
+ if (!scriptPathArg || !outPathArg) {
21
+ console.error('usage: wrap-voicescript-doc.ts <scriptPath> <outPath> [voiceProfile]');
22
+ process.exit(1);
23
+ }
24
+
25
+ const scriptPath = resolve(scriptPathArg);
26
+ const outPath = resolve(outPathArg);
27
+ const voiceProfile = voiceProfileArg || process.env.VOICEBOX_DEFAULT_PROFILE || '';
28
+ const body = readFileSync(scriptPath, 'utf-8');
29
+ const sourceUri = `file://${scriptPath}`;
30
+ const id = createHash('sha256').update(sourceUri).digest('hex').slice(0, 16);
31
+ const doc = {
32
+ id,
33
+ title: 'Podcast VoiceScript',
34
+ sourceUri,
35
+ body,
36
+ mediaType: 'application/yaml',
37
+ metadata: voiceProfile ? { voiceProfile } : {},
38
+ };
39
+
40
+ writeFileSync(outPath, JSON.stringify([doc], null, 2));