@gobing-ai/knowledge-kit 0.1.9 → 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 (72) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +132 -25
  3. package/package.json +1 -1
  4. package/plugins/generations/content-gen/dist/index.js +4 -0
  5. package/plugins/generations/core-facts-gen/dist/index.js +4 -0
  6. package/plugins/generations/daily-article-gen/dist/index.js +4 -0
  7. package/plugins/generations/dailynews-gen/dist/index.js +4 -0
  8. package/plugins/generations/episode-plan-gen/dist/index.js +102 -1
  9. package/plugins/generations/episode-plan-gen/src/index.ts +2 -4
  10. package/plugins/generations/gemini-tts/dist/index.js +4 -0
  11. package/plugins/generations/image-gen/dist/index.js +4 -0
  12. package/plugins/generations/news-report-gen/dist/index.js +4 -0
  13. package/plugins/generations/voice-gen/dist/index.js +4 -0
  14. package/plugins/generations/voxcpm-tts/dist/index.js +4 -0
  15. package/plugins/ingestions/aihot-ingest/dist/index.js +4 -0
  16. package/plugins/ingestions/horizon-ingest/dist/index.js +4 -0
  17. package/plugins/ingestions/last30days-ingest/dist/index.js +11 -3
  18. package/plugins/ingestions/last30days-ingest/src/index.ts +14 -3
  19. package/plugins/ingestions/web-search/dist/index.js +4 -0
  20. package/plugins/kk/plugin.json +1 -1
  21. package/plugins/kk/scripts/daily-scheduled-run.ts +298 -0
  22. package/plugins/kk/scripts/daily-stages.ts +1083 -49
  23. package/plugins/kk/scripts/kk-workflow-stages.ts +76 -10
  24. package/plugins/kk/scripts/safety-artifact-check.ts +235 -0
  25. package/plugins/kk/scripts/sentences.ts +134 -0
  26. package/plugins/kk/scripts/series-names.ts +124 -0
  27. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +194 -113
  28. package/plugins/kk/workflows/kk-daily-safety.yaml +160 -36
  29. package/plugins/publishings/bilibili-pub/dist/index.js +6 -1
  30. package/plugins/publishings/bilibili-pub/dist/profile-check.js +2 -1
  31. package/plugins/publishings/cnblogs-pub/dist/index.js +6 -1
  32. package/plugins/publishings/cnblogs-pub/dist/profile-check.js +2 -1
  33. package/plugins/publishings/csdn-pub/dist/index.js +6 -1
  34. package/plugins/publishings/csdn-pub/dist/profile-check.js +6 -1
  35. package/plugins/publishings/emdash-pub/dist/index.js +4 -0
  36. package/plugins/publishings/infoq-pub/dist/index.js +6 -1
  37. package/plugins/publishings/infoq-pub/dist/profile-check.js +6 -1
  38. package/plugins/publishings/jike-pub/dist/index.js +6 -1
  39. package/plugins/publishings/jike-pub/dist/profile-check.js +6 -1
  40. package/plugins/publishings/juejin-pub/dist/index.js +6 -1
  41. package/plugins/publishings/juejin-pub/dist/profile-check.js +6 -1
  42. package/plugins/publishings/medium-pub/dist/index.js +4 -0
  43. package/plugins/publishings/podcast-pub/dist/index.js +151 -16
  44. package/plugins/publishings/podcast-pub/src/index.ts +62 -5
  45. package/plugins/publishings/podcast-pub/src/seo.ts +2 -4
  46. package/plugins/publishings/podcast-pub/src/show-notes.ts +3 -8
  47. package/plugins/publishings/qiita-pub/dist/index.js +4 -0
  48. package/plugins/publishings/shipinhao-pub/dist/index.js +6 -1
  49. package/plugins/publishings/shipinhao-pub/dist/profile-check.js +2 -1
  50. package/plugins/publishings/sspai-pub/dist/index.js +6 -1
  51. package/plugins/publishings/sspai-pub/dist/profile-check.js +2 -1
  52. package/plugins/publishings/substack-pub/dist/index.js +6 -1
  53. package/plugins/publishings/substack-pub/dist/profile-check.js +6 -1
  54. package/plugins/publishings/surfdash-pub/dist/index.js +691 -63
  55. package/plugins/publishings/surfdash-pub/src/digest-check.ts +284 -0
  56. package/plugins/publishings/surfdash-pub/src/index.ts +656 -84
  57. package/plugins/publishings/surfdash-pub/src/patch.ts +44 -7
  58. package/plugins/publishings/threads-pub/dist/index.js +4 -0
  59. package/plugins/publishings/wechatmp-pub/dist/index.js +937 -98
  60. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +64 -3
  61. package/plugins/publishings/wechatmp-pub/src/flow.ts +1125 -85
  62. package/plugins/publishings/wechatmp-pub/src/index.ts +112 -30
  63. package/plugins/publishings/wechatmp-pub/src/page.ts +105 -0
  64. package/plugins/publishings/weibo-pub/dist/index.js +6 -1
  65. package/plugins/publishings/weibo-pub/dist/profile-check.js +6 -1
  66. package/plugins/publishings/x-pub/dist/index.js +6 -1
  67. package/plugins/publishings/x-pub/dist/profile-check.js +6 -1
  68. package/plugins/publishings/xhs-pub/dist/index.js +6 -1
  69. package/plugins/publishings/xhs-pub/dist/profile-check.js +6 -1
  70. package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +6 -1
  71. package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +2 -1
  72. package/plugins/publishings/zenn-pub/dist/index.js +4 -0
