@gobing-ai/knowledge-kit 0.1.10 → 0.1.12

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 (158) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +2478 -1737
  3. package/package.json +1 -1
  4. package/plugins/generations/image-gen/README.md +108 -0
  5. package/plugins/generations/image-gen/assets/sample/cover__clean-webapp-ui.jpg +0 -0
  6. package/plugins/generations/image-gen/assets/sample/cover__cute.jpg +0 -0
  7. package/plugins/generations/image-gen/assets/sample/cover__daily-news.jpg +0 -0
  8. package/plugins/generations/image-gen/assets/sample/cover__editorial.jpg +0 -0
  9. package/plugins/generations/image-gen/assets/sample/cover__fresh.jpg +0 -0
  10. package/plugins/generations/image-gen/assets/sample/cover__minimalist.jpg +0 -0
  11. package/plugins/generations/image-gen/assets/sample/cover__photorealistic.jpg +0 -0
  12. package/plugins/generations/image-gen/assets/sample/cover__sketch.jpg +0 -0
  13. package/plugins/generations/image-gen/assets/sample/cover__technical-diagram.jpg +0 -0
  14. package/plugins/generations/image-gen/assets/sample/cover__vibrant.jpg +0 -0
  15. package/plugins/generations/image-gen/assets/sample/cover__warm.jpg +0 -0
  16. package/plugins/generations/image-gen/assets/sample/cover__xhs-knowledge.jpg +0 -0
  17. package/plugins/generations/image-gen/assets/sample/daily-news-cover__clean-webapp-ui.jpg +0 -0
  18. package/plugins/generations/image-gen/assets/sample/daily-news-cover__cute.jpg +0 -0
  19. package/plugins/generations/image-gen/assets/sample/daily-news-cover__daily-news.jpg +0 -0
  20. package/plugins/generations/image-gen/assets/sample/daily-news-cover__editorial.jpg +0 -0
  21. package/plugins/generations/image-gen/assets/sample/daily-news-cover__fresh.jpg +0 -0
  22. package/plugins/generations/image-gen/assets/sample/daily-news-cover__minimalist.jpg +0 -0
  23. package/plugins/generations/image-gen/assets/sample/daily-news-cover__photorealistic.jpg +0 -0
  24. package/plugins/generations/image-gen/assets/sample/daily-news-cover__sketch.jpg +0 -0
  25. package/plugins/generations/image-gen/assets/sample/daily-news-cover__technical-diagram.jpg +0 -0
  26. package/plugins/generations/image-gen/assets/sample/daily-news-cover__vibrant.jpg +0 -0
  27. package/plugins/generations/image-gen/assets/sample/daily-news-cover__warm.jpg +0 -0
  28. package/plugins/generations/image-gen/assets/sample/daily-news-cover__xhs-knowledge.jpg +0 -0
  29. package/plugins/generations/image-gen/assets/sample/free__clean-webapp-ui.jpg +0 -0
  30. package/plugins/generations/image-gen/assets/sample/free__cute.jpg +0 -0
  31. package/plugins/generations/image-gen/assets/sample/free__daily-news.jpg +0 -0
  32. package/plugins/generations/image-gen/assets/sample/free__editorial.jpg +0 -0
  33. package/plugins/generations/image-gen/assets/sample/free__fresh.jpg +0 -0
  34. package/plugins/generations/image-gen/assets/sample/free__minimalist.jpg +0 -0
  35. package/plugins/generations/image-gen/assets/sample/free__photorealistic.jpg +0 -0
  36. package/plugins/generations/image-gen/assets/sample/free__sketch.jpg +0 -0
  37. package/plugins/generations/image-gen/assets/sample/free__technical-diagram.jpg +0 -0
  38. package/plugins/generations/image-gen/assets/sample/free__vibrant.jpg +0 -0
  39. package/plugins/generations/image-gen/assets/sample/free__warm.jpg +0 -0
  40. package/plugins/generations/image-gen/assets/sample/free__xhs-knowledge.jpg +0 -0
  41. package/plugins/generations/image-gen/assets/sample/illustration__clean-webapp-ui.jpg +0 -0
  42. package/plugins/generations/image-gen/assets/sample/illustration__cute.jpg +0 -0
  43. package/plugins/generations/image-gen/assets/sample/illustration__daily-news.jpg +0 -0
  44. package/plugins/generations/image-gen/assets/sample/illustration__editorial.jpg +0 -0
  45. package/plugins/generations/image-gen/assets/sample/illustration__fresh.jpg +0 -0
  46. package/plugins/generations/image-gen/assets/sample/illustration__minimalist.jpg +0 -0
  47. package/plugins/generations/image-gen/assets/sample/illustration__photorealistic.jpg +0 -0
  48. package/plugins/generations/image-gen/assets/sample/illustration__sketch.jpg +0 -0
  49. package/plugins/generations/image-gen/assets/sample/illustration__technical-diagram.jpg +0 -0
  50. package/plugins/generations/image-gen/assets/sample/illustration__vibrant.jpg +0 -0
  51. package/plugins/generations/image-gen/assets/sample/illustration__warm.jpg +0 -0
  52. package/plugins/generations/image-gen/assets/sample/illustration__xhs-knowledge.jpg +0 -0
  53. package/plugins/generations/image-gen/assets/sample/xhs-card__clean-webapp-ui.jpg +0 -0
  54. package/plugins/generations/image-gen/assets/sample/xhs-card__cute.jpg +0 -0
  55. package/plugins/generations/image-gen/assets/sample/xhs-card__daily-news.jpg +0 -0
  56. package/plugins/generations/image-gen/assets/sample/xhs-card__editorial.jpg +0 -0
  57. package/plugins/generations/image-gen/assets/sample/xhs-card__fresh.jpg +0 -0
  58. package/plugins/generations/image-gen/assets/sample/xhs-card__minimalist.jpg +0 -0
  59. package/plugins/generations/image-gen/assets/sample/xhs-card__photorealistic.jpg +0 -0
  60. package/plugins/generations/image-gen/assets/sample/xhs-card__sketch.jpg +0 -0
  61. package/plugins/generations/image-gen/assets/sample/xhs-card__technical-diagram.jpg +0 -0
  62. package/plugins/generations/image-gen/assets/sample/xhs-card__vibrant.jpg +0 -0
  63. package/plugins/generations/image-gen/assets/sample/xhs-card__warm.jpg +0 -0
  64. package/plugins/generations/image-gen/assets/sample/xhs-card__xhs-knowledge.jpg +0 -0
  65. package/plugins/generations/image-gen/dist/index.js +102 -58
  66. package/plugins/generations/image-gen/scripts/compose-e2e-artifact.ts +5 -21
  67. package/plugins/generations/image-gen/scripts/gen-samples.ts +208 -0
  68. package/plugins/generations/image-gen/src/compose/chromium.ts +2 -3
  69. package/plugins/generations/image-gen/src/presets.ts +6 -3
  70. package/plugins/generations/image-gen/src/providers/errors.ts +16 -1
  71. package/plugins/generations/image-gen/src/providers/minimax.ts +46 -17
  72. package/plugins/ingestions/web-search/src/index.ts +1 -1
  73. package/plugins/kk/README.md +18 -0
  74. package/plugins/kk/commands/image-extract.md +1 -0
  75. package/plugins/kk/commands/image-generate.md +1 -0
  76. package/plugins/kk/commands/tell-me.md +1 -0
  77. package/plugins/kk/commands/workflow-run.md +7 -1
  78. package/plugins/kk/decisions/extract/it-topics.yaml +24 -0
  79. package/plugins/kk/decisions/kk-itc.yaml +6 -4
  80. package/plugins/kk/plugin.json +1 -1
  81. package/plugins/kk/publish-presets/daily-ai-voice-migration.json +146 -0
  82. package/plugins/kk/publish-presets/daily-ai-voice.json +11 -3
  83. package/plugins/kk/schedules/kk-collect.grok-bot.md.tmpl +69 -0
  84. package/plugins/kk/schedules/kk-daily-ai-voice3.cron.tmpl +18 -0
  85. package/plugins/kk/schedules/kk-daily-publish3-cn-peak.cron.tmpl +15 -5
  86. package/plugins/kk/schedules/kk-daily-publish3-overseas.cron.tmpl +9 -4
  87. package/plugins/kk/scripts/daily-scheduled-run.ts +298 -0
  88. package/plugins/kk/scripts/daily-stages.ts +1403 -156
  89. package/plugins/kk/scripts/kk-workflow-stages.ts +266 -10
  90. package/plugins/kk/scripts/safety-artifact-check.ts +277 -0
  91. package/plugins/kk/scripts/voice3-publish-window.ts +202 -0
  92. package/plugins/kk/skills/article-adapt/SKILL.md +1 -0
  93. package/plugins/kk/skills/audio-authoring/SKILL.md +1 -0
  94. package/plugins/kk/skills/content-judge/SKILL.md +1 -0
  95. package/plugins/kk/skills/content-safety/SKILL.md +1 -0
  96. package/plugins/kk/skills/explain-things/SKILL.md +1 -0
  97. package/plugins/kk/skills/image-authoring/SKILL.md +1 -0
  98. package/plugins/kk/skills/itc-generating/SKILL.md +1 -0
  99. package/plugins/kk/skills/publish/SKILL.md +1 -0
  100. package/plugins/kk/skills/smart-seo/SKILL.md +1 -0
  101. package/plugins/kk/skills/storm-research/SKILL.md +1 -0
  102. package/plugins/kk/skills/taste-unslop/SKILL.md +1 -0
  103. package/plugins/kk/skills/topic/SKILL.md +1 -0
  104. package/plugins/kk/skills/web-automation/SKILL.md +1 -0
  105. package/plugins/kk/skills/xhs-images/SKILL.md +1 -0
  106. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +177 -128
  107. package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +161 -61
  108. package/plugins/kk/workflows/kk-daily-publish3.yaml +82 -26
  109. package/plugins/kk/workflows/kk-daily-safety.yaml +156 -69
  110. package/plugins/kk/workflows/kk-itc-author.yaml +75 -0
  111. package/plugins/kk/workflows/kk-itc-publish-tail.yaml +25 -3
  112. package/plugins/kk/workflows/kk-itc.yaml +36 -4
  113. package/plugins/publishings/bilibili-pub/dist/index.js +2 -1
  114. package/plugins/publishings/bilibili-pub/dist/profile-check.js +2 -1
  115. package/plugins/publishings/cnblogs-pub/dist/index.js +2 -1
  116. package/plugins/publishings/cnblogs-pub/dist/profile-check.js +2 -1
  117. package/plugins/publishings/csdn-pub/dist/index.js +2 -1
  118. package/plugins/publishings/csdn-pub/dist/profile-check.js +2 -1
  119. package/plugins/publishings/infoq-pub/dist/index.js +2 -1
  120. package/plugins/publishings/infoq-pub/dist/profile-check.js +2 -1
  121. package/plugins/publishings/jike-pub/dist/index.js +2 -1
  122. package/plugins/publishings/jike-pub/dist/profile-check.js +2 -1
  123. package/plugins/publishings/juejin-pub/dist/index.js +2 -1
  124. package/plugins/publishings/juejin-pub/dist/profile-check.js +2 -1
  125. package/plugins/publishings/linkedin-pub/dist/index.js +21333 -0
  126. package/plugins/publishings/linkedin-pub/dist/profile-check.js +21005 -0
  127. package/plugins/publishings/linkedin-pub/package.json +19 -0
  128. package/plugins/publishings/linkedin-pub/plugin.json +12 -0
  129. package/plugins/publishings/linkedin-pub/src/index.ts +705 -0
  130. package/plugins/publishings/linkedin-pub/src/profile-check.ts +17 -0
  131. package/plugins/publishings/linkedin-pub/tsconfig.json +4 -0
  132. package/plugins/publishings/podcast-pub/dist/index.js +46 -8
  133. package/plugins/publishings/podcast-pub/src/index.ts +62 -5
  134. package/plugins/publishings/shipinhao-pub/dist/index.js +2 -1
  135. package/plugins/publishings/shipinhao-pub/dist/profile-check.js +2 -1
  136. package/plugins/publishings/sspai-pub/dist/index.js +2 -1
  137. package/plugins/publishings/sspai-pub/dist/profile-check.js +2 -1
  138. package/plugins/publishings/substack-pub/dist/index.js +2 -1
  139. package/plugins/publishings/substack-pub/dist/profile-check.js +2 -1
  140. package/plugins/publishings/surfdash-pub/dist/index.js +461 -53
  141. package/plugins/publishings/surfdash-pub/src/digest-check.ts +85 -19
  142. package/plugins/publishings/surfdash-pub/src/index.ts +592 -65
  143. package/plugins/publishings/threads-pub/dist/index.js +7 -0
  144. package/plugins/publishings/threads-pub/src/index.ts +12 -0
  145. package/plugins/publishings/wechatmp-pub/dist/index.js +936 -98
  146. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +60 -3
  147. package/plugins/publishings/wechatmp-pub/src/flow.ts +1134 -86
  148. package/plugins/publishings/wechatmp-pub/src/index.ts +112 -30
  149. package/plugins/publishings/wechatmp-pub/src/page.ts +105 -0
  150. package/plugins/publishings/weibo-pub/dist/index.js +2 -1
  151. package/plugins/publishings/weibo-pub/dist/profile-check.js +2 -1
  152. package/plugins/publishings/x-pub/dist/index.js +9 -1
  153. package/plugins/publishings/x-pub/dist/profile-check.js +2 -1
  154. package/plugins/publishings/x-pub/src/index.ts +11 -0
  155. package/plugins/publishings/xhs-pub/dist/index.js +2 -1
  156. package/plugins/publishings/xhs-pub/dist/profile-check.js +2 -1
  157. package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +2 -1
  158. package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +2 -1
