@gobing-ai/knowledge-kit 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +128 -25
  3. package/package.json +1 -1
  4. package/plugins/kk/plugin.json +1 -1
  5. package/plugins/kk/scripts/daily-scheduled-run.ts +298 -0
  6. package/plugins/kk/scripts/daily-stages.ts +1057 -108
  7. package/plugins/kk/scripts/kk-workflow-stages.ts +76 -10
  8. package/plugins/kk/scripts/safety-artifact-check.ts +235 -0
  9. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +168 -121
  10. package/plugins/kk/workflows/kk-daily-safety.yaml +129 -50
  11. package/plugins/publishings/bilibili-pub/dist/index.js +2 -1
  12. package/plugins/publishings/bilibili-pub/dist/profile-check.js +2 -1
  13. package/plugins/publishings/cnblogs-pub/dist/index.js +2 -1
  14. package/plugins/publishings/cnblogs-pub/dist/profile-check.js +2 -1
  15. package/plugins/publishings/csdn-pub/dist/index.js +2 -1
  16. package/plugins/publishings/csdn-pub/dist/profile-check.js +2 -1
  17. package/plugins/publishings/infoq-pub/dist/index.js +2 -1
  18. package/plugins/publishings/infoq-pub/dist/profile-check.js +2 -1
  19. package/plugins/publishings/jike-pub/dist/index.js +2 -1
  20. package/plugins/publishings/jike-pub/dist/profile-check.js +2 -1
  21. package/plugins/publishings/juejin-pub/dist/index.js +2 -1
  22. package/plugins/publishings/juejin-pub/dist/profile-check.js +2 -1
  23. package/plugins/publishings/podcast-pub/dist/index.js +46 -8
  24. package/plugins/publishings/podcast-pub/src/index.ts +62 -5
  25. package/plugins/publishings/shipinhao-pub/dist/index.js +2 -1
  26. package/plugins/publishings/shipinhao-pub/dist/profile-check.js +2 -1
  27. package/plugins/publishings/sspai-pub/dist/index.js +2 -1
  28. package/plugins/publishings/sspai-pub/dist/profile-check.js +2 -1
  29. package/plugins/publishings/substack-pub/dist/index.js +2 -1
  30. package/plugins/publishings/substack-pub/dist/profile-check.js +2 -1
  31. package/plugins/publishings/surfdash-pub/dist/index.js +460 -52
  32. package/plugins/publishings/surfdash-pub/src/digest-check.ts +85 -19
  33. package/plugins/publishings/surfdash-pub/src/index.ts +589 -64
  34. package/plugins/publishings/wechatmp-pub/dist/index.js +933 -98
  35. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +60 -3
  36. package/plugins/publishings/wechatmp-pub/src/flow.ts +1125 -85
  37. package/plugins/publishings/wechatmp-pub/src/index.ts +112 -30
  38. package/plugins/publishings/wechatmp-pub/src/page.ts +105 -0
  39. package/plugins/publishings/weibo-pub/dist/index.js +2 -1
  40. package/plugins/publishings/weibo-pub/dist/profile-check.js +2 -1
  41. package/plugins/publishings/x-pub/dist/index.js +2 -1
  42. package/plugins/publishings/x-pub/dist/profile-check.js +2 -1
  43. package/plugins/publishings/xhs-pub/dist/index.js +2 -1
  44. package/plugins/publishings/xhs-pub/dist/profile-check.js +2 -1
  45. package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +2 -1
  46. 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.
@@ -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
@@ -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:
@@ -901,14 +831,14 @@ nodes:
901
831
  description: >-
902
832
  Agent translates both scaffolded locale drafts in place. The two draft paths are read from the
903
833
  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.
834
+ last-writer-wins, design §6), and an empty marker means that locale is absent. The local
835
+ validation node gates final Surfdash publish so its strict all-locale CMS policy is met.
906
836
  dependsOn: [translate-localize]
907
837
  action:
908
838
  kind: agent.run
909
839
  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
840
+ # 20261012 hardening: this is the LAST agent step and a translation failure must not abort
841
+ # the overall report/podcast close, yet the step carried no tolerance and no retry sibling — a stochastic
912
842
  # executor failure here aborted the DAG *after* surfdash and podcast had published, so
913
843
  # publish-classify, mark-published, run-report and ledger never ran for an otherwise complete
914
844
  # day. Tolerating the failure is safe because a scaffold is an unpublished draft: the prompt
@@ -918,26 +848,24 @@ nodes:
918
848
  onError: continue
919
849
  role: scribe
920
850
  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.
851
+ timeoutMs: 600000
852
+ # 2026-10-24: `onError: continue` keeps a failed primary eligible for the capable retry
853
+ # sibling below. The local validation node refuses final publish while either locale remains
854
+ # an untranslated scaffold.
926
855
  input: &translate-input |
927
856
  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
857
  English: ${vars.work_dir}/31-publish/${vars.run_date}_20_translate-localize_en-post-path.txt