@@ -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
@@ -80,7 +82,12 @@ vars:
80
82
  # siblings declare `role: reviewer` (capable-1), two rungs above the primary's `scribe`, so the
81
83
  # default `inline` resolves the retry above any rung the primary reaches by climbing past an
82
84
  # unusable cheap executor (ac5-live-1). The operator may still pin a known-good executor here.
83
- agent_retry: "inline"
85
+ # 2026-10-24 pin: `inline` resolved to agy-gemini, whose headless write path is dead (terminal
86
+ # sandbox denies workspace writes), so the retry failed the same way twice — the whole day closed
87
+ # with no plan. `claude` carries `--permission-mode acceptEdits --allowedTools Write Edit` and is
88
+ # the tier that actually delivered both producers on this date. Re-point it at whatever
89
+ # `spur agent doctor <name>` reports usable; an empty retry tier fails the run loudly.
90
+ agent_retry: "claude"
84
91
  # QC pins — lockstep with episode-plan-gen's DEFAULT_FILTER_CONFIG (env wins), same as voice1.
85
92
  qc_categories: "ai-news,ai-technical,ai-research,ai-industry"
86
93
  qc_min_quality: "2"
@@ -161,7 +168,7 @@ nodes:
161
168
  command: >-
162
169
  kk stage daily-preflight plugins/kk/workflows/kk-daily-ai-voice2.yaml
163
170
  plugins/kk/workflows/kk-daily-safety.yaml
164
- && 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
165
172
  --run "${vars.__runId}" --definition-digest "${vars.__definitionDigest}"
166
173
  --setting window="${vars.window}" --setting language="${vars.language}" --setting vdriver="${vars.vdriver}"
167
174
  --setting skip_review="${vars.skip_review}" --setting transcode_mp3="${vars.transcode_mp3}"
@@ -317,6 +324,7 @@ nodes:
317
324
  options:
318
325
  role: scribe
319
326
  agent: ${vars.agent}
327
+ timeoutMs: 600000
320
328
  # ADR-115 error tier: keep this prompt <= 1000 chars (0133 R3; 0138 R4a copy discipline).
321
329
  # The anchor is shared with plan-translate-retry so the two attempts cannot drift apart.
322
330
  # 20261015 forensics: headless agy's terminal sandbox denies workspace writes for EVERY
@@ -354,6 +362,7 @@ nodes:
354
362
  options:
355
363
  role: reviewer
356
364
  agent: ${vars.agent_retry}
365
+ timeoutMs: 600000
357
366
  input: *plan-translate-input
358
367
  expectFile: ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json
359
368
  allowAppend: false
@@ -375,6 +384,7 @@ nodes:
375
384
  options:
376
385
  role: scribe
377
386
  agent: ${vars.agent}
387
+ timeoutMs: 600000
378
388
  # ADR-115 error tier: keep this prompt <= 1000 chars. No title line: the canonical episode
379
389
  # title is computed by daily-article-gen (0189 R4).
380
390
  # The anchor is shared with article-write-retry so the two attempts cannot drift apart.
@@ -406,6 +416,7 @@ nodes:
406
416
  options:
407
417
  role: reviewer
408
418
  agent: ${vars.agent_retry}
419
+ timeoutMs: 600000
409
420
  input: *article-write-input
410
421
  expectFile: ${vars.work_dir}/22-article/${vars.run_date}_06_article-write_body.md
