@gobing-ai/knowledge-kit 0.0.16 → 0.0.18

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 (77) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +74 -0
  3. package/dist/index.js +46 -100
  4. package/package.json +19 -1
  5. package/plugins/generations/content-gen/dist/index.js +42 -94
  6. package/plugins/generations/core-facts-gen/dist/index.js +51 -100
  7. package/plugins/generations/daily-article-gen/dist/index.js +98 -100
  8. package/plugins/generations/daily-article-gen/src/index.ts +80 -6
  9. package/plugins/generations/dailynews-gen/dist/index.js +144 -130
  10. package/plugins/generations/dailynews-gen/src/script-builder.ts +141 -33
  11. package/plugins/generations/episode-plan-gen/dist/index.js +124 -115
  12. package/plugins/generations/episode-plan-gen/src/index.ts +111 -24
  13. package/plugins/generations/image-gen/dist/index.js +63 -115
  14. package/plugins/generations/news-report-gen/dist/index.js +164 -100
  15. package/plugins/generations/news-report-gen/src/index.ts +199 -7
  16. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/pipeline.py +17 -1
  17. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/qc.py +4 -22
  18. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/voicescript.py +8 -0
  19. package/plugins/generations/voice-gen/dist/index.js +136 -117
  20. package/plugins/generations/voice-gen/src/concat.ts +34 -0
  21. package/plugins/generations/voice-gen/src/index.ts +6 -4
  22. package/plugins/generations/voice-gen/src/voicescript.ts +61 -0
  23. package/plugins/ingestions/aihot-ingest/dist/index.js +47 -97
  24. package/plugins/ingestions/horizon-ingest/dist/index.js +47 -96
  25. package/plugins/ingestions/karakeep-local/dist/index.js +217 -266
  26. package/plugins/ingestions/last30days-ingest/dist/index.js +42 -91
  27. package/plugins/ingestions/web-search/dist/index.js +68 -117
  28. package/plugins/kk/commands/workflow-run.md +25 -2
  29. package/plugins/kk/plugin.json +1 -1
  30. package/plugins/kk/scripts/kk-workflow-stages.ts +522 -163
  31. package/plugins/kk/scripts/validate-voicescript.ts +55 -0
  32. package/plugins/kk/skills/publish/SKILL.md +80 -0
  33. package/plugins/kk/workflows/kk-daily-ai-voice.yaml +125 -120
  34. package/plugins/kk/workflows/leftover-publish-fanout-example.yaml +53 -0
  35. package/plugins/publishings/emdash-pub/dist/index.js +42 -91
  36. package/plugins/publishings/infoq-pub/dist/index.js +21994 -0
  37. package/plugins/publishings/infoq-pub/package.json +17 -0
  38. package/plugins/publishings/infoq-pub/plugin.json +7 -0
  39. package/plugins/publishings/infoq-pub/src/index.ts +123 -0
  40. package/plugins/publishings/infoq-pub/tsconfig.json +4 -0
  41. package/plugins/publishings/juejin-pub/dist/index.js +21994 -0
  42. package/plugins/publishings/juejin-pub/package.json +17 -0
  43. package/plugins/publishings/juejin-pub/plugin.json +7 -0
  44. package/plugins/publishings/juejin-pub/src/index.ts +123 -0
  45. package/plugins/publishings/juejin-pub/tsconfig.json +4 -0
  46. package/plugins/publishings/medium-pub/dist/index.js +22041 -0
  47. package/plugins/publishings/medium-pub/package.json +17 -0
  48. package/plugins/publishings/medium-pub/plugin.json +7 -0
  49. package/plugins/publishings/medium-pub/src/index.ts +215 -0
  50. package/plugins/publishings/medium-pub/tsconfig.json +4 -0
  51. package/plugins/publishings/podcast-pub/dist/index.js +183 -123
  52. package/plugins/publishings/podcast-pub/src/index.ts +47 -3
  53. package/plugins/publishings/podcast-pub/src/show-notes.ts +153 -34
  54. package/plugins/publishings/qiita-pub/dist/index.js +42 -91
  55. package/plugins/publishings/substack-pub/dist/index.js +21994 -0
  56. package/plugins/publishings/substack-pub/package.json +17 -0
  57. package/plugins/publishings/substack-pub/plugin.json +7 -0
  58. package/plugins/publishings/substack-pub/src/index.ts +123 -0
  59. package/plugins/publishings/substack-pub/tsconfig.json +4 -0
  60. package/plugins/publishings/surfdash-pub/dist/index.js +172 -103
  61. package/plugins/publishings/surfdash-pub/src/index.ts +32 -2
  62. package/plugins/publishings/wechatmp-pub/dist/index.js +22119 -0
  63. package/plugins/publishings/wechatmp-pub/package.json +17 -0
  64. package/plugins/publishings/wechatmp-pub/plugin.json +7 -0
  65. package/plugins/publishings/wechatmp-pub/src/index.ts +304 -0
  66. package/plugins/publishings/wechatmp-pub/tsconfig.json +4 -0
  67. package/plugins/publishings/x-pub/dist/index.js +22110 -0
  68. package/plugins/publishings/x-pub/package.json +17 -0
  69. package/plugins/publishings/x-pub/plugin.json +7 -0
  70. package/plugins/publishings/x-pub/src/index.ts +222 -0
  71. package/plugins/publishings/x-pub/tsconfig.json +4 -0
  72. package/plugins/publishings/xhs-pub/dist/index.js +21994 -0
  73. package/plugins/publishings/xhs-pub/package.json +17 -0
  74. package/plugins/publishings/xhs-pub/plugin.json +7 -0
  75. package/plugins/publishings/xhs-pub/src/index.ts +123 -0
  76. package/plugins/publishings/xhs-pub/tsconfig.json +4 -0
  77. package/plugins/publishings/zenn-pub/dist/index.js +42 -91