929
858
  Japanese: ${vars.work_dir}/31-publish/${vars.run_date}_20_translate-localize_ja-post-path.txt
930
859
  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.
931
860
  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.
861
+ 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
862
  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
863
  Do not wait for operator confirmation. Each edited file must end fully in its target language.
935
864
 
936
865
  # 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.
866
+ # failed executor left the en/ja drafts untranslated and the old sync stage could not distinguish
867
+ # a scaffold from a publishable locale. The primary tolerates, this sibling is loud-tolerant and
868
+ # runs only when a scaffolded draft is still untranslated; translate-validate is the final gate.
941
869
  - id: translate-retry
942
870
  description: "Retry the en/ja locale translation on the capable tier when the primary left a scaffolded draft untranslated (2026-10-24)"
943
871
  dependsOn: [translate]
@@ -953,26 +881,144 @@ nodes:
953
881
  onError: continue
954
882
  role: reviewer
955
883
  agent: ${vars.agent_retry}
884
+ timeoutMs: 600000
956
885
  input: *translate-input
957
886
 
958
- - id: translate-sync
959
- description: "Sync the translated en/ja drafts into the surfdash CMS (the zh post path rides the marker file)"
887
+ - id: translate-refine
888
+ 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
889
  dependsOn: [translate, translate-retry]
890
+ condition:
891
+ kind: shell
892
+ options:
893
+ command: 'test "${vars.publish_enabled}" = "true"'
961
894
  action:
962
895
  kind: shell
896
+ onError: continue
963
897
  options:
964
898
  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"