411
422
  allowAppend: false
@@ -450,6 +461,7 @@ nodes:
450
461
  options:
451
462
  role: scribe
452
463
  agent: ${vars.agent}
464
+ timeoutMs: 600000
453
465
  # ADR-115 error tier: keep this prompt <= 1000 chars.
454
466
  input: |
455
467
  Read ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json — a JSON array of plan docs; don't edit it.
@@ -512,6 +524,7 @@ nodes:
512
524
  options:
513
525
  role: scribe
514
526
  agent: ${vars.agent}
527
+ timeoutMs: 600000
515
528
  # ADR-115 error tier: keep this prompt <= 1000 chars.
516
529
  input: |
517
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.
@@ -547,6 +560,8 @@ nodes:
547
560
  are stripped at the nested-spur boundary (the same two `NESTED_SPUR_UNSET` vars the stage
548
561
  runner strips, kk-workflow-stages.ts): without it `spur workflow run` refuses to start inside
549
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.
550
565
  dependsOn: [script]
551
566
  condition:
552
567
  kind: shell
@@ -558,7 +573,8 @@ nodes:
558
573
  command: >-
559
574
  env -u SPUR_WORKFLOW_RUN_ACTIVE -u SPUR_EXPECTED_DEFINITION_DIGEST
560
575
  spur workflow run plugins/kk/workflows/kk-daily-safety.yaml
561
- --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}\"}"
562
578
  --quiet --no-plan
563
579
 
564
580
  - id: review
@@ -705,21 +721,10 @@ nodes:
705
721
  command: >-
706
722
  kk stage daily-publish-prep "${vars.work_dir}" "${vars.run_date}" "${vars.podcast_artist}" "${vars.podcast_album}"
707
723
 
708
- # ---- audio QC: fans in the audio branch and the publish seam ----
709
- - id: quality
710
- description: >-
711
- Verify audio quality, speech rate, and silence/repetition QC metrics. Depends on the audio
712
- tail, wrap-docs and publish-prep (the seam's merge is part of the audited bytes); publish-prep
713
- being skipped on a non-publishing run does not skip the audit.
714
- dependsOn: [duration, wrap-docs, publish-prep]
715
- action:
716
- kind: shell
717
- options:
718
- command: >-
719
- kk stage daily-quality-report "${vars.work_dir}" "${vars.run_date}"
720
-
721
- - id: publish-surfdash
722
- 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"
723
728
  dependsOn: [publish-prep]
724
729
  condition:
725
730
  kind: shell
@@ -729,80 +734,22 @@ nodes:
729
734
  kind: shell
730
735
  options:
731
736
  command: >-
732
- kk stage daily-publish-surfdash "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
733
-
734
- - id: show-notes
735
- 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)"
736
- dependsOn: [quality, publish-surfdash]
737
- # Pool-gated (0266): its only skip path was dependency cascade, which a done+skipped mix
738
- # defeats — a duration-refused run would still render notes for an unpublished episode.
739
- condition:
740
- kind: shell
741
- options:
742
- command: >-
743
- kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
744
- action:
745
- kind: shell
746
- options:
747
- command: >-
748
- PODCAST_PUB_SEO="${vars.podcast_seo}"
749
- PODCAST_PUB_EPISODE_TITLE_MODE="${vars.podcast_episode_title_mode}"
750
- PODCAST_PUB_EPISODE_TITLE_PATTERN="${vars.podcast_episode_title_pattern}"
751
- PODCAST_PUB_SERIES_NAME="${vars.podcast_series_name}"
752
- PODCAST_PUB_SHOW_NAME="${vars.podcast_show_name}"
753
- PODCAST_PUB_TITLE_MAX="${vars.podcast_title_max}"
754
- PODCAST_PUB_TITLE_MAX_KEYWORDS="${vars.podcast_title_max_keywords}"
755
- PODCAST_PUB_SEO_TITLE_SUFFIX="${vars.podcast_seo_title_suffix}"
756
- PODCAST_PUB_SEO_TITLE_MAX="${vars.podcast_seo_title_max}"
757
- PODCAST_PUB_SEO_DESCRIPTION_MIN="${vars.podcast_seo_description_min}"
758
- PODCAST_PUB_SEO_DESCRIPTION_MAX="${vars.podcast_seo_description_max}"
759
- PODCAST_PUB_SEO_DESCRIPTION_SOURCE="${vars.podcast_seo_description_source}"
760
- PODCAST_PUB_SUMMARY_PARAGRAPH="${vars.podcast_summary_paragraph}"
761
- PODCAST_PUB_TIMELINE_INLINE_STYLE="${vars.podcast_timeline_inline_style}"
762
- PODCAST_PUB_AUTO_EPISODE_NUMBER="${vars.podcast_auto_episode_number}"
763
- PODCAST_PUB_TITLE_SANITIZE="${vars.podcast_title_sanitize}"
764
- PODCAST_PUB_LINK_FIELD="${vars.podcast_link_field}"
765
- PODCAST_PUB_SLUG="${vars.podcast_slug}"
766
- PODCAST_PUB_SLUG_PATTERN="${vars.podcast_slug_pattern}"
767
- PODCAST_PUB_SOCIAL_IMAGE="${vars.podcast_social_image}"
768
- PODCAST_PUB_ROUND_DURATION="${vars.podcast_round_duration}"
769
- kk stage daily-show-notes "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.title}"
737
+ kk stage daily-prepare-surfdash "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
770
738
 