@@ -1,3 +1,5 @@
1
+ # Deployment-ready copy. Promote this file to plugins/kk/workflows/kk-daily-ai-voice2.yaml only
2
+ # after approval; all self-validation and nested-workflow references intentionally use active paths.
1
3
  name: kk-daily-ai-voice2
2
4
  kind: dag
3
5
  description: >-
@@ -8,7 +10,7 @@ description: >-
8
10
  set at QC so it never re-enters a later pool — whether or not the run publishes. The daily work is
9
11
  unchanged after that — plan → content QC → article/cover/script →
10
12
  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
13
+ (surfdash + podcast) → soft side channels (xhs, wechat) plus the required local-locale gate → classify → ledger. It
12
14
  supersedes kk-daily-ai-voice.yaml, which stays untouched until cut-over §8.
13
15
  vars:
14
16
  # Composed by the `prepare` node (`kk daily prepare`), then read back into vars through the
@@ -166,7 +168,7 @@ nodes:
166
168
  command: >-
167
169
  kk stage daily-preflight plugins/kk/workflows/kk-daily-ai-voice2.yaml
168
170
  plugins/kk/workflows/kk-daily-safety.yaml
169
- && kk daily prepare --workflow kk-daily-ai-voice2 --run-date "${vars.run_date}" --ledger-sync
171
+ && kk daily prepare --workflow kk-daily-ai-voice2 --work-dir "${vars.work_dir}" --run-date "${vars.run_date}" --ledger-sync
170
172
  --run "${vars.__runId}" --definition-digest "${vars.__definitionDigest}"
