@gobing-ai/knowledge-kit 0.0.32 → 0.0.34

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 (72) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +43047 -24724
  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 +16 -21
  7. package/plugins/generations/daily-article-gen/src/index.ts +8 -12
  8. package/plugins/generations/dailynews-gen/dist/index.js +23 -21
  9. package/plugins/generations/dailynews-gen/src/script-builder.ts +25 -26
  10. package/plugins/generations/episode-plan-gen/dist/index.js +58 -15
  11. package/plugins/generations/episode-plan-gen/src/index.ts +76 -6
  12. package/plugins/generations/gemini-tts/README.md +11 -0
  13. package/plugins/generations/gemini-tts/dist/index.js +10 -9
  14. package/plugins/generations/image-gen/dist/index.js +171 -153
  15. package/plugins/generations/image-gen/presets/formats/xhs-card.yaml +16 -14
  16. package/plugins/generations/image-gen/presets/styles/xhs-knowledge.yaml +2 -2
  17. package/plugins/generations/image-gen/src/compose/card.ts +17 -2
  18. package/plugins/generations/image-gen/src/compose/chromium.ts +60 -42
  19. package/plugins/generations/news-report-gen/dist/index.js +13 -12
  20. package/plugins/generations/voice-gen/dist/index.js +10 -9
  21. package/plugins/ingestions/aihot-ingest/dist/index.js +13 -12
  22. package/plugins/ingestions/horizon-ingest/dist/index.js +13 -12
  23. package/plugins/ingestions/karakeep-local/dist/index.js +13 -13
  24. package/plugins/ingestions/last30days-ingest/dist/index.js +13 -12
  25. package/plugins/ingestions/web-search/dist/index.js +11 -10
  26. package/plugins/kk/README.md +38 -0
  27. package/plugins/kk/decisions/extract/ai-news.yaml +27 -0
  28. package/plugins/kk/decisions/kk-items.yaml +179 -0
  29. package/plugins/kk/plugin.json +1 -1
  30. package/plugins/kk/schedules/cron.tmpl +15 -0
  31. package/plugins/kk/schedules/launchd.plist.tmpl +77 -0
  32. package/plugins/kk/scripts/daily-stages.ts +2023 -0
  33. package/plugins/kk/scripts/daily-voice-style.zh.md +15 -10
  34. package/plugins/kk/scripts/kk-workflow-stages.ts +253 -1368
  35. package/plugins/kk/skills/content-safety/SKILL.md +39 -8
  36. package/plugins/kk/skills/content-safety/references/platforms/apple-podcasts.md +2 -2
  37. package/plugins/kk/skills/web-automation/SKILL.md +100 -0
  38. package/plugins/kk/skills/web-automation/references/browser-launch.md +86 -0
  39. package/plugins/kk/workflows/kk-collect.yaml +256 -0
  40. package/plugins/kk/workflows/kk-daily-ai-voice.yaml +9 -3
  41. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +774 -0
  42. package/plugins/kk/workflows/kk-daily-safety.yaml +167 -0
  43. package/plugins/publishings/emdash-pub/dist/index.js +10 -9
  44. package/plugins/publishings/infoq-pub/dist/index.js +152 -59
  45. package/plugins/publishings/infoq-pub/plugin.json +1 -0
  46. package/plugins/publishings/infoq-pub/src/index.ts +167 -55
  47. package/plugins/publishings/infoq-pub/src/profile-check.ts +4 -2
  48. package/plugins/publishings/juejin-pub/dist/index.js +111 -30
  49. package/plugins/publishings/juejin-pub/plugin.json +1 -0
  50. package/plugins/publishings/juejin-pub/src/index.ts +49 -9
  51. package/plugins/publishings/medium-pub/dist/index.js +10 -9
  52. package/plugins/publishings/podcast-pub/dist/index.js +10 -9
  53. package/plugins/publishings/qiita-pub/dist/index.js +10 -9
  54. package/plugins/publishings/substack-pub/dist/index.js +105 -28
  55. package/plugins/publishings/substack-pub/plugin.json +1 -0
  56. package/plugins/publishings/substack-pub/src/index.ts +39 -8
  57. package/plugins/publishings/substack-pub/src/profile-check.ts +2 -0
  58. package/plugins/publishings/surfdash-pub/dist/index.js +10 -9
  59. package/plugins/publishings/wechatmp-pub/dist/index.js +115 -31
  60. package/plugins/publishings/wechatmp-pub/plugin.json +1 -0
  61. package/plugins/publishings/wechatmp-pub/src/index.ts +42 -9
  62. package/plugins/publishings/wechatmp-pub/src/profile-check.ts +84 -40
  63. package/plugins/publishings/x-pub/dist/index.js +114 -35
  64. package/plugins/publishings/x-pub/plugin.json +1 -0
  65. package/plugins/publishings/x-pub/src/index.ts +68 -14
  66. package/plugins/publishings/xhs-pub/dist/index.js +154 -59
  67. package/plugins/publishings/xhs-pub/plugin.json +1 -0
  68. package/plugins/publishings/xhs-pub/src/flow.ts +9 -3
  69. package/plugins/publishings/xhs-pub/src/index.ts +119 -9
  70. package/plugins/publishings/xhs-pub/src/page.ts +4 -17
  71. package/plugins/publishings/xhs-pub/src/profile-check.ts +28 -15
  72. package/plugins/publishings/zenn-pub/dist/index.js +10 -9