@@ -118,6 +118,59 @@ function assertCrossfade(value: number | undefined): void {
118
118
  }
119
119
  }
120
120
 
121
+ // ─── Content checks (task 0139 R3) ───────────────────────────────────────────
122
+ // Deterministic spoken-copy gates. The workflow wires this validator at the end of the script
123
+ // stage's fresh write and again in wrap-docs (which covers a HITL-edited YAML and the script cache
124
+ // path), so a voicescript that reaches audio carries no title echo, no duplicated segment, no
125
+ // research scaffolding and no colon-mangled URL. Every failure names the segment index and reason.
126
+
127
+ /** Punctuation/space the translator drifts between a title and its body restatement. */
128
+ const CONTENT_ECHO_NOISE_RE = /[,。、::;;!!??…\s「」『』【】]/g;
129
+ /** Research scaffold labels the plan-translate stage must not emit as spoken copy. */
130
+ const CONTENT_SCAFFOLD_LABEL_RE =
131
+ /^(?:\*\*)?[「『]?(?:背景|潜在影响|可做角度|为什么现在值得注意|社区讨论)[」』]?(?:\*\*)?(?:[::,,]|\s|$)/;
132
+ /** `标签,` metadata tails — the plan item's tag line spoken aloud. */
133
+ const CONTENT_TAG_TAIL_RE = /标签[::,,]/;
134
+ /** `wss,//` / `https。//` — the translator replacing a URL colon with a Chinese comma. */
135
+ const CONTENT_URL_MANGLE_RE = /[,。]\/\//;
136
+
137
+ function assertSegmentContent(script: VoiceScript): void {
138
+ const seen = new Map<string, number>();
139
+ for (let idx = 0; idx < script.segments.length; idx++) {
140
+ const text = (script.segments[idx]?.text ?? '').trim();
141
+ if (text === '') continue;
142
+
143
+ const previous = seen.get(text);
144
+ if (previous !== undefined) {
145
+ throw new Error(`content check failed: segment ${idx} duplicates segment ${previous} verbatim`);
146
+ }
147
+ seen.set(text, idx);
148
+
149
+ // Title echo: the 【lead】 content reappearing immediately after the lead, punctuation-drift
150
+ // tolerant — the same comparison `stripLeadingTitleEcho` uses on the producer side.
151
+ const lead = text.match(/^[^【]*【([^】]{4,80})】[,。、\s]*/);
152
+ if (lead !== null) {
153
+ const bracketed = (lead[1] ?? '').replace(CONTENT_ECHO_NOISE_RE, '');
154
+ const rest = text.slice(lead[0].length).replace(CONTENT_ECHO_NOISE_RE, '');
155
+ if (bracketed.length >= 4 && rest.startsWith(bracketed)) {
156
+ throw new Error(
157
+ `content check failed: segment ${idx} repeats its 【${lead[1]}】 title in the body text`,
158
+ );
159
+ }
160
+ }
161
+
162
+ if (CONTENT_SCAFFOLD_LABEL_RE.test(text)) {
163
+ throw new Error(`content check failed: segment ${idx} opens with a research scaffold label`);
164
+ }
165
+ if (CONTENT_TAG_TAIL_RE.test(text)) {
166
+ throw new Error(`content check failed: segment ${idx} carries a 标签 metadata tail`);
167
+ }
168
+ if (CONTENT_URL_MANGLE_RE.test(text)) {
169
+ throw new Error(`content check failed: segment ${idx} has a colon-mangled URL`);
170
+ }
171
+ }
172
+ }
173
+
121
174
  function validateVoiceScript(script: VoiceScript): void {
122
175
  if (!script || typeof script !== 'object') {
123
176
  throw new Error('VoiceScript must be an object');
@@ -173,6 +226,8 @@ function validateVoiceScript(script: VoiceScript): void {
173
226
  assertEngine(segment.engine, `Segment at index ${idx}`);
174
227
  assertLanguage(segment.language, `Segment at index ${idx}`);
175
228
  }
229
+
230
+ assertSegmentContent(script);
176
231
  }
177
232
 
178
233
  function main(): void {
@@ -0,0 +1,80 @@
1
+ ---
2
+ name: publish
3
+ description: >-
4
+ Thin operator surface for leftover publish channels — "publish this draft to
5
+ Medium", "fan out to Qiita and Medium", "prepare a Juejin draft". Shells
6
+ `kk executor run` / `kk fanout` for file-cli and script channels; sequences
7
+ agent-default CMS channels as workflow `agent.run` steps. Does not own
8
+ transport.
9
+ ---
10
+
11
+ # publish — leftover channel operator surface
12
+
13
+ ## Purpose
14
+
15
+ Drive harvested `kind: publish` plugins after a draft exists. **The plugin owns
16
+ transport and Result.** This skill selects the channel, confirms draft vs live,
17
+ and shells `kk` — it does not call an LLM from inside a plugin process and does
18
+ not add a `kk publish` noun.
19
+
20
+ ## Channels
21
+
22
+ | Channel | Plugin | Transport | How this skill invokes it |
23
+ | --- | --- | --- | --- |
24
+ | medium | `medium-pub` | `file-cli` (token API) | `kk executor run medium-pub --in <content.json> --out <result.json>` |
25
+ | wechatmp | `wechatmp-pub` | `file-cli` (`draft/add`) | `kk executor run wechatmp-pub` (default operation `draft`) |
26
+ | x post | `x-pub` | `file-cli` (`POST /2/tweets`) | `kk executor run x-pub` (`options.operation` omitted / `post`) |
27
+ | x article | `x-pub` | `browser` / `agent` | workflow `agent.run` — not `kk fanout` |
28
+ | juejin | `juejin-pub` | `browser` / `agent` | workflow `agent.run` |
29
+ | infoq | `infoq-pub` | `browser` / `agent` | workflow `agent.run` |
30
+ | substack | `substack-pub` | `browser` / `agent` | workflow `agent.run` |
31
+ | xhs | `xhs-pub` | `browser` / `agent` | workflow `agent.run` |
32
+
33
+ Already-harvested file-cli channels (`qiita-pub`, `zenn-pub`, `surfdash-pub`,
34
+ `emdash-pub`, `podcast-pub`) compose the same way.
35
+
36
+ ## Do not
37
+
38
+ - Embed Playwright selectors or reimplement a transport here.
39
+ - Spawn an LLM from `kk fanout` (ADR-007 R4). Agent-default channels are
40
+ sequential workflow `agent.run` steps — see
41
+ `plugins/kk/workflows/leftover-publish-fanout-example.yaml`.
42
+ - Add a first-layer `kk publish` command.
43
+ - Put secrets in `--in` JSON or `plugin.json`. Env only.
44
+
45
+ ## Confirmation
46
+
47
+ Default Result is **draft / unpublished**. Live publish (`public`, WeChat
48
+ `freepublish/submit`, CMS submit) requires an explicit operator confirmation
49
+ in this skill before the plugin is invoked with a live status. Under
50
+ `KK_NONINTERACTIVE=1`, proceed only when the `--in` payload already carries
51
+ the confirmed live status; otherwise fail naming the switch.
52
+
53
+ ## Invocation
54
+
55
+ ```
56
+ <channel> [--in <content.json>] [--out <result.json>] [--live]
57
+ [--fanout <channel>,<channel>]
58
+ ```
59
+
60
+ 1. Resolve `--in` Content (required).
61
+ 2. If `--fanout` lists two or more **file-cli or script** channels: run
62
+ `kk fanout --in <content.json> --targets <list> --out <aggregate.json>`.
63
+ Each target contributes one `targets[]` Result entry.
64
+ 3. If the channel's default driver is `agent`: do **not** call `kk fanout`.
65
+ Tell the operator (or the workflow) to run the page against
66
+ `~/.config/kk/browser-profiles/<channel>` then `kk executor run <plugin>`
67
+ so the plugin writes Result. The example workflow sequences that as
68
+ `agent.run`.
69
+ 4. Otherwise: `kk executor run <plugin> --in … --out …`.
70
+ 5. Report `--out` `ok`, `target`, `id`/`url`, and `error` verbatim on failure.
71
+
72
+ ## Env (plugin-owned; never echoed)
73
+
74
+ | Plugin | Required |
75
+ | --- | --- |
76
+ | medium-pub | `MEDIUM_INTEGRATION_TOKEN` |
77
+ | wechatmp-pub | `WECHAT_APP_ID`, `WECHAT_APP_SECRET` |
78
+ | x-pub `post` | `X_API_KEY`, `X_API_SECRET`, `X_ACCESS_TOKEN`, `X_ACCESS_TOKEN_SECRET` |
79
+
80
+ Missing env → plugin exit 1, `Result.ok false`, error names the variable.
@@ -19,7 +19,12 @@ vars:
19
19
  limit: "10"
20
20
  language: "zh"
21
21
  voice_profile: "robin-news"
22
- voice_generator: "omni-voice-gen"
22
+ # 20260917: voice driver switch — ominivoice (default, omni-voice-gen) | voicebox (voice-gen).
23
+ vdriver: "ominivoice"
24
+ # 20260917 review #7: 8s intro sting prepended to every daily output; empty = skip.
25
+ intro_music: ".works/kk-daily-ai-voice/AI每日资讯_8秒播客片头.mp3"
26
+ # 20260917 review #4/#6: surfdash checkout the patch stage commits to.
27
+ surfdash_root: "/Users/robin/xprojects/surfdash"
23
28
  title: ""
24
29
  cursor: ""
25
30
  state_file: ""
@@ -28,6 +33,7 @@ vars:
28
33
  cover_enabled: "true"
29
34
  plan_max_items: "20"
30
35
  plan_translate_enabled: "true"
36
+ article_write_enabled: "true"
31
37
  plugins_path: ""
32
38
  last30days_topic: "AI news"
33
39
  horizon_hours: "24"
@@ -35,10 +41,12 @@ vars:
35
41
  last30days_enabled: "false"
36
42
  run_date: ""
37
43
  agent: "inline"
38
- surfdash_post_path: ""
39
44
  surfdash_en_post_path: ""
40
45
  surfdash_ja_post_path: ""
41
- qc_categories: "ai-news,ai-industry"
46
+ # 20260917 funnel: allowlist widened to the whole AI taxonomy in lockstep with
47
+ # episode-plan-gen's DEFAULT_FILTER_CONFIG — genuine technical/research stories died on
48
+ # category:not-in-allowlist. `non-ai` stays excluded (that is the point of the gate).
49
+ qc_categories: "ai-news,ai-technical,ai-research,ai-industry"
42
50
  # 0098 dogfood calibration (r7, owner-approved 0099): quality 3→2, impact 2→1 — the shipped
43
51
  # defaults rejected 20/20 live candidates (strongest ai-news items died solely on impact:1<2).
44
52
  # These pins must move in lockstep with episode-plan-gen's DEFAULT_FILTER_CONFIG (env wins).
@@ -121,15 +129,33 @@ states:
121
129
  options:
122
130
  role: scribe
123
131
  agent: ${vars.agent}
132
+ # ADR-115 error tier: keep this prompt <= 1000 chars (0133 R3; 0138 R4a adds the copy discipline).
124
133
  input: |
125
- Read ${vars.work_dir}/2-plan/${vars.run_date}_04_quality-control-content_candidates.json — its metadata.docs (Doc[]) is the episode plan to translate. Do not edit candidates.json.
126
- For every item with needsTranslation, translate "title" and "body" into natural, concise simplified Chinese.
127
- Titles must read as a news event (subject + action + object), derived from the body — e.g. "OpenAI 发布 Sora 应用,集成 Sora 2 模型". Keep product/model/company names verbatim, preserve URLs inside the body, and use 「」 instead of raw ASCII double quotes inside Chinese text.
128
- Keep every other field untouched (id, sourceUri, mediaType, metadata), keep technical terms in their original form where that is clearer, and produce no trailing commas or comments.
129
- Output strictly valid JSON — a same-length, same-order array carrying the same ids — to ${vars.work_dir}/2-plan/${vars.run_date}_03_plan_plan.json.
134
+ Read ${vars.work_dir}/2-plan/${vars.run_date}_04_quality-control-content_candidates.json — metadata.docs (Doc[]) is the plan; don't edit it.
135
+ For items with needsTranslation, translate "title", "body", and "metadata.brief" (when present) into natural simplified Chinese.
136
+ Titles read as a news event (subject + action + object) — e.g. "OpenAI 发布 Sora 应用,集成 Sora 2 模型". Keep product/model/company names verbatim, preserve body URLs, use 「」 not ASCII double quotes.
137
+ Copy discipline: reader-facing news copy — no scaffold labels (「可做角度」「为什么现在值得注意」「社区讨论」「背景」「潜在影响」「影响」), no duplicated labels or hedging (「读者宜另行核实」「仍待验证」) — merge their substance into prose or drop.
138
+ Keep other fields (id, sourceUri, mediaType, metadata), technical terms as-is; no trailing commas or comments.
139
+ Output strictly valid JSON — a same-length, same-order array with the same ids — to ${vars.work_dir}/2-plan/${vars.run_date}_03_plan_plan.json.
130
140
  expectFile: ${vars.work_dir}/2-plan/${vars.run_date}_03_plan_plan.json
131
141
  allowAppend: false
132
142
 
143
+ - id: article-write
144
+ description: "Write the morning-news prose article from the translated plan via agent.run (3-format split: surfdash gets prose, not a news list)"
145
+ onEnter:
146
+ - kind: agent.run
147
+ options:
148
+ role: scribe
149
+ agent: ${vars.agent}
150
+ # ADR-115 error tier: keep this prompt <= 1000 chars (0133 R3).
151
+ input: |
152
+ Read ${vars.work_dir}/2-plan/${vars.run_date}_03_plan_plan.json — metadata.docs (Doc[]) is the translated plan; don't edit it.
153
+ Write a morning-news prose article in natural simplified Chinese: title line "# 每日 AI 资讯 — ${vars.run_date}", an intro paragraph (2–3 sentences), then one section per item (## numbered title, 2–4 sentences of flowing prose, source link [来源:<label>](<sourceUri>) inline), a closing paragraph tying items together.
154
+ Narrative register — smooth transitions between items, no list-like bulleting, no scaffold labels (「可做角度」「背景」「潜在影响」), no hedging (「读者宜另行核实」「仍待验证」). Keep facts/URLs from the plan verbatim; product/model names as-is; 「」 quotes.
155
+ Output the markdown to ${vars.work_dir}/2-article/${vars.run_date}_06_article-write_body.md.
156
+ expectFile: ${vars.work_dir}/2-article/${vars.run_date}_06_article-write_body.md
157
+ allowAppend: false
158
+
133
159
  - id: article
134
160
  description: "Generate the dated daily article from the episode plan (daily-article-gen template)"
135
161
  onEnter:
@@ -203,7 +229,20 @@ states:
203
229
  bun "$st" daily-wrap-docs "${vars.work_dir}" "${vars.run_date}" "${vars.voice_profile}"
204
230
 
205
231
  - id: generate
206
- description: "Invoke voice generator plugin (voice_generator var) to generate audio"
232
+ description: "Invoke voice driver plugin (vdriver var: ominivoice -> omni-voice-gen, voicebox -> voice-gen) to generate audio"
233
+ onEnter:
234
+ - kind: shell
235
+ options:
236
+ command: >-
237
+ kbin="$(command -v kk 2>/dev/null)";
238
+ pkg="$(dirname "$(dirname "$(realpath "$kbin" 2>/dev/null)")")";
239
+ st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
240
+ [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
241
+ [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
242
+ OMNIVOICE_SPEED=1.06 bun "$st" daily-generate "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.transcode_mp3}" "${vars.vdriver}"
243
+
244
+ - id: intro-music
245
+ description: "Prepend the 8s intro sting (intro_music var; empty = skip) to the generated wav, update duration/audioPath"
207
246
  onEnter:
208
247
  - kind: shell
209
248
  options:
@@ -213,7 +252,7 @@ states:
213
252
  st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
214
253
  [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
215
254
  [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
216
- bun "$st" daily-generate "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.transcode_mp3}" "${vars.voice_generator}"
255
+ bun "$st" daily-intro "${vars.work_dir}" "${vars.run_date}" "${vars.intro_music}"
217
256
 
218
257
  - id: quality-control
219
258
  description: "Verify audio quality, speech rate, and silence/repetition QC metrics"
@@ -293,8 +332,8 @@ states:
293
332
  [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
294
333
  bun "$st" daily-publish-classify "${vars.work_dir}" "${vars.run_date}"
295
334
 
296
- - id: publish-partial
297
- description: "Partial publish — at least one target published, at least one failed; echo warning, write a .spur/run marker, preserve result.json for --retry-from, continue to done (task 0118: surfdash failure = no-URL fallback notes; podcast failure = its own partial)"
335
+ - id: patch-surfdash
336
+ description: "20260917 #4/#6 — patch published surfdash posts: podcast cover URL into image/og_image frontmatter + podcast cross-link (zh/en/ja), commit+push"
298
337
  onEnter:
299
338
  - kind: shell
300
339
  options:
@@ -304,10 +343,10 @@ states:
304
343
  st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
305
344
  [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
306
345
  [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
307
- bun "$st" daily-publish-partial "${vars.work_dir}" "${vars.run_date}"
346
+ bun "$st" daily-patch-surfdash "${vars.work_dir}" "${vars.run_date}" "${vars.surfdash_root}"
308
347
 
309
- - id: run-report
310
- description: "Assemble the per-run report (scores+aggregates+rejected counts+step timing) from existing artifacts — news-report-gen; output-only, never throws; 0117"
348
+ - id: publish-partial
349
+ description: "Partial publish — at least one target published, at least one failed; echo warning, write a .spur/run marker, preserve result.json for --retry-from, continue to done (task 0118: surfdash failure = no-URL fallback notes; podcast failure = its own partial)"
311
350
  onEnter:
312
351
  - kind: shell
313
352
  options:
@@ -317,10 +356,10 @@ states:
317
356
  st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
318
357
  [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
319
358
  [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
320
- bun "$st" daily-run-report "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.qc_min_quality}" "${vars.qc_min_importance}" "${vars.qc_min_urgency}" "${vars.qc_min_impact}"
359
+ bun "$st" daily-publish-partial "${vars.work_dir}" "${vars.run_date}"
321
360
 
322
- - id: translate-en
323
- description: "Scaffold en draft from the zh article via surfdash-pub scaffold-locale (postPath from publish result; done-no-content/none-publish runs carry no zh path and skip)"
361
+ - id: run-report
362
+ description: "Assemble the per-run report (scores+aggregates+rejected counts+step timing) from existing artifacts — news-report-gen; output-only, never throws; 0117"
324
363
  onEnter:
325
364
  - kind: shell
326
365
  options:
@@ -330,32 +369,10 @@ states:
330
369
  st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
331
370
  [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
332
371
  [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
333
- bun "$st" daily-translate-en "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
334
- - kind: file.read.into-var
335
- options:
336
- path: ${vars.work_dir}/3-publish/${vars.run_date}_20_translate-en_zh-post-path.txt
337
- var: surfdash_post_path
338
- - kind: file.read.into-var
339
- options:
340
- path: ${vars.work_dir}/3-publish/${vars.run_date}_20_translate-en_en-post-path.txt
341
- var: surfdash_en_post_path
372
+ bun "$st" daily-run-report "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.qc_min_quality}" "${vars.qc_min_importance}" "${vars.qc_min_urgency}" "${vars.qc_min_impact}"
342
373
 
343
- - id: translate-en-agent
344
- description: "Translate the en draft in place via agent.run"
345
- onEnter:
346
- - kind: agent.run
347
- options:
348
- role: scribe
349
- agent: ${vars.agent}
350
- input: |
351
- Translate the English draft post at ${vars.surfdash_en_post_path} from Chinese to English.
352
- Edit the file in place: translate the title in frontmatter and the whole markdown body to natural English. Keep frontmatter keys, dates, URLs, code, and the assets/ image reference untouched. Keep the draft: true flag.
353
- Read the surfdash translator glossary if present at the surfdash checkout root under .agents/skills/surfdash-translator/glossary.md and apply its terminology.
354
- Do not wait for operator confirmation. The file must end fully in English.
355
- expectFile: ${vars.surfdash_en_post_path}
356
-
357
- - id: translate-ja
358
- description: "Scaffold ja draft from the zh article via surfdash-pub scaffold-locale (sed-derived path as cache pre-check; plugin's draftPath is authoritative once scaffolded)"
374
+ - id: translate
375
+ description: "Locales — surfdash-pub scaffolds en/ja drafts (deterministic, plugin-owned), agent translates both, sync into the surfdash CMS"
359
376
  onEnter:
360
377
  - kind: shell
361
378
  options:
@@ -365,29 +382,26 @@ states:
365
382
  st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
366
383
  [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
367
384
  [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
368
- bun "$st" daily-translate-ja "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
385
+ bun "$st" daily-translate-localize "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
369
386
  - kind: file.read.into-var
370
387
  options:
371
- path: ${vars.work_dir}/3-publish/${vars.run_date}_22_translate-ja_ja-post-path.txt
388
+ path: ${vars.work_dir}/3-publish/${vars.run_date}_20_translate-localize_en-post-path.txt
389
+ var: surfdash_en_post_path
390
+ - kind: file.read.into-var
391
+ options:
392
+ path: ${vars.work_dir}/3-publish/${vars.run_date}_20_translate-localize_ja-post-path.txt
372
393
  var: surfdash_ja_post_path
373
-
374
- - id: translate-ja-agent
375
- description: "Translate the ja draft in place via agent.run"
376
- onEnter:
377
394
  - kind: agent.run
378
395
  options:
379
396
  role: scribe
380
397
  agent: ${vars.agent}
381
398
  input: |
382
- Translate the Japanese draft post at ${vars.surfdash_ja_post_path} from Chinese to Japanese.
383
- Edit the file in place: translate the title in frontmatter and the whole markdown body to natural Japanese. Keep frontmatter keys, dates, URLs, code, and the assets/ image reference untouched. Keep the draft: true flag.
384
- Read the surfdash translator glossary if present at the surfdash checkout root under .agents/skills/surfdash-translator/glossary.md and apply its terminology.
385
- Do not wait for operator confirmation. The file must end fully in Japanese.
386
- expectFile: ${vars.surfdash_ja_post_path}
387
-
388
- - id: translate-sync
389
- description: "Sync the zh post and its en/ja locale variants into the surfdash CMS (surfdash-pub sync operation)"
390
- onEnter:
399
+ Translate en/ja draft posts of the zh article — two independent files:
400
+ English draft: ${vars.surfdash_en_post_path}
401
+ Japanese draft: ${vars.surfdash_ja_post_path}
402
+ For each: if its path above is empty, skip that locale. Otherwise edit the 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.
403
+ 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.
404
+ Do not wait for operator confirmation. Each edited file must end fully in its target language.
391
405
  - kind: shell
392
406
  options:
393
407
  command: >-
@@ -396,7 +410,7 @@ states:
396
410
  st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
397
411
  [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
398
412
  [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
399
- bun "$st" daily-translate-sync "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.surfdash_post_path}"
413
+ bun "$st" daily-translate-sync "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.work_dir}/3-publish/${vars.run_date}_20_translate-localize_zh-post-path.txt"
400
414
 
401
415
  - id: done
402
416
  description: "Terminal — Daily AI news pipeline completed (collect → QC'd plan → article/cover/script/voice → publish/locales)"
@@ -442,17 +456,41 @@ transitions:
442
456
  options:
443
457
  command: 'test "${vars.plan_translate_enabled}" = "true"'
444
458
 
459
+ - from: quality-control-content
460
+ to: article-write
461
+ description: "Candidates passed QC, plan-translate disabled, prose enabled -> straight to article-write"
462
+ guard:
463
+ kind: shell
464
+ options:
465
+ command: 'test "${vars.plan_translate_enabled}" != "true" && test "${vars.article_write_enabled}" = "true"'
466
+
445
467
  - from: quality-control-content
446
468
  to: article
447
- description: "Candidates passed QC, plan-translate disabled -> straight to article"
469
+ description: "Prose disabled -> package the template article directly"
470
+ guard:
471
+ kind: shell
472
+ options:
473
+ command: 'test "${vars.article_write_enabled}" != "true"'
474
+
475
+ - from: plan-translate
476
+ to: article-write
477
+ description: "Plan translated and prose enabled -> write prose article from plan"
448
478
  guard:
449
479
  kind: shell
450
480
  options:
451
- command: 'test "${vars.plan_translate_enabled}" != "true"'
481
+ command: 'test "${vars.article_write_enabled}" = "true"'
452
482
 
453
483
  - from: plan-translate
454
484
  to: article
455
- description: "Plan translated -> generate article from plan"
485
+ description: "Plan translated, prose disabled -> package the template article"
486
+ guard:
487
+ kind: shell
488
+ options:
489
+ command: 'test "${vars.article_write_enabled}" != "true"'
490
+
491
+ - from: article-write
492
+ to: article
493
+ description: "Prose body written -> package article content"
456
494
  guard:
457
495
  kind: always
458
496
 
@@ -518,27 +556,30 @@ transitions:
518
556
  guard:
519
557
  kind: always
520
558
 
559
+ # 0140: intentionally NO `qc.passed` conjunct on these two edges (operator decision, ratified
560
+ # 2026-09-17, see the 0140 Q&A). The gate that used to sit here read
561
+ # `jq -r '.metadata.qc.passed // true' <generate content>` — and jq's `//` treats `false` as empty,
562
+ # so it printed "true" for a failing audit and the conjunct passed for every input. It never gated
563
+ # (trace `dogfood-daily-031816` fired quality-control → publish-prep with `passed:false` on disk),
564
+ # so removing it is behaviour-neutral. Policy: degradation-class audits (loudness dips,
565
+ # transcription fidelity) publish warn-only and surface in the run report's ## 音频 QC block;
566
+ # the credibility-damaging class (`repetition_detected`) is hard-failed upstream by 0139 R5a.
567
+ # Do not restore a `qc.passed` guard here without also fixing the `// false`-swallowing expression.
521
568
  - from: quality-control
522
569
  to: publish-prep
523
- description: "publish enabled AND QC passed -> merge article + audio for the publish seam"
570
+ description: "publish enabled -> merge article + audio for the publish seam"
524
571
  guard:
525
572
  kind: shell
526
573
  options:
527
- command: 'test "${vars.publish_enabled}" = "true" && test "$(jq -r ''.metadata.qc.passed // true'' "${vars.work_dir}/3-audio/${vars.run_date}_11_generate_content.json")" = "true"'
574
+ command: 'test "${vars.publish_enabled}" = "true"'
528
575
 
529
576
  - from: quality-control
530
577
  to: done
531
- description: "publish disabled AND QC passed -> audio episode complete (no publish)"
578
+ description: "publish disabled -> audio episode complete (no publish)"
532
579
  guard:
533
580
  kind: shell
534
581
  options:
535
- command: 'test "${vars.publish_enabled}" != "true" && test "$(jq -r ''.metadata.qc.passed // true'' "${vars.work_dir}/3-audio/${vars.run_date}_11_generate_content.json")" = "true"'
536
-
537
- - from: quality-control
538
- to: failed
539
- description: "Audio QC failed -> pipeline failed"
540
- guard:
541
- kind: always
582
+ command: 'test "${vars.publish_enabled}" != "true"'
542
583
 
543
584
  - from: publish-prep
544
585
  to: publish-surfdash
@@ -565,12 +606,16 @@ transitions:
565
606
  kind: always
566
607
 
567
608
  - from: publish
609
+ to: patch-surfdash
610
+ description: "Publish classified -> patch surfdash posts with cover + podcast cross-link"
611
+ guard:
612
+ kind: always
613
+
614
+ - from: patch-surfdash
568
615
  to: run-report
569
- description: "Publish full (all targets published) -> assemble run report first"
616
+ description: "Posts patched -> assemble run report"
570
617
  guard:
571
- kind: shell
572
- options:
573
- command: 'test "$(cat "${vars.work_dir}/3-publish/${vars.run_date}_17_publish_publish-status.txt")" = "full"'
618
+ kind: always
574
619
 
575
620
  - from: publish
576
621
  to: publish-partial
@@ -595,8 +640,8 @@ transitions:
595
640
  kind: always
596
641
 
597
642
  - from: run-report
598
- to: translate-en
599
- description: "Run report assembled after full publish -> scaffold + translate en locale"
643
+ to: translate
644
+ description: "Run report assembled after full publish -> scaffold + translate en/ja locales + sync"
600
645
  guard:
601
646
  kind: shell
602
647
  options:
@@ -610,48 +655,8 @@ transitions:
610
655
  options:
611
656
  command: 'test "$(cat "${vars.work_dir}/3-publish/${vars.run_date}_17_publish_publish-status.txt")" = "partial"'
612
657
 
613
- - from: translate-en
614
- to: translate-en-agent
615
- description: "zh post published and en draft scaffolded -> agent translates en draft in place"
616
- guard:
617
- kind: shell
618
- options:
619
- command: 'test -n "${vars.surfdash_post_path}" && test -f "${vars.surfdash_en_post_path}"'
620
-
621
- - from: translate-en
622
- to: translate-ja
623
- description: "no zh post path -> skip en locale"
624
- guard:
625
- kind: always
626
-
627
- - from: translate-en-agent
628
- to: translate-ja
629
- description: "en draft translated -> scaffold ja locale"
630
- guard:
631
- kind: always
632
-
633
- - from: translate-ja
634
- to: translate-ja-agent
635
- description: "zh post published and ja draft scaffolded -> agent translates ja draft in place"
636
- guard:
637
- kind: shell
638
- options:
639
- command: 'test -n "${vars.surfdash_post_path}" && test -f "${vars.surfdash_ja_post_path}"'
640
-
641
- - from: translate-ja
642
- to: translate-sync
643
- description: "no zh post path -> skip ja locale"
644
- guard:
645
- kind: always
646
-
647
- - from: translate-ja-agent
648
- to: translate-sync
649
- description: "ja draft translated -> sync all locales into the CMS"
650
- guard:
651
- kind: always
652
-
653
- - from: translate-sync
658
+ - from: translate
654
659
  to: done
655
- description: "zh post + locales synced into surfdash CMS -> episode complete"
660
+ description: "zh post + en/ja locales scaffolded, translated, and synced into the surfdash CMS -> episode complete"
656
661
  guard:
657
662
  kind: always
@@ -0,0 +1,53 @@
1
+ "$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
2
+ kind: state-machine
3
+ name: leftover-publish-fanout-example
4
+ description: >-
5
+ Example: fan out two leftover file-cli channels (medium-pub, wechatmp-pub),
6
+ then sequence one agent-default CMS channel (juejin-pub) as agent.run.
7
+ kk fanout does not spawn an LLM.
8
+ initialState: fanout-file-cli
9
+ terminalStates:
10
+ - done
11
+ - failed
12
+ failureStates:
13
+ - failed
14
+ vars:
15
+ contentFile: ".spur/run/content.json"
16
+ fanoutFile: ".spur/run/fanout-result.json"
17
+ juejinFile: ".spur/run/juejin-result.json"
18
+ spurBin: "spur"
19
+ states:
20
+ - id: fanout-file-cli
21
+ description: "kk fanout over leftover file-cli channels (medium + wechatmp draft)"
22
+ onEnter:
23
+ - kind: shell
24
+ options:
25
+ command: >-
26
+ kk fanout --in ${vars.contentFile}
27
+ --targets medium-pub,wechatmp-pub
28
+ --out ${vars.fanoutFile}
29
+ - id: agent-juejin
30
+ description: "Agent-default CMS channel — workflow agent.run, not kk fanout"
31
+ onEnter:
32
+ - kind: agent.run
33
+ options:
34
+ role: coder
35
+ input: |
36
+ Invoke the kk:publish skill for juejin.
37
+ Drive ~/.config/kk/browser-profiles/juejin as driver=agent.
38
+ Then `kk executor run juejin-pub --in ${vars.contentFile} --out ${vars.juejinFile}`.
39
+ Do not embed an LLM in the plugin process. Do not call kk fanout for this channel.
40
+ expectFile: ${vars.juejinFile}
41
+ - id: done
42
+ description: "Fan-out aggregate plus agent CMS Result written"
43
+ - id: failed
44
+ description: "A file-cli target or the agent CMS step failed loud"
45
+ transitions:
46
+ - from: fanout-file-cli
47
+ to: agent-juejin
48
+ guard:
49
+ kind: always
50
+ - from: agent-juejin
51
+ to: done
52
+ guard:
53
+ kind: always