171
173
  --setting window="${vars.window}" --setting language="${vars.language}" --setting vdriver="${vars.vdriver}"
172
174
  --setting skip_review="${vars.skip_review}" --setting transcode_mp3="${vars.transcode_mp3}"
@@ -322,6 +324,7 @@ nodes:
322
324
  options:
323
325
  role: scribe
324
326
  agent: ${vars.agent}
327
+ timeoutMs: 600000
325
328
  # ADR-115 error tier: keep this prompt <= 1000 chars (0133 R3; 0138 R4a copy discipline).
326
329
  # The anchor is shared with plan-translate-retry so the two attempts cannot drift apart.
327
330
  # 20261015 forensics: headless agy's terminal sandbox denies workspace writes for EVERY
@@ -359,6 +362,7 @@ nodes:
359
362
  options:
360
363
  role: reviewer
361
364
  agent: ${vars.agent_retry}
365
+ timeoutMs: 600000
362
366
  input: *plan-translate-input
363
367
  expectFile: ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json
364
368
  allowAppend: false
@@ -380,6 +384,7 @@ nodes:
380
384
  options:
381
385
  role: scribe
382
386
  agent: ${vars.agent}
387
+ timeoutMs: 600000
383
388
  # ADR-115 error tier: keep this prompt <= 1000 chars. No title line: the canonical episode
384
389
  # title is computed by daily-article-gen (0189 R4).
385
390
  # The anchor is shared with article-write-retry so the two attempts cannot drift apart.
@@ -411,6 +416,7 @@ nodes:
411
416
  options:
412
417
  role: reviewer
413
418
  agent: ${vars.agent_retry}
419
+ timeoutMs: 600000
414
420
  input: *article-write-input
415
421
  expectFile: ${vars.work_dir}/22-article/${vars.run_date}_06_article-write_body.md
416
422
  allowAppend: false
@@ -455,6 +461,7 @@ nodes:
455
461
  options:
456
462
  role: scribe
457
463
  agent: ${vars.agent}
464
+ timeoutMs: 600000
458
465
  # ADR-115 error tier: keep this prompt <= 1000 chars.
459
466
  input: |
460
467
  Read ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json — a JSON array of plan docs; don't edit it.
@@ -502,7 +509,7 @@ nodes:
502
509
  # 0238 R3: reads 21-plan/<date>_03_plan_plan.json, so it must not start before the plan