771
- - id: publish-podcast
772
- description: "Publish the episode through podcast-pub with the rendered show notes (metadata.showNotes)"
773
- dependsOn: [show-notes]
774
- condition:
775
- kind: shell
776
- options:
777
- 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]
778
746
  action:
779
747
  kind: shell
780
748
  options:
781
749
  command: >-
782
- PODCAST_PUB_SEO="${vars.podcast_seo}"
783
- PODCAST_PUB_EPISODE_TITLE_MODE="${vars.podcast_episode_title_mode}"
784
- PODCAST_PUB_EPISODE_TITLE_PATTERN="${vars.podcast_episode_title_pattern}"
785
- PODCAST_PUB_SERIES_NAME="${vars.podcast_series_name}"
786
- PODCAST_PUB_SHOW_NAME="${vars.podcast_show_name}"
787
- PODCAST_PUB_TITLE_MAX="${vars.podcast_title_max}"
788
- PODCAST_PUB_TITLE_MAX_KEYWORDS="${vars.podcast_title_max_keywords}"
789
- PODCAST_PUB_SEO_TITLE_SUFFIX="${vars.podcast_seo_title_suffix}"
790
- PODCAST_PUB_SEO_TITLE_MAX="${vars.podcast_seo_title_max}"
791
- PODCAST_PUB_SEO_DESCRIPTION_MIN="${vars.podcast_seo_description_min}"
792
- PODCAST_PUB_SEO_DESCRIPTION_MAX="${vars.podcast_seo_description_max}"
793
- PODCAST_PUB_SEO_DESCRIPTION_SOURCE="${vars.podcast_seo_description_source}"
794
- PODCAST_PUB_SUMMARY_PARAGRAPH="${vars.podcast_summary_paragraph}"
795
- PODCAST_PUB_TIMELINE_INLINE_STYLE="${vars.podcast_timeline_inline_style}"
796
- PODCAST_PUB_AUTO_EPISODE_NUMBER="${vars.podcast_auto_episode_number}"
797
- PODCAST_PUB_TITLE_SANITIZE="${vars.podcast_title_sanitize}"
798
- PODCAST_PUB_LINK_FIELD="${vars.podcast_link_field}"
799
- PODCAST_PUB_SLUG="${vars.podcast_slug}"
800
- PODCAST_PUB_SLUG_PATTERN="${vars.podcast_slug_pattern}"
801
- PODCAST_PUB_SOCIAL_IMAGE="${vars.podcast_social_image}"
802
- PODCAST_PUB_ROUND_DURATION="${vars.podcast_round_duration}"
803
- 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}"
804
751
 
805
- # ---- 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) ----
806
753
  - id: xhs-write
807
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"
808
755
  dependsOn: [script, review]
@@ -818,6 +765,7 @@ nodes:
818
765
  options:
819
766
  role: scribe
820
767
  agent: ${vars.agent}
768
+ timeoutMs: 600000
821
769
  # ADR-115 error tier: keep this prompt <= 1000 chars.
822
770
  input: |
823
771
  Read ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json — a JSON array of news docs; don't edit it.
@@ -866,22 +814,9 @@ nodes:
866
814
  command: >-
867
815
  kk stage daily-publish-xhs "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.xhs_live}" "true"
868
816
 