@@ -0,0 +1,774 @@
1
+ name: kk-daily-ai-voice2
2
+ kind: dag
3
+ description: >-
4
+ Daily AI tech-sharing pipeline for 「咸话咸说之每日AI乱炖」 (column 「每日AI乱炖」), 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 topic-scoped `selections` ledger records the
8
+ set at QC so it never re-enters a later pool — whether or not the run publishes. The daily work is
9
+ unchanged after that — plan → content QC → article/cover/script →
10
+ content safety (nested state machine, kk-daily-safety.yaml) → operator review → audio → publish
11
+ (surfdash + podcast) → independent soft channels (xhs, wechat, locales) → classify → ledger. It
12
+ supersedes kk-daily-ai-voice.yaml, which stays untouched until cut-over §8.
13
+ vars:
14
+ # Composed by the `prepare` node (`kk daily prepare`), then read back into vars through the
15
+ # single-node read chain below — shell actions cannot set vars, and a concurrent `setVars` merge
16
+ # is last-writer-wins, so the DAG exchanges data by fixed paths under work_dir, never by vars.
17
+ work_dir: ""
18
+ # Digits-only run-date stamp the artifact names carry; projected from <work_dir>/run-date.txt.
19
+ run_date: ""
20
+ # Store extract window (kk store extract --window). The old workflow's ingestion window was
21
+ # intrinsic to the fan-in; here it is the pool's freshness bound (kk-collect refreshes every 6h).
22
+ window: "24h"
23
+ language: "zh"
24
+ voice_profile: "robin-news"
25
+ # Voice driver switch — voicebox (voice-gen) | ominivoice (omni-voice-gen) | gemini (gemini-tts).
26
+ # Same default as kk-daily-ai-voice: gemini-tts (20260929 operator override, verified live).
27
+ vdriver: "gemini"
28
+ intro_music: ".works/kk-daily-ai-voice/AI每日资讯_8秒播客片头.mp3"
29
+ surfdash_root: "/Users/robin/xprojects/surfdash"
30
+ title: ""
31
+ skip_review: "false"
32
+ transcode_mp3: "true"
33
+ podcast_artist: "Robin Min"
34
+ podcast_album: "咸话咸说"
35
+ cover_enabled: "true"
36
+ cover_provider: "minimax"
37
+ cover_model: "image-01"
38
+ cover_fallback: ""
39
+ # 0219 R3: image-gen vendor fallback for the XHS render; "" restores strict fail-fast.
40
+ xhs_fallback: "true"
41
+ xhs_enabled: "true"
42
+ xhs_cards: "4"
43
+ xhs_style: "xhs-knowledge"
44
+ xhs_provider: "minimax"
45
+ xhs_model: "image-01"
46
+ # ADR-022 rail: draft-box only. "true" posts live and stays a launch-time opt-in.
47
+ xhs_live: "false"
48
+ wechat_enabled: "true"
49
+ wechat_live: "false"
50
+ wechat_collection: "每日AI乱炖"
51
+ plan_max_items: "20"
52
+ plan_translate_enabled: "true"
53
+ article_write_enabled: "true"
54
+ script_write_enabled: "true"
55
+ plugins_path: ""
56
+ # Content-safety budget and platforms, forwarded to the nested kk-daily-safety state machine.
57
+ safety_max_revise: "2"
58
+ safety_platforms: "xiaoyuzhou,apple-podcasts,wechatmp"
59
+ publish_enabled: "false"
60
+ agent: "inline"
61
+ # 0238 R2: executor for the `-retry` siblings of the two hard-fail agent.run producers. The
62
+ # siblings declare `role: reviewer` (capable-1), two rungs above the primary's `scribe`, so the
63
+ # default `inline` resolves the retry above any rung the primary reaches by climbing past an
64
+ # unusable cheap executor (ac5-live-1). The operator may still pin a known-good executor here.
65
+ agent_retry: "inline"
66
+ # QC pins — lockstep with episode-plan-gen's DEFAULT_FILTER_CONFIG (env wins), same as voice1.
67
+ qc_categories: "ai-news,ai-technical,ai-research,ai-industry"
68
+ qc_min_quality: "2"
69
+ qc_min_importance: "3"
70
+ qc_min_urgency: "2"
71
+ qc_min_impact: "1"
72
+ qc_min_episode_items: "8"
73
+ episode_min_minutes: "15"
74
+ # 2026-10-09 operator decision: ingest bodies are spoken in full and today's pools render 22-46
75
+ # minutes at the current item counts, so the 20-minute ceiling refused real episodes before any
76
+ # TTS request. The pair drives BOTH the pre-spend estimate in `daily-generate` and the measured
77
+ # gate in `daily-episode-duration`.
78
+ episode_max_minutes: "25"
79
+ # Run id, injected by spur at launch (0674 R5/R8) — the selections / ledger / mark-published
80
+ # identity. A shell command may only reference declared vars.
81
+ __runId: ""
82
+
83
+ # One action per node (engine: DagNodeDef.action), no back-edges (ADR-031). A node whose deps are
84
+ # ALL skipped is skipped; a done+skipped mix proceeds — which is how the empty-pool path still
85
+ # reaches run-report and ledger (they list `extract` in dependsOn, G11 R3).
86
+ #
87
+ # NODES ARE DECLARED IN TOPOLOGICAL ORDER (every node after all of its `dependsOn`). The engine
88
+ # settles skips in one sweeping pass over this list, in declaration order, and stops as soon as a
89
+ # pass finds no ready node — a node declared before a dependency that becomes `skipped` later in
90
+ # that same pass stays `pending` and the run ends `dag-unreachable-nodes` instead of `done`.
91
+ #
92
+ # Fail-fast is the other half: any failed node fails the run, so every non-blocking channel (xhs,
93
+ # wechat, locales) keeps its exit-0-on-soft-failure kk stage. `onError: continue` sits where voice1 had
94
+ # it (cover-brief, script-write) and, since 0238 R2, on the two hard-fail agent.run producers
95
+ # (plan-translate, article-write), whose `-retry` siblings below carry the loud second attempt.
96
+ # Wave-level waiting is the accepted ceiling (design §6): a node starts when its own deps settle,
97
+ # but its whole wave is awaited before the next wave is dispatched.
98
+ nodes:
99
+ - id: prepare
100
+ description: >-
101
+ Sync the workflow ledger and compose today's workspace: `kk daily prepare` writes the effective
102
+ work_dir to .spur/runs/kk-daily-ai-voice2/work-dir.txt, the run date to <work_dir>/run-date.txt,
103
+ the artifact directories and the safety revise budget. `kk ledger sync` records runs that
104
+ already finished without a ledger row (failed ones included) — a fail-fast DAG never reaches
105
+ its own `ledger` node, so the next run's prepare is the backfill (design §7).
106
+ action:
107
+ kind: shell
108
+ options:
109
+ command: >-
110
+ kk daily prepare --workflow kk-daily-ai-voice2 --run-date "${vars.run_date}" --ledger-sync
111
+
112
+ - id: read-work-dir
113
+ description: "Project the effective work_dir into vars (single-node wave: nothing has fanned out yet)"
114
+ dependsOn: [prepare]
115
+ action:
116
+ kind: file.read.into-var
117
+ options:
118
+ path: .spur/runs/kk-daily-ai-voice2/work-dir.txt
119
+ var: work_dir
120
+
121
+ - id: read-run-date
122
+ description: "Project the digits-only run-date stamp into vars (single-node wave, after work_dir is known)"
123
+ dependsOn: [read-work-dir]
124
+ action:
125
+ kind: file.read.into-var
126
+ options:
127
+ path: ${vars.work_dir}/run-date.txt
128
+ var: run_date
129
+
130
+ - id: extract
131
+ description: >-
132
+ Select today's episode pool from the kk store: the ai-news topic profile over the labels
133
+ kk-collect wrote, minus every story already selected or published for the ai-news topic (the
134
+ store's `selections` ledger, written by the `select` node below). Writes
135
+ <work_dir>/10-collect/pool.json (the fan-in blended.json shape) and prints {count}. An empty
136
+ pool writes [] and exits 0 — the run's no-content path, not a failure.
137
+ dependsOn: [read-run-date]
138
+ action:
139
+ kind: shell
140
+ options:
141
+ command: >-
142
+ kk store extract --topic ai-news --window "${vars.window}"
143
+ --out "${vars.work_dir}/10-collect/pool.json"
144
+
145
+ - id: plan
146
+ description: >-
147
+ Score/dedup/select + annotate the pool into the episode plan (episode-plan-gen; annotate rides
148
+ inside planEpisode). The labels extract inlined into metadata replace its keyword re-scoring.
149
+ Guarded on the pool: an empty pool skips the whole content subtree, and run-report/ledger
150
+ still run.
151
+ dependsOn: [extract]
152
+ condition:
153
+ kind: shell
154
+ options:
155
+ command: 'kk store pool-count "${vars.work_dir}/10-collect/pool.json" --min 1'
156
+ action:
157
+ kind: shell
158
+ options:
159
+ command: >-
160
+ kk stage daily-plan "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.plan_max_items}"
161
+
162
+ - id: qc-content
163
+ description: >-
164
+ Content QC: filter the plan's annotated candidates by category allowlist + score thresholds
165
+ (episode-plan-gen --mode filter). Writes 21-plan/<date>_04_quality-control-content_candidates.json,
166
+ whose length guards the whole content fan-out below — a pool that filtered to zero items ends
167
+ the run as no-content instead of building an empty episode.
168
+ dependsOn: [plan]
169
+ action:
170
+ kind: shell
171
+ options:
172
+ command: >-
173
+ 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}"
174
+
175
+ - id: select
176
+ description: >-
177
+ Record today's planned set in the store's topic-scoped `selections` ledger (0228). It runs at
178
+ QC, the moment the run has committed to its episode set, so the cross-day exclusion no longer
179
+ depends on the DAG reaching its tail: a run that fails, pauses at review or skips publish still
180
+ excludes what it planned. Guarded on the QC candidates — a pool that filtered to zero items
181
+ selects nothing.
182
+ dependsOn: [qc-content]
183
+ condition:
184
+ kind: shell
185
+ options:
186
+ command: >-
187
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
188
+ action:
189
+ kind: shell
190
+ options:
191
+ command: >-
192
+ kk store select --topic ai-news --workflow kk-daily-ai-voice2 --run "${vars.__runId}"
193
+ --in "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json"
194
+
195
+ # ---- content branches: translate / prose article / cover / script author (concurrent) ----
196
+ #
197
+ # 0238 R2: `plan-translate` and `article-write` are the two HARD-FAIL agent.run producers — a
198
+ # missing expectFile fails the run (R4 property, kept). The resolved executor is stochastic: a run
199
+ # answered a greeting with zero tool calls and exited 0 (0238 evidence). The engine has no node
200
+ # retry, so each producer carries a `-retry` sibling on a DIFFERENT tier (`agent_retry`, role
201
+ # reviewer) with the SAME input/expectFile; the primary becomes `onError: continue` and the sibling
202
+ # (no onError) is the loud gate. Downstream readers depend on BOTH, so the done+skipped mix of a
203
+ # successful primary still proceeds. Delete the siblings when spur grows an agent.run retry.
204
+ - id: plan-translate
205
+ description: "Translate the needsTranslation plan items into the output plan.json via agent.run; onError: continue — plan-translate-retry below is the loud gate (0238 R2)"
206
+ dependsOn: [qc-content]
207
+ condition:
208
+ kind: shell
209
+ options:
210
+ command: >-
211
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
212
+ && test "${vars.plan_translate_enabled}" = "true"
213
+ && ! test -s "${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json"
214
+ action:
215
+ kind: agent.run
216
+ # 0238 R2: a stochastic executor no-op must not end the run here — the retry sibling decides.
217
+ onError: continue
218
+ options:
219
+ role: scribe
220
+ agent: ${vars.agent}
221
+ # ADR-115 error tier: keep this prompt <= 1000 chars (0133 R3; 0138 R4a copy discipline).
222
+ # The anchor is shared with plan-translate-retry so the two attempts cannot drift apart.
223
+ input: &plan-translate-input |
224
+ 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.
225
+ 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.
226
+ Titles read as a news event; keep product/model names verbatim, preserve body URLs, use 「」 not ASCII double quotes.
227
+ Copy discipline: reader-facing news copy — no scaffold labels (「可做角度」「为什么现在值得注意」「社区讨论」「背景」「潜在影响」「影响」), no duplicated labels or hedging (「读者宜另行核实」「仍待验证」) — merge their substance into prose or drop.
228
+ Keep other fields (id, sourceUri, mediaType, metadata) as-is; no comments or trailing commas.
229
+ Output strictly valid JSON — a same-length, same-order array — to ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json.
230
+ expectFile: ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json
231
+ allowAppend: false
232
+
233
+ - id: plan-translate-retry
234
+ description: >-
235
+ Bounded recovery for plan-translate (0238 R2): runs only when the primary settled without its
236
+ expectFile, retries the same input on a different tier, and carries NO onError — a second miss
237
+ fails the run loudly instead of letting an untranslated plan flow downstream.
238
+ dependsOn: [plan-translate]
239
+ condition:
240
+ kind: shell
241
+ options:
242
+ command: >-
243
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
244
+ && test "${vars.plan_translate_enabled}" = "true"
245
+ && ! test -s "${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json"
246
+ action:
247
+ kind: agent.run
248
+ options:
249
+ role: reviewer
250
+ agent: ${vars.agent_retry}
251
+ input: *plan-translate-input
252
+ expectFile: ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json
253
+ allowAppend: false
254
+
255
+ - id: article-write
256
+ description: "Write the tech-sharing prose article from the translated plan via agent.run (3-format split: surfdash gets prose, not a news list); onError: continue — article-write-retry below is the loud gate (0238 R2)"
257
+ dependsOn: [qc-content, plan-translate, plan-translate-retry]
258
+ condition:
259
+ kind: shell
260
+ options:
261
+ command: >-
262
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
263
+ && test "${vars.article_write_enabled}" = "true"
264
+ && ! test -s "${vars.work_dir}/22-article/${vars.run_date}_06_article-write_body.md"
265
+ action:
266
+ kind: agent.run
267
+ # 0238 R2: a stochastic executor no-op must not end the run here — the retry sibling decides.
268
+ onError: continue
269
+ options:
270
+ role: scribe
271
+ agent: ${vars.agent}
272
+ # ADR-115 error tier: keep this prompt <= 1000 chars. No title line: the canonical episode
273
+ # title is computed by daily-article-gen (0189 R4).
274
+ # The anchor is shared with article-write-retry so the two attempts cannot drift apart.
275
+ input: &article-write-input |
276
+ Read ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json — metadata.docs (Doc[]) is the translated plan; don't edit it.
277
+ Write vivid simplified-Chinese tech news for 咸话咸说之每日AI乱炖 (column 每日AI乱炖, host Robin). No title line. Open with exactly「这里是由Robin为你带来的咸话咸说之每日AI乱炖」。Never「我是咸话咸说」or an AI-production footer. Each ## section: what happened, why it matters, one concrete detail — a human reporter, not a telegram. Inline [来源:<label>](<sourceUri>). Close by tying the items together.
278
+ 选题边界:只写技术内容;某条主线若属政治/外交或贸易管制/军事/公共政策争议/社会公共事件,跳过该条并在文末加一行「> 注:<N> 条条目因选题边界未写入:<id>(<原因>)、<id>(<原因>)」。
279
+ 事实纪律:每个事实取自该条计划内容,不虚构、不引他处;数字与主体不得放大;引语只能归属其原句主语;保留「据报道/称/承诺」等限定词;导语/收束不加计划外程度词;材料不足就少写或不写,不凑数。
280
+ Narrative register — smooth transitions, no list-like bulleting, no scaffold labels (「可做角度」「背景」「潜在影响」), no hedging (「读者宜另行核实」「仍待验证」). 「」 quotes.
281
+ Output the markdown to ${vars.work_dir}/22-article/${vars.run_date}_06_article-write_body.md.
282
+ expectFile: ${vars.work_dir}/22-article/${vars.run_date}_06_article-write_body.md
283
+ allowAppend: false
284
+
285
+ - id: article-write-retry
286
+ description: >-
287
+ Bounded recovery for article-write (0238 R2): runs only when the primary settled without its
288
+ expectFile, retries the same input on a different tier, and carries NO onError — a second miss
289
+ fails the run loudly instead of publishing an article nobody wrote.
290
+ dependsOn: [article-write]
291
+ condition:
292
+ kind: shell
293
+ options:
294
+ command: >-
295
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
296
+ && test "${vars.article_write_enabled}" = "true"
297
+ && ! test -s "${vars.work_dir}/22-article/${vars.run_date}_06_article-write_body.md"
298
+ action:
299
+ kind: agent.run
300
+ options:
301
+ role: reviewer
302
+ agent: ${vars.agent_retry}
303
+ input: *article-write-input
304
+ expectFile: ${vars.work_dir}/22-article/${vars.run_date}_06_article-write_body.md
305
+ allowAppend: false
306
+
307
+ - id: article
308
+ description: >-
309
+ Generate the dated daily article Content from the episode plan (daily-article-gen template).
310
+ Depends on the QC candidates (so the prose/translate nodes can be disabled without skipping
311
+ this node: a done dependency in the mix keeps it runnable) as well as on both authoring nodes
312
+ AND their retry siblings, and it carries the same candidates guard — a pool that filtered to
313
+ zero items must not build an empty article.
314
+ dependsOn: [qc-content, plan-translate, plan-translate-retry, article-write, article-write-retry]
315
+ condition:
316
+ kind: shell
317
+ options:
318
+ command: >-
319
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
320
+ action:
321
+ kind: shell
322
+ options:
323
+ command: >-
324
+ kk stage daily-article "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
325
+
326
+ - id: cover-brief
327
+ 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)"
328
+ # 0238 R3: reads 21-plan/<date>_03_plan_plan.json, so it must not start before the plan
329
+ # producers settle. `qc-content` stays in the list: a done dependency in the mix keeps this node
330
+ # runnable when the plan-authoring nodes are disabled (all-skipped deps skip a node).
331
+ dependsOn: [qc-content, plan-translate, plan-translate-retry]
332
+ condition:
333
+ kind: shell
334
+ options:
335
+ command: >-
336
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
337
+ && test "${vars.cover_enabled}" = "true"
338
+ && test ! -f "${vars.work_dir}/23-cover/${vars.run_date}_07_cover_cover.png"
339
+ action:
340
+ kind: agent.run
341
+ # A bad or failed brief must never halt the run: the stage falls back to the format's default
342
+ # subject and keeps the same pinned format/style/provider/model.
343
+ onError: continue
344
+ options:
345
+ role: scribe
346
+ agent: ${vars.agent}
347
+ # ADR-115 error tier: keep this prompt <= 1000 chars.
348
+ input: |
349
+ Read ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json — a JSON array of plan docs; don't edit it.
350
+ 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.
351
+ Write one concrete visual scene in English, ≤240 characters, that a viewer would connect to that story: objects, settings, symbols, metaphor.
352
+ No text, letters, numbers, logos, brand marks, flags, or recognizable real people. No style, color, lighting, or composition words — the preset owns those.
353
+ Output strictly valid JSON {"subject": "..."} to ${vars.work_dir}/23-cover/${vars.run_date}_07_cover-brief.json.
354
+ expectFile: ${vars.work_dir}/23-cover/${vars.run_date}_07_cover-brief.json
355
+ allowAppend: false
356
+
357
+ - id: cover
358
+ description: "Render the daily cover through the pinned daily-news presets (F614)"
359
+ dependsOn: [cover-brief]
360
+ action:
361
+ kind: shell
362
+ options:
363
+ command: >-
364
+ kk stage daily-cover "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.cover_provider}" "${vars.cover_model}" "${vars.cover_fallback}"
365
+
366
+ - id: cover-normalize
367
+ description: "Normalize the rendered cover (dimensions/format pins) so every consumer sees the same asset"
368
+ dependsOn: [cover]
369
+ action:
370
+ kind: shell
371
+ options:
372
+ command: >-
373
+ kk stage daily-cover-normalize "${vars.work_dir}" "${vars.run_date}"
374
+
375
+ - id: script-style
376
+ description: "Snapshot the shipped zh style contract into the work dir (separate node: it is also an audit input for the revision loop)"
377
+ dependsOn: [qc-content]
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-script-style "${vars.work_dir}" "${vars.run_date}"
388
+
389
+ - id: script-write
390
+ 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)"
391
+ # 0238 R3: reads 21-plan/<date>_03_plan_plan.json, so it must not start before the plan
392
+ # producers settle. `script-style` stays first in the list: a done dependency in the mix keeps
393
+ # this node runnable when the plan-authoring nodes are disabled (all-skipped deps skip a node).
394
+ dependsOn: [script-style, plan-translate, plan-translate-retry]
395
+ condition:
396
+ kind: shell
397
+ options:
398
+ command: >-
399
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
400
+ && test "${vars.script_write_enabled}" = "true"
401
+ && test "${vars.language}" = "zh"
402
+ && ! test -s "${vars.work_dir}/24-script/${vars.run_date}_08_script-write_plan.json"
403
+ action:
404
+ kind: agent.run
405
+ onError: continue
406
+ options:
407
+ role: scribe
408
+ agent: ${vars.agent}
409
+ # ADR-115 error tier: keep this prompt <= 1000 chars.
410
+ input: |
411
+ 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.
412
+ 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).
413
+ 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,各配中文原因。
414
+ 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`.
415
+ expectFile: ${vars.work_dir}/24-script/${vars.run_date}_08_script-write_plan.json
416
+ allowAppend: false
417
+
418
+ - id: script
419
+ description: >-
420
+ Compile the episode plan Doc[] into structured broadcast VoiceScript YAML Content. Depends on
421
+ `article` so the node still runs when the plan-authoring nodes were disabled or skipped (the
422
+ builder falls back to templates) — but not when the whole content branch is skipped.
423
+ dependsOn: [article, script-style, script-write]
424
+ condition:
425
+ kind: shell
426
+ options:
427
+ command: >-
428
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
429
+ action:
430
+ kind: shell
431
+ options:
432
+ command: >-
433
+ kk stage daily-script "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.language}" "${vars.title}" "${vars.voice_profile}"
434
+
435
+ # ---- content safety: a nested state machine (no sub-workflow action exists; ADR-031) ----
436
+ - id: safety
437
+ description: >-
438
+ Content safety as a nested state machine (kk-daily-safety.yaml): review → judge → bounded
439
+ revise loop → clean|block. It blocks here (and only here), so a non-zero exit means the drafts
440
+ must not publish — `block` or an integrity failure. The launcher vars the parent run exports
441
+ are stripped at the nested-spur boundary (the same two `NESTED_SPUR_UNSET` vars the stage
442
+ runner strips, kk-workflow-stages.ts): without it `spur workflow run` refuses to start inside
443
+ an active run. Non-publishing runs skip the gate entirely, exactly like voice1.
444
+ dependsOn: [script]
445
+ condition:
446
+ kind: shell
447
+ options:
448
+ command: 'test "${vars.publish_enabled}" = "true"'
449
+ action:
450
+ kind: shell
451
+ options:
452
+ command: >-
453
+ env -u SPUR_WORKFLOW_RUN_ACTIVE -u SPUR_EXPECTED_DEFINITION_DIGEST
454
+ spur workflow run plugins/kk/workflows/kk-daily-safety.yaml
455
+ --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}\"}"
456
+ --quiet --no-plan
457
+
458
+ - id: review
459
+ description: >-
460
+ Operator review of the safety-gated drafts: `pause: true` stops the run here and RESUME is the
461
+ approval (the engine marks a paused node done on resume, so this node carries no action);
462
+ cancelling the run is the rejection. Same contract as voice1's HITL confirmation, now a node
463
+ instead of a state. Skipped when skip_review=true or when there is nothing to publish.
464
+ dependsOn: [safety]
465
+ pause: true
466
+ condition:
467
+ kind: shell
468
+ options:
469
+ command: 'test "${vars.skip_review}" != "true"'
470
+
471
+ # ---- audio branch ----
472
+ # wrap-docs must be DECLARED BEFORE generate: the DAG requires nodes in topological order, and
473
+ # generate consumes wrap-docs's _10_wrap-docs_docs.json as its TTS input (0212 dogfood finding —
474
+ # an edge to a later-declared node wedges the dispatcher with generate never dispatched).
475
+ - id: wrap-docs
476
+ description: "Wrap VoiceScript into Doc[] input for the voice generator plugin (also the publish-prep input)"
477
+ dependsOn: [script, review]
478
+ condition:
479
+ kind: shell
480
+ options:
481
+ command: >-
482
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
483
+ action:
484
+ kind: shell
485
+ options:
486
+ command: >-
487
+ kk stage daily-wrap-docs "${vars.work_dir}" "${vars.run_date}" "${vars.voice_profile}"
488
+
489
+ - id: generate
490
+ description: "Invoke the voice driver plugin (vdriver: gemini -> gemini-tts) to generate the audio"
491
+ # wrap-docs is a data dependency, not a sibling: daily-generate reads its _10_wrap-docs_docs.json
492
+ # as the TTS input. Without this edge the two nodes race in the same wave (0212 dogfood finding).
493
+ dependsOn: [script, safety, review, wrap-docs]
494
+ condition:
495
+ kind: shell
496
+ options:
497
+ command: >-
498
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
499
+ action:
500
+ kind: shell
501
+ options:
502
+ command: >-
503
+ OMNIVOICE_SPEED=1.06 kk stage daily-generate "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.transcode_mp3}" "${vars.vdriver}" "1" "${vars.episode_max_minutes}"
504
+
505
+ - id: intro
506
+ description: "Prepend the 8s intro sting (intro_music var; empty = skip), update duration/audioPath"
507
+ dependsOn: [generate]
508
+ action:
509
+ kind: shell
510
+ options:
511
+ command: >-
512
+ kk stage daily-intro "${vars.work_dir}" "${vars.run_date}" "${vars.intro_music}"
513
+
514
+ - id: duration
515
+ description: "Fail loud unless final episode duration sits in the 15–20 min window (duration authoritative)"
516
+ dependsOn: [intro]
517
+ action:
518
+ kind: shell
519
+ options:
520
+ command: >-
521
+ kk stage daily-episode-duration "${vars.work_dir}" "${vars.run_date}" "${vars.episode_min_minutes}" "${vars.episode_max_minutes}"
522
+
523
+ # ---- publish-prep branch ----
524
+ # After duration, not wrap-docs: daily-publish-prep reads the generate content's mp3Path.
525
+ # Starting it in the same wave as generate fails the run before TTS has written that file
526
+ # (2026-10-08 dogfood). duration already includes intro, so the stamp lands on the final MP3.
527
+ - id: publish-prep
528
+ description: "Merge article Content with the final MP3 (after intro + duration) and stamp its ID3 tags — the publish seam's input"
529
+ dependsOn: [duration]
530
+ condition:
531
+ kind: shell
532
+ options:
533
+ command: 'test "${vars.publish_enabled}" = "true"'
534
+ action:
535
+ kind: shell
536
+ options:
537
+ command: >-
538
+ kk stage daily-publish-prep "${vars.work_dir}" "${vars.run_date}" "${vars.podcast_artist}" "${vars.podcast_album}"
539
+
540
+ # ---- audio QC: fans in the audio branch and the publish seam ----
541
+ - id: quality
542
+ description: >-
543
+ Verify audio quality, speech rate, and silence/repetition QC metrics. Depends on the audio
544
+ tail, wrap-docs and publish-prep (the seam's merge is part of the audited bytes); publish-prep
545
+ being skipped on a non-publishing run does not skip the audit.
546
+ dependsOn: [duration, wrap-docs, publish-prep]
547
+ action:
548
+ kind: shell
549
+ options:
550
+ command: >-
551
+ kk stage daily-quality-report "${vars.work_dir}" "${vars.run_date}"
552
+
553
+ - id: publish-surfdash
554
+ 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"
555
+ dependsOn: [publish-prep]
556
+ condition:
557
+ kind: shell
558
+ options:
559
+ command: 'test "${vars.publish_enabled}" = "true"'
560
+ action:
561
+ kind: shell
562
+ options:
563
+ command: >-
564
+ kk stage daily-publish-surfdash "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
565
+
566
+ - id: show-notes
567
+ 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)"
568
+ dependsOn: [quality, publish-surfdash]
569
+ action:
570
+ kind: shell
571
+ options:
572
+ command: >-
573
+ kk stage daily-show-notes "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.title}"
574
+
575
+ - id: publish-podcast
576
+ description: "Publish the episode through podcast-pub with the rendered show notes (metadata.showNotes)"
577
+ dependsOn: [show-notes]
578
+ condition:
579
+ kind: shell
580
+ options:
581
+ command: 'test "${vars.publish_enabled}" = "true"'
582
+ action:
583
+ kind: shell
584
+ options:
585
+ command: >-
586
+ kk stage daily-publish-podcast "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
587
+
588
+ # ---- independent soft channels: xhs, wechat, locales (exit 0 on soft failure; fail-fast DAG) ----
589
+ - id: xhs-write
590
+ description: "Plan the XHS card series across the episode's plan items via agent.run; onError: continue — a failed plan skips the XHS target, never the run"
591
+ dependsOn: [script, review]
592
+ condition:
593
+ kind: shell
594
+ options:
595
+ command: >-
596
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
597
+ && test "${vars.xhs_enabled}" = "true"
598
+ action:
599
+ kind: agent.run
600
+ onError: continue
601
+ options:
602
+ role: scribe
603
+ agent: ${vars.agent}
604
+ # ADR-115 error tier: keep this prompt <= 1000 chars.
605
+ input: |
606
+ Read ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json — a JSON array of news docs; don't edit it.
607
+ Plan the whole episode, not the lead item alone. Card 1 is the episode hook.
608
+ Write an XHS note: "title" (≤20 chars) and "body" (Chinese lead paragraph + 3-5 hashtags) — the stage appends one numbered line per image below it. 选题边界:公共事务主线不写入,只写技术内容。
609
+ Write exactly ${vars.xhs_cards} cards in plan order: card 1 "cover"; one "content" card per plan item, up to ${vars.xhs_cards} - 2 of them; the last "closing". Each card carries "role", "copy" (Chinese headline ≤12 chars), "points" (2-4 short Chinese bullets), "layout", "subject" (one concrete English visual scene), "sourcePoint" (that item's fact) and "itemId" (its "id").
610
+ Ground every field in that item — no invented facts. No writing, letters, numbers, logos or people; screens stay blank.
611
+ Output strictly valid JSON {"title": "...", "body": "...", "cards": [...]} to ${vars.work_dir}/25-xhs/${vars.run_date}_18_xhs_plan.json.
612
+ expectFile: ${vars.work_dir}/25-xhs/${vars.run_date}_18_xhs_plan.json
613
+ allowAppend: false
614
+
615
+ - id: xhs-render
616
+ description: "Render the card series through the pinned xhs-card/xhs-knowledge presets, attach the episode cover last and pair every image with its numbered line (0235 note_scope=episode); onError: continue — the publish node names the missing series and stays soft"
617
+ dependsOn: [xhs-write]
618
+ # A skipped xhs-write still admits its dependents, so each XHS node carries its own gate:
619
+ # without it a run with xhs_enabled=false would re-package a stale same-day 25-xhs/ series.
620
+ condition:
621
+ kind: shell
622
+ options:
623
+ command: 'test "${vars.xhs_enabled}" = "true"'
624
+ action:
625
+ kind: shell
626
+ onError: continue
627
+ options:
628
+ command: >-
629
+ 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}" "episode"
630
+
631
+ - id: xhs-publish
632
+ description: "Publish the packaged card series to XHS, plus the episode's 发播客 note as a follow-up in the same console visit (0236, one cap entry) — 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)"
633
+ # publish-prep is a data dependency, not a sibling: daily-publish-xhs reads its
634
+ # _13_publish-prep_content.json as the 发播客 note, so without this edge the two nodes race and the
635
+ # note is dropped silently (the stage logs and still exits 0). A skipped publish-prep — a
636
+ # non-publishing run — still admits this node, so the edge cannot deadlock the DAG.
637
+ dependsOn: [xhs-render, publish-prep]
638
+ condition:
639
+ kind: shell
640
+ options:
641
+ command: 'test "${vars.xhs_enabled}" = "true"'
642
+ action:
643
+ kind: shell
644
+ options:
645
+ command: >-
646
+ kk stage daily-publish-xhs "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.xhs_live}" "true"
647
+
648
+ - id: wechat-publish
649
+ 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)"
650
+ dependsOn: [publish-prep]
651
+ condition:
652
+ kind: shell
653
+ options:
654
+ command: 'test "${vars.publish_enabled}" = "true" && test "${vars.wechat_enabled}" = "true"'
655
+ action:
656
+ kind: shell
657
+ options:
658
+ command: >-
659
+ kk stage daily-publish-wechat "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.wechat_live}" "${vars.wechat_collection}"
660
+
661
+ - id: translate-localize
662
+ description: "Locales — surfdash-pub scaffolds the en/ja drafts (deterministic, plugin-owned) and writes one marker file per locale naming the draft to translate"
663
+ dependsOn: [publish-surfdash]
664
+ condition:
665
+ kind: shell
666
+ options:
667
+ command: 'test "${vars.publish_enabled}" = "true"'
668
+ action:
669
+ kind: shell
670
+ options:
671
+ command: >-
672
+ kk stage daily-translate-localize "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
673
+
674
+ - id: translate
675
+ description: >-
676
+ Agent translates both scaffolded locale drafts in place. The two draft paths are read from the
677
+ marker files at their fixed paths (no var projection after the fan-out: concurrent setVars is
678
+ last-writer-wins, design §6), and an empty marker means that locale is absent. Non-blocking:
679
+ the sync node and publish-classify treat the locales as an independent channel.
680
+ dependsOn: [translate-localize]
681
+ action:
682
+ kind: agent.run
683
+ options:
684
+ role: scribe
685
+ agent: ${vars.agent}
686
+ input: |
687
+ 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:
688
+ English: ${vars.work_dir}/31-publish/${vars.run_date}_20_translate-localize_en-post-path.txt
689
+ Japanese: ${vars.work_dir}/31-publish/${vars.run_date}_20_translate-localize_ja-post-path.txt
690
+ 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.
691
+ 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.
692
+ Do not wait for operator confirmation. Each edited file must end fully in its target language.
693
+
694
+ - id: translate-sync
695
+ description: "Sync the translated en/ja drafts into the surfdash CMS (the zh post path rides the marker file)"
696
+ dependsOn: [translate]
697
+ action:
698
+ kind: shell
699
+ options:
700
+ command: >-
701
+ 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"
702
+
703
+ # ---- fan-in: classify the publish outcome, then close the run ----
704
+ - id: publish-classify
705
+ description: >-
706
+ Fan-in of every publish leaf (surfdash, podcast, xhs, wechat, locales) with policy `all`:
707
+ classifies the sequenced surfdash → podcast outcomes into publish-status.txt full|partial|none
708
+ (exit 0 in all three), records the independent channels' completeness, and folds in the
709
+ surfdash post-patching + the partial-publish marker that voice1 ran as separate states (a
710
+ classify is the only node that knows the status). Non-publishing runs skip the whole subtree.
711
+ dependsOn: [publish-podcast, publish-surfdash, xhs-publish, wechat-publish, translate-sync]
712
+ dependencyPolicy: all
713
+ condition:
714
+ kind: shell
715
+ options:
716
+ command: 'test "${vars.publish_enabled}" = "true"'
717
+ action:
718
+ kind: shell
719
+ options:
720
+ command: >-
721
+ kk daily publish-classify --work-dir "${vars.work_dir}" --run-date "${vars.run_date}"
722
+ --surfdash-root "${vars.surfdash_root}" --plugins-path "${vars.plugins_path}" --fold-tail
723
+
724
+ - id: mark-published
725
+ description: >-
726
+ Promote this run's `selected` rows to `published` in the store's `selections` ledger (0228
727
+ R2). It runs after publish-classify but only when publish actually ran (`publish_enabled=true`),
728
+ so a DRY RUN never promotes: its rows stay `selected`, still exclude the next day, and the
729
+ operator frees them with `kk store release --run <id>`. Skipped on an empty pool (there is no
730
+ selection to promote).
731
+ dependsOn: [publish-classify, extract]
732
+ condition:
733
+ kind: shell
734
+ options:
735
+ command: >-
736
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
737
+ && test "${vars.publish_enabled}" = "true"
738
+ action:
739
+ kind: shell
740
+ options:
741
+ command: >-
742
+ kk store mark-published --topic ai-news --run "${vars.__runId}"
743
+
744
+ - id: run-report
745
+ description: >-
746
+ Assemble the per-run report from the existing artifacts (news-report-gen; output-only, never
747
+ throws) and record the pool outcome next to it: `no-content` when the store pool was empty or
748
+ filtered to zero items. It lists `extract` in dependsOn, so an empty pool still runs it (the
749
+ engine's skip rule: all-deps-skipped skips a node, a done+skipped mix proceeds).
750
+ dependsOn: [mark-published, publish-classify, extract]
751
+ action:
752
+ kind: shell
753
+ options:
754
+ command: >-
755
+ kk daily run-report --work-dir "${vars.work_dir}" --run-date "${vars.run_date}"
756
+ --plugins-path "${vars.plugins_path}" --qc-min-quality "${vars.qc_min_quality}"
757
+ --qc-min-importance "${vars.qc_min_importance}" --qc-min-urgency "${vars.qc_min_urgency}"
758
+ --qc-min-impact "${vars.qc_min_impact}" --pool "${vars.work_dir}/10-collect/pool.json"
759
+ --candidates "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json"
760
+
761
+ - id: ledger
762
+ description: >-
763
+ Final node: append this run's section to .works/kk-daily-ai-voice2/ledger.md. It lists
764
+ `extract` in dependsOn so an empty-pool run still records (with the run-report's `no-content`
765
+ line as its Result). onError: continue — a run's own trace is still `running` while a node
766
+ executes, so `kk ledger append` refuses it (exit 1); the next run's `prepare` backfills
767
+ through `kk ledger sync`.
768
+ dependsOn: [run-report, extract]
769
+ action:
770
+ kind: shell
771
+ onError: continue
772
+ options:
773
+ command: >-
774
+ kk ledger append --workflow kk-daily-ai-voice2 --run "${vars.__runId}" --work-dir "${vars.work_dir}"