503
510
  # producers settle. `script-style` stays first in the list: a done dependency in the mix keeps
504
511
  # this node runnable when the plan-authoring nodes are disabled (all-skipped deps skip a node).
505
- dependsOn: [script-style, plan-translate, plan-translate-retry]
512
+ dependsOn: [script-style, plan-translate, plan-translate-retry, article]
506
513
  condition:
507
514
  kind: shell
508
515
  options:
@@ -517,6 +524,7 @@ nodes:
517
524
  options:
518
525
  role: scribe
519
526
  agent: ${vars.agent}
527
+ timeoutMs: 600000
520
528
  # ADR-115 error tier: keep this prompt <= 1000 chars.
521
529
  input: |
522
530
  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.
@@ -552,6 +560,8 @@ nodes:
552
560
  are stripped at the nested-spur boundary (the same two `NESTED_SPUR_UNSET` vars the stage
553
561
  runner strips, kk-workflow-stages.ts): without it `spur workflow run` refuses to start inside
554
562
  an active run. Non-publishing runs skip the gate entirely, exactly like voice1.
563
+ `skip_review` is forwarded as the nested `unattended` flag, so a real safety escalation cannot
564
+ silently reach publication when the caller has no pause node.
555
565
  dependsOn: [script]
556
566
  condition:
557
567
  kind: shell
@@ -563,7 +573,8 @@ nodes:
563
573
  command: >-
564
574
  env -u SPUR_WORKFLOW_RUN_ACTIVE -u SPUR_EXPECTED_DEFINITION_DIGEST
565
575
  spur workflow run plugins/kk/workflows/kk-daily-safety.yaml
566
- --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}\"}"
576
+ --run-id "${vars.__runId}-safety"
577
+ --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}\",\"agent_retry\":\"${vars.agent_retry}\",\"unattended\":\"${vars.skip_review}\"}"
567
578
  --quiet --no-plan
568
579
 
569
580
  - id: review
@@ -609,8 +620,8 @@ nodes:
609
620
  kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
610
621
  action:
611
622
  kind: shell
623
+ onError: continue
612
624
  options:
613
- onError: continue
614
625
  command: >-
615
626
  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}" "${vars.episode_min_minutes}"
616
627
 
@@ -630,8 +641,8 @@ nodes:
630
641
  && ! jq -e '(.metadata.generator == "kk:voxcpm-tts" or .metadata.generator == "kk:gemini-tts") and .metadata.qc.passed == true' "${vars.work_dir}/30-audio/${vars.run_date}_11_generate_content.json"
631
642
  action:
632
643
  kind: shell
644
+ onError: continue
633
645
  options:
634
- onError: continue
635
646
  command: >-
636
647
  kk stage daily-generate "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.transcode_mp3}" "gemini" "1" "${vars.episode_max_minutes}" "${vars.episode_min_minutes}"
637
648
 
@@ -710,21 +721,10 @@ nodes:
710
721
  command: >-
711
722
  kk stage daily-publish-prep "${vars.work_dir}" "${vars.run_date}" "${vars.podcast_artist}" "${vars.podcast_album}"
712
723
 