869
- - id: wechat-publish
870
- 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)"
871
- dependsOn: [publish-prep]
872
- condition:
873
- kind: shell
874
- options:
875
- command: 'test "${vars.publish_enabled}" = "true" && test "${vars.wechat_enabled}" = "true"'
876
- action:
877
- kind: shell
878
- options:
879
- command: >-
880
- kk stage daily-publish-wechat "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.wechat_live}" "${vars.wechat_collection}"
881
-
882
817
  - id: translate-localize
883
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"
884
- dependsOn: [publish-surfdash]
819
+ dependsOn: [publish-surfdash-prepare]
885
820
  condition:
886
821
  kind: shell
887
822
  options:
@@ -896,14 +831,14 @@ nodes:
896
831
  description: >-
897
832
  Agent translates both scaffolded locale drafts in place. The two draft paths are read from the
898
833
  marker files at their fixed paths (no var projection after the fan-out: concurrent setVars is
899
- last-writer-wins, design §6), and an empty marker means that locale is absent. Non-blocking:
900
- 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.
901
836
  dependsOn: [translate-localize]
902
837
  action:
903
838
  kind: agent.run
904
839
  options:
905
- # 20261012 hardening: this is the LAST agent step and the locales are documented as an
906
- # 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
907
842
  # executor failure here aborted the DAG *after* surfdash and podcast had published, so
908
843
  # publish-classify, mark-published, run-report and ledger never ran for an otherwise complete
909
844
  # day. Tolerating the failure is safe because a scaffold is an unpublished draft: the prompt
@@ -913,32 +848,177 @@ nodes:
913
848
  onError: continue
914
849
  role: scribe
915
850
  agent: ${vars.agent}
916
- input: |
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.
855
+ input: &translate-input |
917
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:
918
857
  English: ${vars.work_dir}/31-publish/${vars.run_date}_20_translate-localize_en-post-path.txt
919
858
  Japanese: ${vars.work_dir}/31-publish/${vars.run_date}_20_translate-localize_ja-post-path.txt
920
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.
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.
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.
921
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.
922
863
  Do not wait for operator confirmation. Each edited file must end fully in its target language.
923
864
 
924
- - id: translate-sync
925
- description: "Sync the translated en/ja drafts into the surfdash CMS (the zh post path rides the marker file)"
865
+ # 2026-10-24: the locale translation carried `onError: continue` with NO retry sibling, so a
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.
869
+ - id: translate-retry
870
+ description: "Retry the en/ja locale translation on the capable tier when the primary left a scaffolded draft untranslated (2026-10-24)"
926
871
  dependsOn: [translate]
872
+ condition:
873
+ kind: shell
874
+ options:
875
+ command: >-
876
+ test "$(cat "${vars.work_dir}/31-publish/${vars.run_date}_20_translate-localize_zh-post-path.txt" 2>/dev/null)" != ""
877
+ && { 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; }
878
+ action:
879
+ kind: agent.run
880
+ options:
881
+ onError: continue
882
+ role: reviewer
883
+ agent: ${vars.agent_retry}
884
+ timeoutMs: 600000
885
+ input: *translate-input
886
+
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"
889
+ dependsOn: [translate, translate-retry]
890
+ condition:
891
+ kind: shell
892
+ options:
893
+ command: 'test "${vars.publish_enabled}" = "true"'
894
+ action:
895
+ kind: shell
896
+ onError: continue
897
+ options:
898
+ command: >-
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"'
927
986
  action:
928
987
  kind: shell
929
988
  options:
930
989
  command: >-
931
- 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"
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}"
932
1012
 
933
1013
  # ---- fan-in: classify the publish outcome, then close the run ----
934
1014
  - id: publish-classify
935
1015
  description: >-
936
- 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`:
937
1017
  classifies the sequenced surfdash → podcast outcomes into publish-status.txt full|partial|none
938
- (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
939
1019
  surfdash post-patching + the partial-publish marker that voice1 ran as separate states (a
940
1020
  classify is the only node that knows the status). Non-publishing runs skip the whole subtree.
941
- dependsOn: [publish-podcast, publish-surfdash, xhs-publish, wechat-publish, translate-sync]
1021
+ dependsOn: [publish-podcast, publish-surfdash, xhs-publish, wechat-publish, translate-validate]
942
1022
  dependencyPolicy: all
943
1023
  condition:
944
1024
  kind: shell
@@ -963,7 +1043,8 @@ nodes:
963
1043
  kind: shell
964
1044
  options:
965
1045
  command: >-
966
- 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
967
1048
  && test "${vars.publish_enabled}" = "true"
968
1049
  action:
969
1050
  kind: shell