@gobing-ai/knowledge-kit 0.0.32 → 0.0.33

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 (65) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +32320 -15021
  3. package/package.json +4 -4
  4. package/plugins/generations/content-gen/dist/index.js +10 -9
  5. package/plugins/generations/core-facts-gen/dist/index.js +13 -12
  6. package/plugins/generations/daily-article-gen/dist/index.js +14 -13
  7. package/plugins/generations/daily-article-gen/src/index.ts +1 -1
  8. package/plugins/generations/dailynews-gen/dist/index.js +10 -9
  9. package/plugins/generations/episode-plan-gen/dist/index.js +58 -15
  10. package/plugins/generations/episode-plan-gen/src/index.ts +76 -6
  11. package/plugins/generations/gemini-tts/README.md +11 -0
  12. package/plugins/generations/gemini-tts/dist/index.js +10 -9
  13. package/plugins/generations/image-gen/dist/index.js +154 -151
  14. package/plugins/generations/image-gen/src/compose/chromium.ts +60 -42
  15. package/plugins/generations/news-report-gen/dist/index.js +13 -12
  16. package/plugins/generations/voice-gen/dist/index.js +10 -9
  17. package/plugins/ingestions/aihot-ingest/dist/index.js +13 -12
  18. package/plugins/ingestions/horizon-ingest/dist/index.js +13 -12
  19. package/plugins/ingestions/karakeep-local/dist/index.js +13 -13
  20. package/plugins/ingestions/last30days-ingest/dist/index.js +13 -12
  21. package/plugins/ingestions/web-search/dist/index.js +11 -10
  22. package/plugins/kk/README.md +38 -0
  23. package/plugins/kk/decisions/extract/ai-news.yaml +21 -0
  24. package/plugins/kk/decisions/kk-items.yaml +113 -0
  25. package/plugins/kk/plugin.json +1 -1
  26. package/plugins/kk/schedules/cron.tmpl +15 -0
  27. package/plugins/kk/schedules/launchd.plist.tmpl +77 -0
  28. package/plugins/kk/scripts/daily-stages.ts +1533 -0
  29. package/plugins/kk/scripts/kk-workflow-stages.ts +247 -1365
  30. package/plugins/kk/skills/content-safety/SKILL.md +38 -7
  31. package/plugins/kk/skills/web-automation/SKILL.md +100 -0
  32. package/plugins/kk/skills/web-automation/references/browser-launch.md +86 -0
  33. package/plugins/kk/workflows/kk-collect.yaml +258 -0
  34. package/plugins/kk/workflows/kk-daily-ai-voice.yaml +9 -3
  35. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +655 -0
  36. package/plugins/kk/workflows/kk-daily-safety.yaml +170 -0
  37. package/plugins/publishings/emdash-pub/dist/index.js +10 -9
  38. package/plugins/publishings/infoq-pub/dist/index.js +152 -59
  39. package/plugins/publishings/infoq-pub/plugin.json +1 -0
  40. package/plugins/publishings/infoq-pub/src/index.ts +167 -55
  41. package/plugins/publishings/infoq-pub/src/profile-check.ts +4 -2
  42. package/plugins/publishings/juejin-pub/dist/index.js +111 -30
  43. package/plugins/publishings/juejin-pub/plugin.json +1 -0
  44. package/plugins/publishings/juejin-pub/src/index.ts +49 -9
  45. package/plugins/publishings/medium-pub/dist/index.js +10 -9
  46. package/plugins/publishings/podcast-pub/dist/index.js +10 -9
  47. package/plugins/publishings/qiita-pub/dist/index.js +10 -9
  48. package/plugins/publishings/substack-pub/dist/index.js +105 -28
  49. package/plugins/publishings/substack-pub/plugin.json +1 -0
  50. package/plugins/publishings/substack-pub/src/index.ts +39 -8
  51. package/plugins/publishings/substack-pub/src/profile-check.ts +2 -0
  52. package/plugins/publishings/surfdash-pub/dist/index.js +10 -9
  53. package/plugins/publishings/wechatmp-pub/dist/index.js +115 -31
  54. package/plugins/publishings/wechatmp-pub/plugin.json +1 -0
  55. package/plugins/publishings/wechatmp-pub/src/index.ts +42 -9
  56. package/plugins/publishings/wechatmp-pub/src/profile-check.ts +84 -40
  57. package/plugins/publishings/x-pub/dist/index.js +114 -35
  58. package/plugins/publishings/x-pub/plugin.json +1 -0
  59. package/plugins/publishings/x-pub/src/index.ts +68 -14
  60. package/plugins/publishings/xhs-pub/dist/index.js +100 -54
  61. package/plugins/publishings/xhs-pub/plugin.json +1 -0
  62. package/plugins/publishings/xhs-pub/src/index.ts +8 -4
  63. package/plugins/publishings/xhs-pub/src/page.ts +4 -17
  64. package/plugins/publishings/xhs-pub/src/profile-check.ts +28 -15
  65. package/plugins/publishings/zenn-pub/dist/index.js +10 -9