713
- # ---- audio QC: fans in the audio branch and the publish seam ----
714
- - id: quality
715
- description: >-
716
- Verify audio quality, speech rate, and silence/repetition QC metrics. Depends on the audio
717
- tail, wrap-docs and publish-prep (the seam's merge is part of the audited bytes); publish-prep
718
- being skipped on a non-publishing run does not skip the audit.
719
- dependsOn: [duration, wrap-docs, publish-prep]
720
- action:
721
- kind: shell
722
- options:
723
- command: >-
724
- kk stage daily-quality-report "${vars.work_dir}" "${vars.run_date}"
725
-
726
- - id: publish-surfdash
727
- 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"
724
+ # Surfdash's CMS sync now requires all locale files. Materialize zh first so the locale branch
725
+ # can scaffold and translate en/ja before the final publish invokes refine/sync.
726
+ - id: publish-surfdash-prepare
727
+ description: "Materialize the local zh Surfdash post without invoking CMS sync; locale translation runs from this path"
728
728
  dependsOn: [publish-prep]
729
729
  condition:
730
730
  kind: shell
@@ -734,80 +734,22 @@ nodes:
734
734
  kind: shell
735
735
  options:
736
736
  command: >-
737
- kk stage daily-publish-surfdash "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
737
+ kk stage daily-prepare-surfdash "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
738
738
 
739
- - id: show-notes
740
- 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)"
741
- dependsOn: [quality, publish-surfdash]
742
- # Pool-gated (0266): its only skip path was dependency cascade, which a done+skipped mix
743
- # defeats — a duration-refused run would still render notes for an unpublished episode.
744
- condition:
745
- kind: shell
746
- options:
747
- command: >-
748
- kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
749
- action:
750
- kind: shell
751
- options:
752
- command: >-
753
- PODCAST_PUB_SEO="${vars.podcast_seo}"
754
- PODCAST_PUB_EPISODE_TITLE_MODE="${vars.podcast_episode_title_mode}"
755
- PODCAST_PUB_EPISODE_TITLE_PATTERN="${vars.podcast_episode_title_pattern}"
756
- PODCAST_PUB_SERIES_NAME="${vars.podcast_series_name}"
757
- PODCAST_PUB_SHOW_NAME="${vars.podcast_show_name}"
758
- PODCAST_PUB_TITLE_MAX="${vars.podcast_title_max}"
759
- PODCAST_PUB_TITLE_MAX_KEYWORDS="${vars.podcast_title_max_keywords}"
760
- PODCAST_PUB_SEO_TITLE_SUFFIX="${vars.podcast_seo_title_suffix}"
761
- PODCAST_PUB_SEO_TITLE_MAX="${vars.podcast_seo_title_max}"
762
- PODCAST_PUB_SEO_DESCRIPTION_MIN="${vars.podcast_seo_description_min}"
763
- PODCAST_PUB_SEO_DESCRIPTION_MAX="${vars.podcast_seo_description_max}"
764
- PODCAST_PUB_SEO_DESCRIPTION_SOURCE="${vars.podcast_seo_description_source}"
765
- PODCAST_PUB_SUMMARY_PARAGRAPH="${vars.podcast_summary_paragraph}"
766
- PODCAST_PUB_TIMELINE_INLINE_STYLE="${vars.podcast_timeline_inline_style}"
767
- PODCAST_PUB_AUTO_EPISODE_NUMBER="${vars.podcast_auto_episode_number}"
768
- PODCAST_PUB_TITLE_SANITIZE="${vars.podcast_title_sanitize}"
769
- PODCAST_PUB_LINK_FIELD="${vars.podcast_link_field}"
770
- PODCAST_PUB_SLUG="${vars.podcast_slug}"
771
- PODCAST_PUB_SLUG_PATTERN="${vars.podcast_slug_pattern}"
772
- PODCAST_PUB_SOCIAL_IMAGE="${vars.podcast_social_image}"
773
- PODCAST_PUB_ROUND_DURATION="${vars.podcast_round_duration}"
774
- kk stage daily-show-notes "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.title}"
775
-
776
- - id: publish-podcast
777
- description: "Publish the episode through podcast-pub with the rendered show notes (metadata.showNotes)"
778
- dependsOn: [show-notes]
779
- condition:
780
- kind: shell
781
- options:
782
- command: 'test "${vars.publish_enabled}" = "true"'
739
+ # ---- audio QC: fans in the audio branch and the publish seam ----
740
+ - id: quality
741
+ description: >-
742
+ Verify audio quality, speech rate, and silence/repetition QC metrics. Depends on the audio
743
+ tail, wrap-docs and publish-prep (the seam's merge is part of the audited bytes); publish-prep
744
+ being skipped on a non-publishing run does not skip the audit.
745
+ dependsOn: [duration, wrap-docs, publish-prep]
783
746
  action:
784
747
  kind: shell
785
748
  options:
786
749
  command: >-
787
- PODCAST_PUB_SEO="${vars.podcast_seo}"
788
- PODCAST_PUB_EPISODE_TITLE_MODE="${vars.podcast_episode_title_mode}"
789
- PODCAST_PUB_EPISODE_TITLE_PATTERN="${vars.podcast_episode_title_pattern}"
790
- PODCAST_PUB_SERIES_NAME="${vars.podcast_series_name}"
791
- PODCAST_PUB_SHOW_NAME="${vars.podcast_show_name}"
792
- PODCAST_PUB_TITLE_MAX="${vars.podcast_title_max}"
793
- PODCAST_PUB_TITLE_MAX_KEYWORDS="${vars.podcast_title_max_keywords}"
794
- PODCAST_PUB_SEO_TITLE_SUFFIX="${vars.podcast_seo_title_suffix}"
795
- PODCAST_PUB_SEO_TITLE_MAX="${vars.podcast_seo_title_max}"
796
- PODCAST_PUB_SEO_DESCRIPTION_MIN="${vars.podcast_seo_description_min}"
797
- PODCAST_PUB_SEO_DESCRIPTION_MAX="${vars.podcast_seo_description_max}"
798
- PODCAST_PUB_SEO_DESCRIPTION_SOURCE="${vars.podcast_seo_description_source}"
799
- PODCAST_PUB_SUMMARY_PARAGRAPH="${vars.podcast_summary_paragraph}"
800
- PODCAST_PUB_TIMELINE_INLINE_STYLE="${vars.podcast_timeline_inline_style}"
801
- PODCAST_PUB_AUTO_EPISODE_NUMBER="${vars.podcast_auto_episode_number}"
802
- PODCAST_PUB_TITLE_SANITIZE="${vars.podcast_title_sanitize}"
803
- PODCAST_PUB_LINK_FIELD="${vars.podcast_link_field}"
804
- PODCAST_PUB_SLUG="${vars.podcast_slug}"
805
- PODCAST_PUB_SLUG_PATTERN="${vars.podcast_slug_pattern}"
806
- PODCAST_PUB_SOCIAL_IMAGE="${vars.podcast_social_image}"
807
- PODCAST_PUB_ROUND_DURATION="${vars.podcast_round_duration}"
808
- kk stage daily-publish-podcast "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
750
+ kk stage daily-quality-report "${vars.work_dir}" "${vars.run_date}"
809
751
 
810
- # ---- independent soft channels: xhs, wechat, locales (exit 0 on soft failure; fail-fast DAG) ----
752
+ # ---- soft side channels + local-locale gate (exit 0 on channel failure; final Surfdash remains fail-closed) ----
811
753
  - id: xhs-write
812
754
  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"
813
755
  dependsOn: [script, review]
@@ -823,6 +765,7 @@ nodes:
823
765
  options:
824
766
  role: scribe
825
767
  agent: ${vars.agent}
768
+ timeoutMs: 600000
826
769
  # ADR-115 error tier: keep this prompt <= 1000 chars.
827
770
  input: |
828
771
  Read ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json — a JSON array of news docs; don't edit it.
@@ -871,22 +814,9 @@ nodes:
871
814
  command: >-
872
815
  kk stage daily-publish-xhs "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.xhs_live}" "true"
873
816
 
874
- - id: wechat-publish
875
- 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)"
876
- dependsOn: [publish-prep]
877
- condition:
878
- kind: shell
879
- options:
880
- command: 'test "${vars.publish_enabled}" = "true" && test "${vars.wechat_enabled}" = "true"'
881
- action:
882
- kind: shell
883
- options:
884
- command: >-
885
- kk stage daily-publish-wechat "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.wechat_live}" "${vars.wechat_collection}"
886
-
887
817
  - id: translate-localize
888
818
  description: "Locales — surfdash-pub scaffolds the en/ja drafts (deterministic, plugin-owned) and writes one marker file per locale naming the draft to translate"