899
+ kk stage daily-surfdash-refine "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
900
+
901
+ - id: translate-validate
902
+ description: "Validate and fingerprint the refined translated locale files locally before the final Surfdash publish; no CMS side effect"
903
+ dependsOn: [translate-refine]
904
+ condition:
905
+ kind: shell
906
+ options:
907
+ command: 'test "${vars.publish_enabled}" = "true"'
908
+ action:
909
+ kind: shell
910
+ onError: continue
911
+ options:
912
+ command: >-
913
+ 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"
914
+
915
+ - id: publish-surfdash
916
+ 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"
917
+ dependsOn: [publish-prep, translate-validate]
918
+ condition:
919
+ kind: shell
920
+ options:
921
+ command: 'test "${vars.publish_enabled}" = "true"'
922
+ action:
923
+ kind: shell
924
+ options:
925
+ command: >-
926
+ kk stage daily-publish-surfdash "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "true"
927
+
928
+ - id: wechat-publish
929
+ 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"
930
+ dependsOn: [publish-prep, publish-surfdash]
931
+ dependencyPolicy: all
932
+ condition:
933
+ kind: shell
934
+ options:
935
+ command: 'test "${vars.publish_enabled}" = "true" && test "${vars.wechat_enabled}" = "true"'
936
+ action:
937
+ kind: shell
938
+ options:
939
+ command: >-
940
+ kk stage daily-publish-wechat "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.wechat_live}" "${vars.wechat_collection}"
941
+
942
+ - id: show-notes
943
+ 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)"
944
+ dependsOn: [quality, publish-surfdash]
945
+ # Pool-gated (0266): its only skip path was dependency cascade, which a done+skipped mix
946
+ # defeats — a duration-refused run would still render notes for an unpublished episode.
947
+ condition:
948
+ kind: shell
949
+ options:
950
+ command: >-
951
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
952
+ action:
953
+ kind: shell
954
+ options:
955
+ command: >-
956
+ PODCAST_PUB_SEO="${vars.podcast_seo}"
957
+ PODCAST_PUB_EPISODE_TITLE_MODE="${vars.podcast_episode_title_mode}"
958
+ PODCAST_PUB_EPISODE_TITLE_PATTERN="${vars.podcast_episode_title_pattern}"
959
+ PODCAST_PUB_SERIES_NAME="${vars.podcast_series_name}"
960
+ PODCAST_PUB_SHOW_NAME="${vars.podcast_show_name}"
961
+ PODCAST_PUB_TITLE_MAX="${vars.podcast_title_max}"
962
+ PODCAST_PUB_TITLE_MAX_KEYWORDS="${vars.podcast_title_max_keywords}"
963
+ PODCAST_PUB_SEO_TITLE_SUFFIX="${vars.podcast_seo_title_suffix}"
964
+ PODCAST_PUB_SEO_TITLE_MAX="${vars.podcast_seo_title_max}"
965
+ PODCAST_PUB_SEO_DESCRIPTION_MIN="${vars.podcast_seo_description_min}"
966
+ PODCAST_PUB_SEO_DESCRIPTION_MAX="${vars.podcast_seo_description_max}"
967
+ PODCAST_PUB_SEO_DESCRIPTION_SOURCE="${vars.podcast_seo_description_source}"
968
+ PODCAST_PUB_SUMMARY_PARAGRAPH="${vars.podcast_summary_paragraph}"
969
+ PODCAST_PUB_TIMELINE_INLINE_STYLE="${vars.podcast_timeline_inline_style}"
970
+ PODCAST_PUB_AUTO_EPISODE_NUMBER="${vars.podcast_auto_episode_number}"
971
+ PODCAST_PUB_TITLE_SANITIZE="${vars.podcast_title_sanitize}"
972
+ PODCAST_PUB_LINK_FIELD="${vars.podcast_link_field}"
973
+ PODCAST_PUB_SLUG="${vars.podcast_slug}"
974
+ PODCAST_PUB_SLUG_PATTERN="${vars.podcast_slug_pattern}"
975
+ PODCAST_PUB_SOCIAL_IMAGE="${vars.podcast_social_image}"
976
+ PODCAST_PUB_ROUND_DURATION="${vars.podcast_round_duration}"
977
+ kk stage daily-show-notes "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.title}"
978
+
979
+ - id: publish-podcast
980
+ description: "Publish the episode through podcast-pub with the rendered show notes (metadata.showNotes)"
981
+ dependsOn: [show-notes]
982
+ condition:
983
+ kind: shell
984
+ options:
985
+ command: 'test "${vars.publish_enabled}" = "true"'
986
+ action:
987
+ kind: shell
988
+ options:
989
+ command: >-
990
+ PODCAST_PUB_SEO="${vars.podcast_seo}"
991
+ PODCAST_PUB_EPISODE_TITLE_MODE="${vars.podcast_episode_title_mode}"
992
+ PODCAST_PUB_EPISODE_TITLE_PATTERN="${vars.podcast_episode_title_pattern}"
993
+ PODCAST_PUB_SERIES_NAME="${vars.podcast_series_name}"
994
+ PODCAST_PUB_SHOW_NAME="${vars.podcast_show_name}"
995
+ PODCAST_PUB_TITLE_MAX="${vars.podcast_title_max}"
996
+ PODCAST_PUB_TITLE_MAX_KEYWORDS="${vars.podcast_title_max_keywords}"
997
+ PODCAST_PUB_SEO_TITLE_SUFFIX="${vars.podcast_seo_title_suffix}"
998
+ PODCAST_PUB_SEO_TITLE_MAX="${vars.podcast_seo_title_max}"
999
+ PODCAST_PUB_SEO_DESCRIPTION_MIN="${vars.podcast_seo_description_min}"
1000
+ PODCAST_PUB_SEO_DESCRIPTION_MAX="${vars.podcast_seo_description_max}"
1001
+ PODCAST_PUB_SEO_DESCRIPTION_SOURCE="${vars.podcast_seo_description_source}"
1002
+ PODCAST_PUB_SUMMARY_PARAGRAPH="${vars.podcast_summary_paragraph}"
1003
+ PODCAST_PUB_TIMELINE_INLINE_STYLE="${vars.podcast_timeline_inline_style}"
1004
+ PODCAST_PUB_AUTO_EPISODE_NUMBER="${vars.podcast_auto_episode_number}"
1005
+ PODCAST_PUB_TITLE_SANITIZE="${vars.podcast_title_sanitize}"
1006
+ PODCAST_PUB_LINK_FIELD="${vars.podcast_link_field}"
1007
+ PODCAST_PUB_SLUG="${vars.podcast_slug}"
1008
+ PODCAST_PUB_SLUG_PATTERN="${vars.podcast_slug_pattern}"
1009
+ PODCAST_PUB_SOCIAL_IMAGE="${vars.podcast_social_image}"
1010
+ PODCAST_PUB_ROUND_DURATION="${vars.podcast_round_duration}"
1011
+ kk stage daily-publish-podcast "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
966
1012
 
967
1013
  # ---- fan-in: classify the publish outcome, then close the run ----
968
1014
  - id: publish-classify
969
1015
  description: >-
970
- Fan-in of every publish leaf (surfdash, podcast, xhs, wechat, locales) with policy `all`:
1016
+ Fan-in of every publish leaf (surfdash, podcast, xhs, wechat, locale validation) with policy `all`:
971
1017
  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
1018
+ (exit 0 in all three), records side-channel/locale completeness, and folds in the
973
1019
  surfdash post-patching + the partial-publish marker that voice1 ran as separate states (a
974
1020
  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]
1021
+ dependsOn: [publish-podcast, publish-surfdash, xhs-publish, wechat-publish, translate-validate]
976
1022
  dependencyPolicy: all
977
1023
  condition:
978
1024
  kind: shell
@@ -997,7 +1043,8 @@ nodes:
997
1043
  kind: shell
998
1044
  options:
999
1045
  command: >-
1000
- kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
1046
+ test "$(cat "${vars.work_dir}/31-publish/${vars.run_date}_17_publish_publish-status.txt" 2>/dev/null)" = "full"
1047
+ && kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
1001
1048
  && test "${vars.publish_enabled}" = "true"
1002
1049
  action:
1003
1050
  kind: shell