@@ -0,0 +1,655 @@
1
+ name: kk-daily-ai-voice2
2
+ kind: dag
3
+ description: >-
4
+ Daily AI tech-sharing pipeline for the 「AI 乱炖」 column of show 「咸话咸说」, rebuilt as a static
5
+ dependency DAG over the kk store (task 0211, ADR-031, design docs/design/kk-collect-and-daily-voice2.md §6).
6
+ It starts from the store, not from ingestion: `kk store extract` selects today's pool from the
7
+ labels `kk-collect` (0208/0209) wrote, and the store's `episode_usage` ledger is the cross-day
8
+ dedup. The daily work is unchanged after that — plan → content QC → article/cover/script →
9
+ content safety (nested state machine, kk-daily-safety.yaml) → operator review → audio → publish
10
+ (surfdash + podcast) → independent soft channels (xhs, wechat, locales) → classify → ledger. It
11
+ supersedes kk-daily-ai-voice.yaml, which stays untouched until cut-over §8.
12
+ vars:
13
+ # Composed by the `prepare` node (`kk daily prepare`), then read back into vars through the
14
+ # single-node read chain below — shell actions cannot set vars, and a concurrent `setVars` merge
15
+ # is last-writer-wins, so the DAG exchanges data by fixed paths under work_dir, never by vars.
16
+ work_dir: ""
17
+ # Digits-only run-date stamp the artifact names carry; projected from <work_dir>/run-date.txt.
18
+ run_date: ""
19
+ # Store extract window (kk store extract --window). The old workflow's ingestion window was
20
+ # intrinsic to the fan-in; here it is the pool's freshness bound (kk-collect refreshes every 6h).
21
+ window: "24h"
22
+ language: "zh"
23
+ voice_profile: "robin-news"
24
+ # Voice driver switch — voicebox (voice-gen) | ominivoice (omni-voice-gen) | gemini (gemini-tts).
25
+ # Same default as kk-daily-ai-voice: gemini-tts (20260929 operator override, verified live).
26
+ vdriver: "gemini"
27
+ intro_music: ".works/kk-daily-ai-voice/AI每日资讯_8秒播客片头.mp3"
28
+ surfdash_root: "/Users/robin/xprojects/surfdash"
29
+ title: ""
30
+ skip_review: "false"
31
+ transcode_mp3: "true"
32
+ podcast_artist: "Robin Min"
33
+ podcast_album: "咸话咸说"
34
+ cover_enabled: "true"
35
+ cover_provider: "minimax"
36
+ cover_model: "image-01"
37
+ cover_fallback: ""
38
+ # 0219 R3: image-gen vendor fallback for the XHS render; "" restores strict fail-fast.
39
+ xhs_fallback: "true"
40
+ xhs_enabled: "true"
41
+ xhs_cards: "4"
42
+ xhs_style: "xhs-knowledge"
43
+ xhs_provider: "minimax"
44
+ xhs_model: "image-01"
45
+ # ADR-022 rail: draft-box only. "true" posts live and stays a launch-time opt-in.
46
+ xhs_live: "false"
47
+ wechat_enabled: "true"
48
+ wechat_live: "false"
49
+ wechat_collection: "AI 乱炖"
50
+ plan_max_items: "20"
51
+ plan_translate_enabled: "true"
52
+ article_write_enabled: "true"
53
+ script_write_enabled: "true"
54
+ plugins_path: ""
55
+ # Content-safety budget and platforms, forwarded to the nested kk-daily-safety state machine.
56
+ safety_max_revise: "2"
57
+ safety_platforms: "xiaoyuzhou,apple-podcasts,wechatmp"
58
+ publish_enabled: "false"
59
+ agent: "inline"
60
+ # QC pins — lockstep with episode-plan-gen's DEFAULT_FILTER_CONFIG (env wins), same as voice1.
61
+ qc_categories: "ai-news,ai-technical,ai-research,ai-industry"
62
+ qc_min_quality: "2"
63
+ qc_min_importance: "3"
64
+ qc_min_urgency: "2"
65
+ qc_min_impact: "1"
66
+ qc_min_episode_items: "8"
67
+ episode_min_minutes: "15"
68
+ episode_max_minutes: "20"
69
+ # Run id, injected by spur at launch (0674 R5/R8) — the episode_usage / ledger / mark-used
70
+ # identity. A shell command may only reference declared vars.
71
+ __runId: ""
72
+
73
+ # One action per node (engine: DagNodeDef.action), no back-edges (ADR-031). A node whose deps are
74
+ # ALL skipped is skipped; a done+skipped mix proceeds — which is how the empty-pool path still
75
+ # reaches run-report and ledger (they list `extract` in dependsOn, G11 R3).
76
+ #
77
+ # NODES ARE DECLARED IN TOPOLOGICAL ORDER (every node after all of its `dependsOn`). The engine
78
+ # settles skips in one sweeping pass over this list, in declaration order, and stops as soon as a
79
+ # pass finds no ready node — a node declared before a dependency that becomes `skipped` later in
80
+ # that same pass stays `pending` and the run ends `dag-unreachable-nodes` instead of `done`.
81
+ #
82
+ # Fail-fast is the other half: any failed node fails the run, so every non-blocking channel (xhs,
83
+ # wechat, locales) keeps its exit-0-on-soft-failure kk stage, and `onError: continue` stays where
84
+ # voice1 had it. Wave-level waiting is the accepted ceiling (design §6): a node starts when its own
85
+ # deps settle, but its whole wave is awaited before the next wave is dispatched.
86
+ nodes:
87
+ - id: prepare
88
+ description: >-
89
+ Sync the workflow ledger and compose today's workspace: `kk daily prepare` writes the effective
90
+ work_dir to .spur/runs/kk-daily-ai-voice2/work-dir.txt, the run date to <work_dir>/run-date.txt,
91
+ the artifact directories and the safety revise budget. `kk ledger sync` records runs that
92
+ already finished without a ledger row (failed ones included) — a fail-fast DAG never reaches
93
+ its own `ledger` node, so the next run's prepare is the backfill (design §7).
94
+ action:
95
+ kind: shell
96
+ options:
97
+ command: >-
98
+ kk daily prepare --workflow kk-daily-ai-voice2 --run-date "${vars.run_date}" --ledger-sync
99
+
100
+ - id: read-work-dir
101
+ description: "Project the effective work_dir into vars (single-node wave: nothing has fanned out yet)"
102
+ dependsOn: [prepare]
103
+ action:
104
+ kind: file.read.into-var
105
+ options:
106
+ path: .spur/runs/kk-daily-ai-voice2/work-dir.txt
107
+ var: work_dir
108
+
109
+ - id: read-run-date
110
+ description: "Project the digits-only run-date stamp into vars (single-node wave, after work_dir is known)"
111
+ dependsOn: [read-work-dir]
112
+ action:
113
+ kind: file.read.into-var
114
+ options:
115
+ path: ${vars.work_dir}/run-date.txt
116
+ var: run_date
117
+
118
+ - id: extract
119
+ description: >-
120
+ Select today's episode pool from the kk store: the ai-news topic profile over the labels
121
+ kk-collect wrote, minus everything this workflow already used (episode_usage). Writes
122
+ <work_dir>/10-collect/pool.json (the fan-in blended.json shape) and prints {count}. An empty
123
+ pool writes [] and exits 0 — the run's no-content path, not a failure.
124
+ dependsOn: [read-run-date]
125
+ action:
126
+ kind: shell
127
+ options:
128
+ command: >-
129
+ kk store extract --topic ai-news --window "${vars.window}" --exclude-used kk-daily-ai-voice2
130
+ --out "${vars.work_dir}/10-collect/pool.json"
131
+
132
+ - id: plan
133
+ description: >-
134
+ Score/dedup/select + annotate the pool into the episode plan (episode-plan-gen; annotate rides
135
+ inside planEpisode). The labels extract inlined into metadata replace its keyword re-scoring.
136
+ Guarded on the pool: an empty pool skips the whole content subtree, and run-report/ledger
137
+ still run.
138
+ dependsOn: [extract]
139
+ condition:
140
+ kind: shell
141
+ options:
142
+ command: 'kk store pool-count "${vars.work_dir}/10-collect/pool.json" --min 1'
143
+ action:
144
+ kind: shell
145
+ options:
146
+ command: >-
147
+ kk stage daily-plan "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.plan_max_items}"
148
+
149
+ - id: qc-content
150
+ description: >-
151
+ Content QC: filter the plan's annotated candidates by category allowlist + score thresholds
152
+ (episode-plan-gen --mode filter). Writes 21-plan/<date>_04_quality-control-content_candidates.json,
153
+ whose length guards the whole content fan-out below — a pool that filtered to zero items ends
154
+ the run as no-content instead of building an empty episode.
155
+ dependsOn: [plan]
156
+ action:
157
+ kind: shell
158
+ options:
159
+ command: >-
160
+ kk stage daily-qc-content "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.qc_categories}" "${vars.qc_min_quality}" "${vars.qc_min_importance}" "${vars.qc_min_urgency}" "${vars.qc_min_impact}" "${vars.qc_min_episode_items}"
161
+
162
+ # ---- content branches: translate / prose article / cover / script author (concurrent) ----
163
+ - id: plan-translate
164
+ description: "Translate the needsTranslation plan items into the output plan.json via agent.run"
165
+ dependsOn: [qc-content]
166
+ condition:
167
+ kind: shell
168
+ options:
169
+ command: >-
170
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
171
+ && test "${vars.plan_translate_enabled}" = "true"
172
+ action:
173
+ kind: agent.run
174
+ options:
175
+ role: scribe
176
+ agent: ${vars.agent}
177
+ # ADR-115 error tier: keep this prompt <= 1000 chars (0133 R3; 0138 R4a copy discipline).
178
+ input: |
179
+ Read ${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json — metadata.docs (Doc[]) is the plan; don't edit it.
180
+ For needsTranslation items, adapt "title", "body" and "metadata.brief" into spoken simplified Chinese — full coverage, not a summary: keep every fact, number, name, quote and hedge (口播全长改编,不摘要不省略), body ≈0.6–0.7× the source.
181
+ Titles read as a news event; keep product/model names verbatim, preserve body URLs, use 「」 not ASCII double quotes.
182
+ Copy discipline: reader-facing news copy — no scaffold labels (「可做角度」「为什么现在值得注意」「社区讨论」「背景」「潜在影响」「影响」), no duplicated labels or hedging (「读者宜另行核实」「仍待验证」) — merge their substance into prose or drop.
183
+ Keep other fields (id, sourceUri, mediaType, metadata) as-is; no comments or trailing commas.
184
+ Output strictly valid JSON — a same-length, same-order array — to ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json.
185
+ expectFile: ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json
186
+ allowAppend: false
187
+
188
+ - id: article-write
189
+ description: "Write the tech-sharing prose article from the translated plan via agent.run (3-format split: surfdash gets prose, not a news list)"
190
+ dependsOn: [qc-content]
191
+ condition:
192
+ kind: shell
193
+ options:
194
+ command: >-
195
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
196
+ && test "${vars.article_write_enabled}" = "true"
197
+ action:
198
+ kind: agent.run
199
+ options:
200
+ role: scribe
201
+ agent: ${vars.agent}
202
+ # ADR-115 error tier: keep this prompt <= 1000 chars. No title line: the canonical episode
203
+ # title is computed by daily-article-gen (0189 R4).
204
+ input: |
205
+ Read ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json — metadata.docs (Doc[]) is the translated plan; don't edit it.
206
+ Write a tech-sharing prose article in natural simplified Chinese for the 「AI 乱炖」 column (show 「咸话咸说」, tools/papers/hands-on practice 随聊): no title line (the pipeline computes it), an intro paragraph (2–3 sentences), then one section per item (## numbered title, 2–4 sentences of flowing prose, source link [来源:<label>](<sourceUri>) inline), a closing paragraph tying items together.
207
+ 选题边界:只写技术内容;某条主线若属政治/外交或贸易管制/军事/公共政策争议/社会公共事件,跳过该条并在文末加一行「> 注:<N> 条条目因选题边界未写入:<id>(<原因>)、<id>(<原因>)」。
208
+ 事实纪律:每个事实取自该条计划内容,不虚构、不引他处;数字与主体不得放大;引语只能归属其原句主语;保留「据报道/称/承诺」等限定词;导语/收束不加计划外程度词;材料不足就少写或不写,不凑数。
209
+ Narrative register — smooth transitions, no list-like bulleting, no scaffold labels (「可做角度」「背景」「潜在影响」), no hedging (「读者宜另行核实」「仍待验证」). 「」 quotes.
210
+ Output the markdown to ${vars.work_dir}/22-article/${vars.run_date}_06_article-write_body.md.
211
+ expectFile: ${vars.work_dir}/22-article/${vars.run_date}_06_article-write_body.md
212
+ allowAppend: false
213
+
214
+ - id: article
215
+ description: >-
216
+ Generate the dated daily article Content from the episode plan (daily-article-gen template).
217
+ Depends on the QC candidates (so the prose/translate nodes can be disabled without skipping
218
+ this node: a done dependency in the mix keeps it runnable) as well as on both authoring nodes,
219
+ and it carries the same candidates guard — a pool that filtered to zero items must not build
220
+ an empty article.
221
+ dependsOn: [qc-content, plan-translate, article-write]
222
+ condition:
223
+ kind: shell
224
+ options:
225
+ command: >-
226
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
227
+ action:
228
+ kind: shell
229
+ options:
230
+ command: >-
231
+ kk stage daily-article "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
232
+
233
+ - id: cover-brief
234
+ description: "Author the cover subject from the lead plan item via agent.run (F614; onError: continue — a bad brief falls back to the format default subject in the render stage)"
235
+ dependsOn: [qc-content]
236
+ condition:
237
+ kind: shell
238
+ options:
239
+ command: >-
240
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
241
+ && test "${vars.cover_enabled}" = "true"
242
+ && test ! -f "${vars.work_dir}/23-cover/${vars.run_date}_07_cover_cover.png"
243
+ action:
244
+ kind: agent.run
245
+ # A bad or failed brief must never halt the run: the stage falls back to the format's default
246
+ # subject and keeps the same pinned format/style/provider/model.
247
+ onError: continue
248
+ options:
249
+ role: scribe
250
+ agent: ${vars.agent}
251
+ # ADR-115 error tier: keep this prompt <= 1000 chars.
252
+ input: |
253
+ Read ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json — a JSON array of plan docs; don't edit it.
254
+ Take item 1 only (its "title" and "metadata.brief") — the episode's lead story. If item 1's mainline is public affairs (politics/diplomacy, trade controls, military, policy disputes, social events), take the first in-scope item instead.
255
+ Write one concrete visual scene in English, ≤240 characters, that a viewer would connect to that story: objects, settings, symbols, metaphor.
256
+ No text, letters, numbers, logos, brand marks, flags, or recognizable real people. No style, color, lighting, or composition words — the preset owns those.
257
+ Output strictly valid JSON {"subject": "..."} to ${vars.work_dir}/23-cover/${vars.run_date}_07_cover-brief.json.
258
+ expectFile: ${vars.work_dir}/23-cover/${vars.run_date}_07_cover-brief.json
259
+ allowAppend: false
260
+
261
+ - id: cover
262
+ description: "Render the daily cover through the pinned daily-news presets (F614)"
263
+ dependsOn: [cover-brief]
264
+ action:
265
+ kind: shell
266
+ options:
267
+ command: >-
268
+ kk stage daily-cover "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.cover_provider}" "${vars.cover_model}" "${vars.cover_fallback}"
269
+
270
+ - id: cover-normalize
271
+ description: "Normalize the rendered cover (dimensions/format pins) so every consumer sees the same asset"
272
+ dependsOn: [cover]
273
+ action:
274
+ kind: shell
275
+ options:
276
+ command: >-
277
+ kk stage daily-cover-normalize "${vars.work_dir}" "${vars.run_date}"
278
+
279
+ - id: script-style
280
+ description: "Snapshot the shipped zh style contract into the work dir (separate node: it is also an audit input for the revision loop)"
281
+ dependsOn: [qc-content]
282
+ condition:
283
+ kind: shell
284
+ options:
285
+ command: >-
286
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
287
+ action:
288
+ kind: shell
289
+ options:
290
+ command: >-
291
+ kk stage daily-script-style "${vars.work_dir}" "${vars.run_date}"
292
+
293
+ - id: script-write
294
+ description: "Author the per-item script plan (intro/outro + lead/insight per doc id) from the plan via agent.run; onError: continue — a failed plan falls back to templates (F613)"
295
+ dependsOn: [script-style]
296
+ condition:
297
+ kind: shell
298
+ options:
299
+ command: >-
300
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
301
+ && test "${vars.script_write_enabled}" = "true"
302
+ && test "${vars.language}" = "zh"
303
+ action:
304
+ kind: agent.run
305
+ onError: continue
306
+ options:
307
+ role: scribe
308
+ agent: ${vars.agent}
309
+ # ADR-115 error tier: keep this prompt <= 1000 chars.
310
+ input: |
311
+ Read ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json — a JSON array of docs; don't edit it. Also read the style contract ${vars.work_dir}/24-script/${vars.run_date}_08_script-write_style.md and obey its char caps, duration budget and banned phrases.
312
+ For each doc write a `lead` (a spoken bridge, not containing the title) and an `insight` (grounded only in that doc's facts: keep its numbers and hedges(据报道), add no new degree words).
313
+ intro/outro hook only in-scope items; no greeting or sign-off — the builder adds both. 选题边界:政治/外交、贸易管制、军事、公共政策争议、社会公共事件条目绝不复述;读文章 22-article/*_06_article-write_body.md,其文末注记点名的 id 一并进 excluded,各配中文原因。
314
+ Output only JSON {"version":1,"intro":"…","outro":"…","items":[{"id":"…","lead":"…","insight":"…"}],"excluded":[{"id":"…","reason":"…"}]} to ${vars.work_dir}/24-script/${vars.run_date}_08_script-write_plan.json. Exactly one `items` entry per doc not in `excluded`.
315
+ expectFile: ${vars.work_dir}/24-script/${vars.run_date}_08_script-write_plan.json
316
+ allowAppend: false
317
+
318
+ - id: script
319
+ description: >-
320
+ Compile the episode plan Doc[] into structured broadcast VoiceScript YAML Content. Depends on
321
+ `article` so the node still runs when the plan-authoring nodes were disabled or skipped (the
322
+ builder falls back to templates) — but not when the whole content branch is skipped.
323
+ dependsOn: [article, script-style, script-write]
324
+ condition:
325
+ kind: shell
326
+ options:
327
+ command: >-
328
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
329
+ action:
330
+ kind: shell
331
+ options:
332
+ command: >-
333
+ kk stage daily-script "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.language}" "${vars.title}" "${vars.voice_profile}"
334
+
335
+ # ---- content safety: a nested state machine (no sub-workflow action exists; ADR-031) ----
336
+ - id: safety
337
+ description: >-
338
+ Content safety as a nested state machine (kk-daily-safety.yaml): review → judge → bounded
339
+ revise loop → clean|block. It blocks here (and only here), so a non-zero exit means the drafts
340
+ must not publish — `block` or an integrity failure. The launcher vars the parent run exports
341
+ are stripped at the nested-spur boundary (the same two `NESTED_SPUR_UNSET` vars the stage
342
+ runner strips, kk-workflow-stages.ts): without it `spur workflow run` refuses to start inside
343
+ an active run. Non-publishing runs skip the gate entirely, exactly like voice1.
344
+ dependsOn: [script]
345
+ condition:
346
+ kind: shell
347
+ options:
348
+ command: 'test "${vars.publish_enabled}" = "true"'
349
+ action:
350
+ kind: shell
351
+ options:
352
+ command: >-
353
+ env -u SPUR_WORKFLOW_RUN_ACTIVE -u SPUR_EXPECTED_DEFINITION_DIGEST
354
+ spur workflow run plugins/kk/workflows/kk-daily-safety.yaml
355
+ --vars '{"work_dir":"${vars.work_dir}","run_date":"${vars.run_date}","safety_max_revise":"${vars.safety_max_revise}","safety_platforms":"${vars.safety_platforms}","agent":"${vars.agent}"}'
356
+ --quiet --no-plan
357
+
358
+ - id: review
359
+ description: >-
360
+ Operator review of the safety-gated drafts: `pause: true` stops the run here and RESUME is the
361
+ approval (the engine marks a paused node done on resume, so this node carries no action);
362
+ cancelling the run is the rejection. Same contract as voice1's HITL confirmation, now a node
363
+ instead of a state. Skipped when skip_review=true or when there is nothing to publish.
364
+ dependsOn: [safety]
365
+ pause: true
366
+ condition:
367
+ kind: shell
368
+ options:
369
+ command: 'test "${vars.skip_review}" != "true"'
370
+
371
+ # ---- audio branch ----
372
+ # wrap-docs must be DECLARED BEFORE generate: the DAG requires nodes in topological order, and
373
+ # generate consumes wrap-docs's _10_wrap-docs_docs.json as its TTS input (0212 dogfood finding —
374
+ # an edge to a later-declared node wedges the dispatcher with generate never dispatched).
375
+ - id: wrap-docs
376
+ description: "Wrap VoiceScript into Doc[] input for the voice generator plugin (also the publish-prep input)"
377
+ dependsOn: [script, review]
378
+ condition:
379
+ kind: shell
380
+ options:
381
+ command: >-
382
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
383
+ action:
384
+ kind: shell
385
+ options:
386
+ command: >-
387
+ kk stage daily-wrap-docs "${vars.work_dir}" "${vars.run_date}" "${vars.voice_profile}"
388
+
389
+ - id: generate
390
+ description: "Invoke the voice driver plugin (vdriver: gemini -> gemini-tts) to generate the audio"
391
+ # wrap-docs is a data dependency, not a sibling: daily-generate reads its _10_wrap-docs_docs.json
392
+ # as the TTS input. Without this edge the two nodes race in the same wave (0212 dogfood finding).
393
+ dependsOn: [script, safety, review, wrap-docs]
394
+ condition:
395
+ kind: shell
396
+ options:
397
+ command: >-
398
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
399
+ action:
400
+ kind: shell
401
+ options:
402
+ command: >-
403
+ OMNIVOICE_SPEED=1.06 kk stage daily-generate "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.transcode_mp3}" "${vars.vdriver}"
404
+
405
+ - id: intro
406
+ description: "Prepend the 8s intro sting (intro_music var; empty = skip), update duration/audioPath"
407
+ dependsOn: [generate]
408
+ action:
409
+ kind: shell
410
+ options:
411
+ command: >-
412
+ kk stage daily-intro "${vars.work_dir}" "${vars.run_date}" "${vars.intro_music}"
413
+
414
+ - id: duration
415
+ description: "Fail loud unless final episode duration sits in the 15–20 min window (duration authoritative)"
416
+ dependsOn: [intro]
417
+ action:
418
+ kind: shell
419
+ options:
420
+ command: >-
421
+ kk stage daily-episode-duration "${vars.work_dir}" "${vars.run_date}" "${vars.episode_min_minutes}" "${vars.episode_max_minutes}"
422
+
423
+ # ---- publish-prep branch ----
424
+ - id: publish-prep
425
+ description: "Merge article Content with the MP3 (rejects a non-.mp3 enclosure) and stamp its ID3 tags — the publish seam's input"
426
+ dependsOn: [wrap-docs]
427
+ condition:
428
+ kind: shell
429
+ options:
430
+ command: 'test "${vars.publish_enabled}" = "true"'
431
+ action:
432
+ kind: shell
433
+ options:
434
+ command: >-
435
+ kk stage daily-publish-prep "${vars.work_dir}" "${vars.run_date}" "${vars.podcast_artist}" "${vars.podcast_album}"
436
+
437
+ # ---- audio QC: fans in the audio branch and the publish seam ----
438
+ - id: quality
439
+ description: >-
440
+ Verify audio quality, speech rate, and silence/repetition QC metrics. Depends on the audio
441
+ tail, wrap-docs and publish-prep (the seam's merge is part of the audited bytes); publish-prep
442
+ being skipped on a non-publishing run does not skip the audit.
443
+ dependsOn: [duration, wrap-docs, publish-prep]
444
+ action:
445
+ kind: shell
446
+ options:
447
+ command: >-
448
+ kk stage daily-quality-report "${vars.work_dir}" "${vars.run_date}"
449
+
450
+ - id: publish-surfdash
451
+ description: "Publish the merged Content to surfdash (single target); its result.json is what show-notes links and what patch-surfdash (folded into publish-classify) patches"
452
+ dependsOn: [publish-prep]
453
+ condition:
454
+ kind: shell
455
+ options:
456
+ command: 'test "${vars.publish_enabled}" = "true"'
457
+ action:
458
+ kind: shell
459
+ options:
460
+ command: >-
461
+ kk stage daily-publish-surfdash "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
462
+
463
+ - id: show-notes
464
+ description: "Render timeline show notes from the episode plan (podcast-pub show-notes op; article URL from the surfdash result postPath — a surfdash failure degrades to the legacy full-body layout)"
465
+ dependsOn: [quality, publish-surfdash]
466
+ action:
467
+ kind: shell
468
+ options:
469
+ command: >-
470
+ kk stage daily-show-notes "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.title}"
471
+
472
+ - id: publish-podcast
473
+ description: "Publish the episode through podcast-pub with the rendered show notes (metadata.showNotes)"
474
+ dependsOn: [show-notes]
475
+ condition:
476
+ kind: shell
477
+ options:
478
+ command: 'test "${vars.publish_enabled}" = "true"'
479
+ action:
480
+ kind: shell
481
+ options:
482
+ command: >-
483
+ kk stage daily-publish-podcast "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
484
+
485
+ # ---- independent soft channels: xhs, wechat, locales (exit 0 on soft failure; fail-fast DAG) ----
486
+ - id: xhs-write
487
+ description: "Plan the XHS card series from the lead plan item via agent.run; onError: continue — a failed plan skips the XHS target, never the run"
488
+ dependsOn: [script, review]
489
+ condition:
490
+ kind: shell
491
+ options:
492
+ command: >-
493
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
494
+ && test "${vars.xhs_enabled}" = "true"
495
+ action:
496
+ kind: agent.run
497
+ onError: continue
498
+ options:
499
+ role: scribe
500
+ agent: ${vars.agent}
501
+ # ADR-115 error tier: keep this prompt <= 1000 chars.
502
+ input: |
503
+ Read ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json — a JSON array of news docs; don't edit it.
504
+ Take item 1 only (its "title" and "metadata.brief") — the lead story.
505
+ Write an XHS note: "title" (≤20 chars) and "body" (Chinese note ≤1000 chars, ending with 3-5 hashtags). 选题边界:公共事务主线不写入,只写技术内容。
506
+ Write exactly ${vars.xhs_cards} cards: card 1 role "cover", the rest "content", except a final "closing". Each card carries "role", "copy" (Chinese on-card headline ≤12 chars), "points" (2-4 short Chinese bullets), "layout", "subject" (one concrete English visual scene), "sourcePoint" (the lead-story fact that card uses).
507
+ Ground every "copy", "points" and "sourcePoint" in that story — no invented facts. No text, letters, numbers, logos or real people in "subject".
508
+ Output strictly valid JSON {"title": "...", "body": "...", "cards": [...]} to ${vars.work_dir}/25-xhs/${vars.run_date}_18_xhs_plan.json.
509
+ expectFile: ${vars.work_dir}/25-xhs/${vars.run_date}_18_xhs_plan.json
510
+ allowAppend: false
511
+
512
+ - id: xhs-render
513
+ description: "Render the card series through the pinned xhs-card/xhs-knowledge presets; onError: continue — the publish node names the missing series and stays soft"
514
+ dependsOn: [xhs-write]
515
+ action:
516
+ kind: shell
517
+ onError: continue
518
+ options:
519
+ command: >-
520
+ kk stage daily-xhs-render "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.xhs_cards}" "${vars.xhs_style}" "${vars.xhs_provider}" "${vars.xhs_model}" "${vars.xhs_fallback}"
521
+
522
+ - id: xhs-publish
523
+ description: "Publish the packaged card series to XHS — draft, or live only when xhs_live=true (ADR-022); independent and non-blocking: its outcome never enters publish_status, and xhs-pub's own headless preflight + publish cap make a sessionless run fail soft (exit 0)"
524
+ dependsOn: [xhs-render]
525
+ action:
526
+ kind: shell
527
+ options:
528
+ command: >-
529
+ kk stage daily-publish-xhs "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.xhs_live}"
530
+
531
+ - id: wechat-publish
532
+ description: "Publish the daily article to the WeChat MP draft box via wechatmp-pub — draft, or live only when wechat_live=true; independent and non-blocking (a sessionless profile refuses fast, exit 0)"
533
+ dependsOn: [publish-prep]
534
+ condition:
535
+ kind: shell
536
+ options:
537
+ command: 'test "${vars.publish_enabled}" = "true" && test "${vars.wechat_enabled}" = "true"'
538
+ action:
539
+ kind: shell
540
+ options:
541
+ command: >-
542
+ kk stage daily-publish-wechat "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.wechat_live}" "${vars.wechat_collection}"
543
+
544
+ - id: translate-localize
545
+ description: "Locales — surfdash-pub scaffolds the en/ja drafts (deterministic, plugin-owned) and writes one marker file per locale naming the draft to translate"
546
+ dependsOn: [publish-surfdash]
547
+ condition:
548
+ kind: shell
549
+ options:
550
+ command: 'test "${vars.publish_enabled}" = "true"'
551
+ action:
552
+ kind: shell
553
+ options:
554
+ command: >-
555
+ kk stage daily-translate-localize "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
556
+
557
+ - id: translate
558
+ description: >-
559
+ Agent translates both scaffolded locale drafts in place. The two draft paths are read from the
560
+ marker files at their fixed paths (no var projection after the fan-out: concurrent setVars is
561
+ last-writer-wins, design §6), and an empty marker means that locale is absent. Non-blocking:
562
+ the sync node and publish-classify treat the locales as an independent channel.
563
+ dependsOn: [translate-localize]
564
+ action:
565
+ kind: agent.run
566
+ options:
567
+ role: scribe
568
+ agent: ${vars.agent}
569
+ input: |
570
+ Translate the en/ja draft posts of the zh article. Read the two marker files at these fixed paths — each holds one draft path, or is empty when that locale was not scaffolded:
571
+ English: ${vars.work_dir}/31-publish/${vars.run_date}_20_translate-localize_en-post-path.txt
572
+ Japanese: ${vars.work_dir}/31-publish/${vars.run_date}_20_translate-localize_ja-post-path.txt
573
+ For each: if the marker is empty or unreadable, skip that locale. Otherwise edit the named file in place — translate the frontmatter title and the whole markdown body to natural English/Japanese respectively. Keep frontmatter keys, dates, URLs, code, and the assets/ image reference untouched. Set draft: false (the translated post publishes to the site) and remove any canonical: frontmatter line pointing at a different locale's URL.
574
+ Keep frontmatter valid YAML: double-quote any value containing ': '. Read the surfdash translator glossary if present at the surfdash checkout root under .agents/skills/surfdash-translator/glossary.md and apply its terminology.
575
+ Do not wait for operator confirmation. Each edited file must end fully in its target language.
576
+
577
+ - id: translate-sync
578
+ description: "Sync the translated en/ja drafts into the surfdash CMS (the zh post path rides the marker file)"
579
+ dependsOn: [translate]
580
+ action:
581
+ kind: shell
582
+ options:
583
+ command: >-
584
+ kk stage daily-translate-sync "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.work_dir}/31-publish/${vars.run_date}_20_translate-localize_zh-post-path.txt"
585
+
586
+ # ---- fan-in: classify the publish outcome, then close the run ----
587
+ - id: publish-classify
588
+ description: >-
589
+ Fan-in of every publish leaf (surfdash, podcast, xhs, wechat, locales) with policy `all`:
590
+ classifies the sequenced surfdash → podcast outcomes into publish-status.txt full|partial|none
591
+ (exit 0 in all three), records the independent channels' completeness, and folds in the
592
+ surfdash post-patching + the partial-publish marker that voice1 ran as separate states (a
593
+ classify is the only node that knows the status). Non-publishing runs skip the whole subtree.
594
+ dependsOn: [publish-podcast, publish-surfdash, xhs-publish, wechat-publish, translate-sync]
595
+ dependencyPolicy: all
596
+ condition:
597
+ kind: shell
598
+ options:
599
+ command: 'test "${vars.publish_enabled}" = "true"'
600
+ action:
601
+ kind: shell
602
+ options:
603
+ command: >-
604
+ kk daily publish-classify --work-dir "${vars.work_dir}" --run-date "${vars.run_date}"
605
+ --surfdash-root "${vars.surfdash_root}" --plugins-path "${vars.plugins_path}" --fold-tail
606
+
607
+ - id: mark-used
608
+ description: >-
609
+ Record the items this run used in the store's episode_usage (the cross-day dedup): runs after
610
+ publish-classify, so a run that failed to classify records nothing. Skipped on an empty pool
611
+ (there is no plan to mark).
612
+ dependsOn: [publish-classify, extract]
613
+ condition:
614
+ kind: shell
615
+ options:
616
+ command: >-
617
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
618
+ action:
619
+ kind: shell
620
+ options:
621
+ command: >-
622
+ kk store mark-used --workflow kk-daily-ai-voice2 --run "${vars.__runId}"
623
+ --in "${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json"
624
+
625
+ - id: run-report
626
+ description: >-
627
+ Assemble the per-run report from the existing artifacts (news-report-gen; output-only, never
628
+ throws) and record the pool outcome next to it: `no-content` when the store pool was empty or
629
+ filtered to zero items. It lists `extract` in dependsOn, so an empty pool still runs it (the
630
+ engine's skip rule: all-deps-skipped skips a node, a done+skipped mix proceeds).
631
+ dependsOn: [mark-used, publish-classify, extract]
632
+ action:
633
+ kind: shell
634
+ options:
635
+ command: >-
636
+ kk daily run-report --work-dir "${vars.work_dir}" --run-date "${vars.run_date}"
637
+ --plugins-path "${vars.plugins_path}" --qc-min-quality "${vars.qc_min_quality}"
638
+ --qc-min-importance "${vars.qc_min_importance}" --qc-min-urgency "${vars.qc_min_urgency}"
639
+ --qc-min-impact "${vars.qc_min_impact}" --pool "${vars.work_dir}/10-collect/pool.json"
640
+ --candidates "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json"
641
+
642
+ - id: ledger
643
+ description: >-
644
+ Final node: append this run's section to .works/kk-daily-ai-voice2/ledger.md. It lists
645
+ `extract` in dependsOn so an empty-pool run still records (with the run-report's `no-content`
646
+ line as its Result). onError: continue — a run's own trace is still `running` while a node
647
+ executes, so `kk ledger append` refuses it (exit 1); the next run's `prepare` backfills
648
+ through `kk ledger sync`.
649
+ dependsOn: [run-report, extract]
650
+ action:
651
+ kind: shell
652
+ onError: continue
653
+ options:
654
+ command: >-
655
+ kk ledger append --workflow kk-daily-ai-voice2 --run "${vars.__runId}" --work-dir "${vars.work_dir}"