889
- dependsOn: [publish-surfdash]
819
+ dependsOn: [publish-surfdash-prepare]
890
820
  condition:
891
821
  kind: shell
892
822
  options:
@@ -899,16 +829,18 @@ nodes:
899
829
 
900
830
  - id: translate
901
831
  description: >-
902
- Agent translates both scaffolded locale drafts in place. The two draft paths are read from the
832
+ Agent translates both scaffolded locale drafts into <run_date>_21_translate_<locale>.md under the
833
+ work dir (2026-11-04: both executor sandboxes refuse writes into the Surfdash checkout, and the
834
+ step still exits 0); translate-refine installs them over the drafts. The two draft paths are read from the
903
835
  marker files at their fixed paths (no var projection after the fan-out: concurrent setVars is
904
- last-writer-wins, design §6), and an empty marker means that locale is absent. Non-blocking:
905
- the sync node and publish-classify treat the locales as an independent channel.
836
+ last-writer-wins, design §6), and an empty marker means that locale is absent. The local
837
+ validation node gates final Surfdash publish so its strict all-locale CMS policy is met.
906
838
  dependsOn: [translate-localize]
907
839
  action:
908
840
  kind: agent.run
909
841
  options:
910
- # 20261012 hardening: this is the LAST agent step and the locales are documented as an
911
- # independent channel, yet the step carried no tolerance and no retry sibling — a stochastic
842
+ # 20261012 hardening: this is the LAST agent step and a translation failure must not abort
843
+ # the overall report/podcast close, yet the step carried no tolerance and no retry sibling — a stochastic
912
844
  # executor failure here aborted the DAG *after* surfdash and podcast had published, so
913
845
  # publish-classify, mark-published, run-report and ledger never ran for an otherwise complete
914
846
  # day. Tolerating the failure is safe because a scaffold is an unpublished draft: the prompt
@@ -918,26 +850,24 @@ nodes:
918
850
  onError: continue
919
851
  role: scribe
920
852
  agent: ${vars.agent}
921
- # 2026-10-24: `onError: continue` documents the locales as non-blocking, but a failure here
922
- # landed AFTER surfdash/podcast and left `translate-sync` to sync nothing while
923
- # `publish-classify` still reported the day `full` — the en/ja posts never existed. The
924
- # retry sibling below carries the capable tier; `daily-publish-completeness` is what makes
925
- # the residue visible instead of a silent skip.
853
+ timeoutMs: 600000
854
+ # 2026-10-24: `onError: continue` keeps a failed primary eligible for the capable retry
855
+ # sibling below. The local validation node refuses final publish while either locale remains
856
+ # an untranslated scaffold.
926
857
  input: &translate-input |
927
858
  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:
928
859
  English: ${vars.work_dir}/31-publish/${vars.run_date}_20_translate-localize_en-post-path.txt
929
860
  Japanese: ${vars.work_dir}/31-publish/${vars.run_date}_20_translate-localize_ja-post-path.txt
930
- 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.
861
+ For each: if the marker is empty or unreadable, skip that locale. Otherwise read the named draft but do NOT edit it (it sits in the Surfdash checkout, outside your writable workspace): write the complete translated file — frontmatter and body — to ${vars.work_dir}/31-publish/${vars.run_date}_21_translate_en.md (English) or ${vars.work_dir}/31-publish/${vars.run_date}_21_translate_ja.md (Japanese); a later stage installs it over the draft. 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.
931
862
  Also translate these frontmatter values into the target locale, keeping all other keys untouched: description, seo_title, seo_description, and each key_points item. Each stays one coherent standalone sentence that summarizes the same story as the zh source — never splice fragments from different items or stories into one, and key_points must keep exactly the same number of items as the zh post. The 2026-10-23 run failed this: spliced description/key_points had to be hand-repaired before sync.
932
- Series names are fixed product identity, never invented or freely translated: the en series name is "Daily AI Stew" (show "Salty Talk"); the ja series name is "デイリーAIシチュー" (show "咸話咸説"). Never emit 毎日AIブレンド, ごった煮, "Daily AI Medley", "hodgepodge", "Daily AI Briefing", or the zh name 每日AI乱炖 in en/ja prose or link text (a URL keeps whatever it contains). Openers: en "Your Daily AI Stew from Robin, on Salty Talk."; ja "咸話咸説のデイリーAIシチュー、Robin がお届けします。" Listen links: en "[listen to Daily AI Stew · <date>](<url>)", ja "[デイリーAIシチュー · <date> を聴く](<url>)". In each key_points item keep the "Label — sentence" shape: the label before " — " stays a short phrase of at most 14 characters (ja) / 28 (en), never truncated with "…". daily-translate-sync rewrites known retired names deterministically and fails the sync on leftovers.
863
+ Series names are fixed product identity, never invented or freely translated: the en series name is "Daily AI Stew" (show "Salty Talk"); the ja series name is "デイリーAIシチュー" (show "咸話咸説"). Never emit 毎日AIブレンド, ごった煮, "Daily AI Medley", "hodgepodge", "Daily AI Briefing", or the zh name 每日AI乱炖 in en/ja prose or link text (a URL keeps whatever it contains). Openers: en "Your Daily AI Stew from Robin, on Salty Talk."; ja "咸話咸説のデイリーAIシチュー、Robin がお届けします。" Listen links: en "[listen to Daily AI Stew · <date>](<url>)", ja "[デイリーAIシチュー · <date> を聴く](<url>)". In each key_points item keep the "Label — sentence" shape: the label before " — " stays a short phrase of at most 14 characters (ja) / 28 (en), never truncated with "…". The local translate-validate stage rewrites known retired names deterministically and fails the publish gate on leftovers.
933
864
  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.
934
- Do not wait for operator confirmation. Each edited file must end fully in its target language.
865
+ Do not wait for operator confirmation. Each written file must end fully in its target language.
935
866
 
936
867
  # 2026-10-24: the locale translation carried `onError: continue` with NO retry sibling, so a
937
- # failed executor left the en/ja drafts untranslated and `translate-sync` synced nothing while the
938
- # day still classified `full`. Same shape as the two hard-fail producers above: the primary
939
- # tolerates, this sibling is loud-tolerant (a locale is an independent channel, so it continues
940
- # rather than failing the DAG) and runs only when a scaffolded draft is still untranslated.
868
+ # failed executor left the en/ja drafts untranslated and the old sync stage could not distinguish
869
+ # a scaffold from a publishable locale. The primary tolerates, this sibling is loud-tolerant and
870
+ # runs only when a scaffolded draft is still untranslated; translate-validate is the final gate.
941
871
  - id: translate-retry
942
872
  description: "Retry the en/ja locale translation on the capable tier when the primary left a scaffolded draft untranslated (2026-10-24)"
943
873
  dependsOn: [translate]
@@ -946,33 +876,151 @@ nodes:
946
876
  options:
947
877
  command: >-
948
878
  test "$(cat "${vars.work_dir}/31-publish/${vars.run_date}_20_translate-localize_zh-post-path.txt" 2>/dev/null)" != ""
949
- && { for L in en ja; do M="${vars.work_dir}/31-publish/${vars.run_date}_20_translate-localize_$L-post-path.txt"; P="$(cat "$M" 2>/dev/null)"; [ -z "$P" ] && continue; grep -q '^draft: *false' "$P" || { echo "untranslated: $L ($P)"; exit 0; }; done; exit 1; }
879
+ && { for L in en ja; do M="${vars.work_dir}/31-publish/${vars.run_date}_20_translate-localize_$L-post-path.txt"; P="$(cat "$M" 2>/dev/null)"; [ -z "$P" ] && continue; O="${vars.work_dir}/31-publish/${vars.run_date}_21_translate_$L.md"; grep -q '^draft: *false' "$O" || { echo "untranslated: $L ($O)"; exit 0; }; done; exit 1; }
950
880
  action:
951
881
  kind: agent.run
952
882
  options:
953
883
  onError: continue
954
884
  role: reviewer
955
885
  agent: ${vars.agent_retry}
886
+ timeoutMs: 600000
956
887
  input: *translate-input
957
888
 
958
- - id: translate-sync
959
- description: "Sync the translated en/ja drafts into the surfdash CMS (the zh post path rides the marker file)"
889
+ - id: translate-refine
890
+ description: "Run the Surfdash-owned refine pass over zh/en/ja locally before the locale fingerprint is admitted; no CMS, git, or remote side effect"
960
891
  dependsOn: [translate, translate-retry]
892
+ condition:
893
+ kind: shell
894
+ options:
895
+ command: 'test "${vars.publish_enabled}" = "true"'
896
+ action:
897
+ kind: shell
898
+ onError: continue
899
+ options:
900
+ command: >-
901
+ kk stage daily-surfdash-refine "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
902
+
903
+ - id: translate-validate
904
+ description: "Validate and fingerprint the refined translated locale files locally before the final Surfdash publish; no CMS side effect"
905
+ dependsOn: [translate-refine]
906
+ condition:
907
+ kind: shell
908
+ options:
909
+ command: 'test "${vars.publish_enabled}" = "true"'
910
+ action:
911
+ kind: shell
912
+ onError: continue
913
+ options:
914
+ command: >-
915
+ kk stage daily-translate-validate "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.work_dir}/31-publish/${vars.run_date}_20_translate-localize_zh-post-path.txt"
916
+
917
+ - id: publish-surfdash
918
+ description: "Publish the validated merged Content to surfdash (single target); its result.json is what show-notes links and what patch-surfdash (folded into publish-classify) patches"
919
+ dependsOn: [publish-prep, translate-validate]
920
+ condition:
921
+ kind: shell
922
+ options:
923
+ command: 'test "${vars.publish_enabled}" = "true"'
924
+ action:
925
+ kind: shell
926
+ options:
927
+ command: >-
928
+ kk stage daily-publish-surfdash "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "true"
929
+
930
+ - id: wechat-publish
931
+ description: "Publish the daily article via wechatmp-pub after Surfdash settles so its observed public URL fills 原文链接; draft by default, live only when wechat_live=true; failures remain non-blocking"
932
+ dependsOn: [publish-prep, publish-surfdash]
933
+ dependencyPolicy: all
934
+ condition:
935
+ kind: shell
936
+ options:
937
+ command: 'test "${vars.publish_enabled}" = "true" && test "${vars.wechat_enabled}" = "true"'
938
+ action:
939
+ kind: shell
940
+ options:
941
+ command: >-
942
+ kk stage daily-publish-wechat "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.wechat_live}" "${vars.wechat_collection}"
943
+
944
+ - id: show-notes
945
+ 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)"
946
+ dependsOn: [quality, publish-surfdash]
947
+ # Pool-gated (0266): its only skip path was dependency cascade, which a done+skipped mix
948
+ # defeats — a duration-refused run would still render notes for an unpublished episode.
949
+ condition:
950
+ kind: shell
951
+ options:
952
+ command: >-
953
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
954
+ action:
955
+ kind: shell
956
+ options:
957
+ command: >-
958
+ PODCAST_PUB_SEO="${vars.podcast_seo}"
959
+ PODCAST_PUB_EPISODE_TITLE_MODE="${vars.podcast_episode_title_mode}"
960
+ PODCAST_PUB_EPISODE_TITLE_PATTERN="${vars.podcast_episode_title_pattern}"
961
+ PODCAST_PUB_SERIES_NAME="${vars.podcast_series_name}"
962
+ PODCAST_PUB_SHOW_NAME="${vars.podcast_show_name}"
963
+ PODCAST_PUB_TITLE_MAX="${vars.podcast_title_max}"
964
+ PODCAST_PUB_TITLE_MAX_KEYWORDS="${vars.podcast_title_max_keywords}"
965
+ PODCAST_PUB_SEO_TITLE_SUFFIX="${vars.podcast_seo_title_suffix}"
966
+ PODCAST_PUB_SEO_TITLE_MAX="${vars.podcast_seo_title_max}"
967
+ PODCAST_PUB_SEO_DESCRIPTION_MIN="${vars.podcast_seo_description_min}"
968
+ PODCAST_PUB_SEO_DESCRIPTION_MAX="${vars.podcast_seo_description_max}"
969
+ PODCAST_PUB_SEO_DESCRIPTION_SOURCE="${vars.podcast_seo_description_source}"
970
+ PODCAST_PUB_SUMMARY_PARAGRAPH="${vars.podcast_summary_paragraph}"
971
+ PODCAST_PUB_TIMELINE_INLINE_STYLE="${vars.podcast_timeline_inline_style}"
972
+ PODCAST_PUB_AUTO_EPISODE_NUMBER="${vars.podcast_auto_episode_number}"
973
+ PODCAST_PUB_TITLE_SANITIZE="${vars.podcast_title_sanitize}"
974
+ PODCAST_PUB_LINK_FIELD="${vars.podcast_link_field}"
975
+ PODCAST_PUB_SLUG="${vars.podcast_slug}"
976
+ PODCAST_PUB_SLUG_PATTERN="${vars.podcast_slug_pattern}"
977
+ PODCAST_PUB_SOCIAL_IMAGE="${vars.podcast_social_image}"
978
+ PODCAST_PUB_ROUND_DURATION="${vars.podcast_round_duration}"
979
+ kk stage daily-show-notes "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.title}"
980
+
981
+ - id: publish-podcast
982
+ description: "Publish the episode through podcast-pub with the rendered show notes (metadata.showNotes)"
983
+ dependsOn: [show-notes]
984
+ condition:
985
+ kind: shell
986
+ options:
987
+ command: 'test "${vars.publish_enabled}" = "true"'
961
988
  action:
962
989
  kind: shell
963
990
  options:
964
991
  command: >-
965
- 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"
992
+ PODCAST_PUB_SEO="${vars.podcast_seo}"
993
+ PODCAST_PUB_EPISODE_TITLE_MODE="${vars.podcast_episode_title_mode}"
994
+ PODCAST_PUB_EPISODE_TITLE_PATTERN="${vars.podcast_episode_title_pattern}"
995
+ PODCAST_PUB_SERIES_NAME="${vars.podcast_series_name}"
996
+ PODCAST_PUB_SHOW_NAME="${vars.podcast_show_name}"
997
+ PODCAST_PUB_TITLE_MAX="${vars.podcast_title_max}"
998
+ PODCAST_PUB_TITLE_MAX_KEYWORDS="${vars.podcast_title_max_keywords}"
999
+ PODCAST_PUB_SEO_TITLE_SUFFIX="${vars.podcast_seo_title_suffix}"
1000
+ PODCAST_PUB_SEO_TITLE_MAX="${vars.podcast_seo_title_max}"
1001
+ PODCAST_PUB_SEO_DESCRIPTION_MIN="${vars.podcast_seo_description_min}"
1002
+ PODCAST_PUB_SEO_DESCRIPTION_MAX="${vars.podcast_seo_description_max}"
1003
+ PODCAST_PUB_SEO_DESCRIPTION_SOURCE="${vars.podcast_seo_description_source}"
1004
+ PODCAST_PUB_SUMMARY_PARAGRAPH="${vars.podcast_summary_paragraph}"
1005
+ PODCAST_PUB_TIMELINE_INLINE_STYLE="${vars.podcast_timeline_inline_style}"
1006
+ PODCAST_PUB_AUTO_EPISODE_NUMBER="${vars.podcast_auto_episode_number}"
1007
+ PODCAST_PUB_TITLE_SANITIZE="${vars.podcast_title_sanitize}"
1008
+ PODCAST_PUB_LINK_FIELD="${vars.podcast_link_field}"
1009
+ PODCAST_PUB_SLUG="${vars.podcast_slug}"
1010
+ PODCAST_PUB_SLUG_PATTERN="${vars.podcast_slug_pattern}"
1011
+ PODCAST_PUB_SOCIAL_IMAGE="${vars.podcast_social_image}"
1012
+ PODCAST_PUB_ROUND_DURATION="${vars.podcast_round_duration}"
1013
+ kk stage daily-publish-podcast "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
966
1014
 
967
1015
  # ---- fan-in: classify the publish outcome, then close the run ----
968
1016
  - id: publish-classify
969
1017
  description: >-
970
- Fan-in of every publish leaf (surfdash, podcast, xhs, wechat, locales) with policy `all`:
1018
+ Fan-in of every publish leaf (surfdash, podcast, xhs, wechat, locale validation) with policy `all`:
971
1019
  classifies the sequenced surfdash → podcast outcomes into publish-status.txt full|partial|none
972
- (exit 0 in all three), records the independent channels' completeness, and folds in the
1020
+ (exit 0 in all three), records side-channel/locale completeness, and folds in the
973
1021
  surfdash post-patching + the partial-publish marker that voice1 ran as separate states (a
974
1022
  classify is the only node that knows the status). Non-publishing runs skip the whole subtree.
975
- dependsOn: [publish-podcast, publish-surfdash, xhs-publish, wechat-publish, translate-sync]
1023
+ dependsOn: [publish-podcast, publish-surfdash, xhs-publish, wechat-publish, translate-validate]
976
1024
  dependencyPolicy: all
977
1025
  condition:
978
1026
  kind: shell
@@ -997,7 +1045,8 @@ nodes:
997
1045
  kind: shell
998
1046
  options:
999
1047
  command: >-
1000
- kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
1048
+ test "$(cat "${vars.work_dir}/31-publish/${vars.run_date}_17_publish_publish-status.txt" 2>/dev/null)" = "full"
1049
+ && kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
1001
1050
  && test "${vars.publish_enabled}" = "true"
1002
1051
  action:
1003
1052
  kind: shell