@gobing-ai/knowledge-kit 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +2478 -1737
  3. package/package.json +1 -1
  4. package/plugins/generations/image-gen/README.md +108 -0
  5. package/plugins/generations/image-gen/assets/sample/cover__clean-webapp-ui.jpg +0 -0
  6. package/plugins/generations/image-gen/assets/sample/cover__cute.jpg +0 -0
  7. package/plugins/generations/image-gen/assets/sample/cover__daily-news.jpg +0 -0
  8. package/plugins/generations/image-gen/assets/sample/cover__editorial.jpg +0 -0
  9. package/plugins/generations/image-gen/assets/sample/cover__fresh.jpg +0 -0
  10. package/plugins/generations/image-gen/assets/sample/cover__minimalist.jpg +0 -0
  11. package/plugins/generations/image-gen/assets/sample/cover__photorealistic.jpg +0 -0
  12. package/plugins/generations/image-gen/assets/sample/cover__sketch.jpg +0 -0
  13. package/plugins/generations/image-gen/assets/sample/cover__technical-diagram.jpg +0 -0
  14. package/plugins/generations/image-gen/assets/sample/cover__vibrant.jpg +0 -0
  15. package/plugins/generations/image-gen/assets/sample/cover__warm.jpg +0 -0
  16. package/plugins/generations/image-gen/assets/sample/cover__xhs-knowledge.jpg +0 -0
  17. package/plugins/generations/image-gen/assets/sample/daily-news-cover__clean-webapp-ui.jpg +0 -0
  18. package/plugins/generations/image-gen/assets/sample/daily-news-cover__cute.jpg +0 -0
  19. package/plugins/generations/image-gen/assets/sample/daily-news-cover__daily-news.jpg +0 -0
  20. package/plugins/generations/image-gen/assets/sample/daily-news-cover__editorial.jpg +0 -0
  21. package/plugins/generations/image-gen/assets/sample/daily-news-cover__fresh.jpg +0 -0
  22. package/plugins/generations/image-gen/assets/sample/daily-news-cover__minimalist.jpg +0 -0
  23. package/plugins/generations/image-gen/assets/sample/daily-news-cover__photorealistic.jpg +0 -0
  24. package/plugins/generations/image-gen/assets/sample/daily-news-cover__sketch.jpg +0 -0
  25. package/plugins/generations/image-gen/assets/sample/daily-news-cover__technical-diagram.jpg +0 -0
  26. package/plugins/generations/image-gen/assets/sample/daily-news-cover__vibrant.jpg +0 -0
  27. package/plugins/generations/image-gen/assets/sample/daily-news-cover__warm.jpg +0 -0
  28. package/plugins/generations/image-gen/assets/sample/daily-news-cover__xhs-knowledge.jpg +0 -0
  29. package/plugins/generations/image-gen/assets/sample/free__clean-webapp-ui.jpg +0 -0
  30. package/plugins/generations/image-gen/assets/sample/free__cute.jpg +0 -0
  31. package/plugins/generations/image-gen/assets/sample/free__daily-news.jpg +0 -0
  32. package/plugins/generations/image-gen/assets/sample/free__editorial.jpg +0 -0
  33. package/plugins/generations/image-gen/assets/sample/free__fresh.jpg +0 -0
  34. package/plugins/generations/image-gen/assets/sample/free__minimalist.jpg +0 -0
  35. package/plugins/generations/image-gen/assets/sample/free__photorealistic.jpg +0 -0
  36. package/plugins/generations/image-gen/assets/sample/free__sketch.jpg +0 -0
  37. package/plugins/generations/image-gen/assets/sample/free__technical-diagram.jpg +0 -0
  38. package/plugins/generations/image-gen/assets/sample/free__vibrant.jpg +0 -0
  39. package/plugins/generations/image-gen/assets/sample/free__warm.jpg +0 -0
  40. package/plugins/generations/image-gen/assets/sample/free__xhs-knowledge.jpg +0 -0
  41. package/plugins/generations/image-gen/assets/sample/illustration__clean-webapp-ui.jpg +0 -0
  42. package/plugins/generations/image-gen/assets/sample/illustration__cute.jpg +0 -0
  43. package/plugins/generations/image-gen/assets/sample/illustration__daily-news.jpg +0 -0
  44. package/plugins/generations/image-gen/assets/sample/illustration__editorial.jpg +0 -0
  45. package/plugins/generations/image-gen/assets/sample/illustration__fresh.jpg +0 -0
  46. package/plugins/generations/image-gen/assets/sample/illustration__minimalist.jpg +0 -0
  47. package/plugins/generations/image-gen/assets/sample/illustration__photorealistic.jpg +0 -0
  48. package/plugins/generations/image-gen/assets/sample/illustration__sketch.jpg +0 -0
  49. package/plugins/generations/image-gen/assets/sample/illustration__technical-diagram.jpg +0 -0
  50. package/plugins/generations/image-gen/assets/sample/illustration__vibrant.jpg +0 -0
  51. package/plugins/generations/image-gen/assets/sample/illustration__warm.jpg +0 -0
  52. package/plugins/generations/image-gen/assets/sample/illustration__xhs-knowledge.jpg +0 -0
  53. package/plugins/generations/image-gen/assets/sample/xhs-card__clean-webapp-ui.jpg +0 -0
  54. package/plugins/generations/image-gen/assets/sample/xhs-card__cute.jpg +0 -0
  55. package/plugins/generations/image-gen/assets/sample/xhs-card__daily-news.jpg +0 -0
  56. package/plugins/generations/image-gen/assets/sample/xhs-card__editorial.jpg +0 -0
  57. package/plugins/generations/image-gen/assets/sample/xhs-card__fresh.jpg +0 -0
  58. package/plugins/generations/image-gen/assets/sample/xhs-card__minimalist.jpg +0 -0
  59. package/plugins/generations/image-gen/assets/sample/xhs-card__photorealistic.jpg +0 -0
  60. package/plugins/generations/image-gen/assets/sample/xhs-card__sketch.jpg +0 -0
  61. package/plugins/generations/image-gen/assets/sample/xhs-card__technical-diagram.jpg +0 -0
  62. package/plugins/generations/image-gen/assets/sample/xhs-card__vibrant.jpg +0 -0
  63. package/plugins/generations/image-gen/assets/sample/xhs-card__warm.jpg +0 -0
  64. package/plugins/generations/image-gen/assets/sample/xhs-card__xhs-knowledge.jpg +0 -0
  65. package/plugins/generations/image-gen/dist/index.js +102 -58
  66. package/plugins/generations/image-gen/scripts/compose-e2e-artifact.ts +5 -21
  67. package/plugins/generations/image-gen/scripts/gen-samples.ts +208 -0
  68. package/plugins/generations/image-gen/src/compose/chromium.ts +2 -3
  69. package/plugins/generations/image-gen/src/presets.ts +6 -3
  70. package/plugins/generations/image-gen/src/providers/errors.ts +16 -1
  71. package/plugins/generations/image-gen/src/providers/minimax.ts +46 -17
  72. package/plugins/ingestions/web-search/src/index.ts +1 -1
  73. package/plugins/kk/README.md +18 -0
  74. package/plugins/kk/commands/image-extract.md +1 -0
  75. package/plugins/kk/commands/image-generate.md +1 -0
  76. package/plugins/kk/commands/tell-me.md +1 -0
  77. package/plugins/kk/commands/workflow-run.md +7 -1
  78. package/plugins/kk/decisions/extract/it-topics.yaml +24 -0
  79. package/plugins/kk/decisions/kk-itc.yaml +6 -4
  80. package/plugins/kk/plugin.json +1 -1
  81. package/plugins/kk/publish-presets/daily-ai-voice-migration.json +146 -0
  82. package/plugins/kk/publish-presets/daily-ai-voice.json +11 -3
  83. package/plugins/kk/schedules/kk-collect.grok-bot.md.tmpl +69 -0
  84. package/plugins/kk/schedules/kk-daily-ai-voice3.cron.tmpl +18 -0
  85. package/plugins/kk/schedules/kk-daily-publish3-cn-peak.cron.tmpl +15 -5
  86. package/plugins/kk/schedules/kk-daily-publish3-overseas.cron.tmpl +9 -4
  87. package/plugins/kk/scripts/daily-scheduled-run.ts +298 -0
  88. package/plugins/kk/scripts/daily-stages.ts +1403 -156
  89. package/plugins/kk/scripts/kk-workflow-stages.ts +266 -10
  90. package/plugins/kk/scripts/safety-artifact-check.ts +277 -0
  91. package/plugins/kk/scripts/voice3-publish-window.ts +202 -0
  92. package/plugins/kk/skills/article-adapt/SKILL.md +1 -0
  93. package/plugins/kk/skills/audio-authoring/SKILL.md +1 -0
  94. package/plugins/kk/skills/content-judge/SKILL.md +1 -0
  95. package/plugins/kk/skills/content-safety/SKILL.md +1 -0
  96. package/plugins/kk/skills/explain-things/SKILL.md +1 -0
  97. package/plugins/kk/skills/image-authoring/SKILL.md +1 -0
  98. package/plugins/kk/skills/itc-generating/SKILL.md +1 -0
  99. package/plugins/kk/skills/publish/SKILL.md +1 -0
  100. package/plugins/kk/skills/smart-seo/SKILL.md +1 -0
  101. package/plugins/kk/skills/storm-research/SKILL.md +1 -0
  102. package/plugins/kk/skills/taste-unslop/SKILL.md +1 -0
  103. package/plugins/kk/skills/topic/SKILL.md +1 -0
  104. package/plugins/kk/skills/web-automation/SKILL.md +1 -0
  105. package/plugins/kk/skills/xhs-images/SKILL.md +1 -0
  106. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +177 -128
  107. package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +161 -61
  108. package/plugins/kk/workflows/kk-daily-publish3.yaml +82 -26
  109. package/plugins/kk/workflows/kk-daily-safety.yaml +156 -69
  110. package/plugins/kk/workflows/kk-itc-author.yaml +75 -0
  111. package/plugins/kk/workflows/kk-itc-publish-tail.yaml +25 -3
  112. package/plugins/kk/workflows/kk-itc.yaml +36 -4
  113. package/plugins/publishings/bilibili-pub/dist/index.js +2 -1
  114. package/plugins/publishings/bilibili-pub/dist/profile-check.js +2 -1
  115. package/plugins/publishings/cnblogs-pub/dist/index.js +2 -1
  116. package/plugins/publishings/cnblogs-pub/dist/profile-check.js +2 -1
  117. package/plugins/publishings/csdn-pub/dist/index.js +2 -1
  118. package/plugins/publishings/csdn-pub/dist/profile-check.js +2 -1
  119. package/plugins/publishings/infoq-pub/dist/index.js +2 -1
  120. package/plugins/publishings/infoq-pub/dist/profile-check.js +2 -1
  121. package/plugins/publishings/jike-pub/dist/index.js +2 -1
  122. package/plugins/publishings/jike-pub/dist/profile-check.js +2 -1
  123. package/plugins/publishings/juejin-pub/dist/index.js +2 -1
  124. package/plugins/publishings/juejin-pub/dist/profile-check.js +2 -1
  125. package/plugins/publishings/linkedin-pub/dist/index.js +21333 -0
  126. package/plugins/publishings/linkedin-pub/dist/profile-check.js +21005 -0
  127. package/plugins/publishings/linkedin-pub/package.json +19 -0
  128. package/plugins/publishings/linkedin-pub/plugin.json +12 -0
  129. package/plugins/publishings/linkedin-pub/src/index.ts +705 -0
  130. package/plugins/publishings/linkedin-pub/src/profile-check.ts +17 -0
  131. package/plugins/publishings/linkedin-pub/tsconfig.json +4 -0
  132. package/plugins/publishings/podcast-pub/dist/index.js +46 -8
  133. package/plugins/publishings/podcast-pub/src/index.ts +62 -5
  134. package/plugins/publishings/shipinhao-pub/dist/index.js +2 -1
  135. package/plugins/publishings/shipinhao-pub/dist/profile-check.js +2 -1
  136. package/plugins/publishings/sspai-pub/dist/index.js +2 -1
  137. package/plugins/publishings/sspai-pub/dist/profile-check.js +2 -1
  138. package/plugins/publishings/substack-pub/dist/index.js +2 -1
  139. package/plugins/publishings/substack-pub/dist/profile-check.js +2 -1
  140. package/plugins/publishings/surfdash-pub/dist/index.js +461 -53
  141. package/plugins/publishings/surfdash-pub/src/digest-check.ts +85 -19
  142. package/plugins/publishings/surfdash-pub/src/index.ts +592 -65
  143. package/plugins/publishings/threads-pub/dist/index.js +7 -0
  144. package/plugins/publishings/threads-pub/src/index.ts +12 -0
  145. package/plugins/publishings/wechatmp-pub/dist/index.js +936 -98
  146. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +60 -3
  147. package/plugins/publishings/wechatmp-pub/src/flow.ts +1134 -86
  148. package/plugins/publishings/wechatmp-pub/src/index.ts +112 -30
  149. package/plugins/publishings/wechatmp-pub/src/page.ts +105 -0
  150. package/plugins/publishings/weibo-pub/dist/index.js +2 -1
  151. package/plugins/publishings/weibo-pub/dist/profile-check.js +2 -1
  152. package/plugins/publishings/x-pub/dist/index.js +9 -1
  153. package/plugins/publishings/x-pub/dist/profile-check.js +2 -1
  154. package/plugins/publishings/x-pub/src/index.ts +11 -0
  155. package/plugins/publishings/xhs-pub/dist/index.js +2 -1
  156. package/plugins/publishings/xhs-pub/dist/profile-check.js +2 -1
  157. package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +2 -1
  158. package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +2 -1
@@ -24,6 +24,12 @@ vars:
24
24
  publish_window: "after-generate"
25
25
  publish_preset: ""
26
26
  publish_dummy: "false"
27
+ # 0334 R7: opt-in live consent channel, forwarded verbatim to the nested publish3
28
+ # `live_channels`. Empty by default — this variable alone can never produce a live send
29
+ # (ADR-022: only an operator launch that sets it, with tokens the plan stage validates
30
+ # against the window, promotes rows; `publish_dummy=true` and a non-empty token list
31
+ # refuse each other at plan time).
32
+ publish_live_channels: ""
27
33
  # Composed by the `prepare` node (`kk daily prepare`), then read back into vars through the
28
34
  # single-node read chain below — shell actions cannot set vars, and a concurrent `setVars` merge
29
35
  # is last-writer-wins, so the DAG exchanges data by fixed paths under work_dir, never by vars.
@@ -140,6 +146,7 @@ nodes:
140
146
  command: >-
141
147
  kk stage daily-preflight plugins/kk/workflows/kk-daily-ai-voice2.yaml
142
148
  plugins/kk/workflows/kk-daily-safety.yaml
149
+ plugins/kk/workflows/kk-daily-publish3.yaml
143
150
  && kk daily prepare --workflow kk-daily-ai-voice2 --run-date "${vars.run_date}" --ledger-sync
144
151
  --run "${vars.__runId}" --definition-digest "${vars.__definitionDigest}"
145
152
  --setting window="${vars.window}" --setting language="${vars.language}" --setting vdriver="${vars.vdriver}"
@@ -243,6 +250,31 @@ nodes:
243
250
  command: >-
244
251
  kk stage daily-qc-content "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.qc_categories}" "${vars.qc_min_quality}" "${vars.qc_min_importance}" "${vars.qc_min_urgency}" "${vars.qc_min_impact}" "${vars.qc_min_episode_items}" "${vars.episode_min_minutes}" "${vars.vdriver}"
245
252
 
253
+ # One guard for the whole pool (0324 R3; 0329 F01/F15): it sits right after `qc-content`, in
254
+ # front of every token reader, and NEVER exits non-zero — in this engine a failed dependency
255
+ # never releases its dependents, so a failing gate on an empty pool wedges the graph into
256
+ # dag-unreachable and turns a thin day into a failed run. The token carries the verdict
257
+ # instead: PASS opens the content fan-out, EMPTY makes every token-gated node skip, and the
258
+ # run still closes `done` with a no-content run report.
259
+ - id: pool-gate
260
+ description: >-
261
+ The single pool guard (0324 R3, 0329): `kk store pool-count --min 1` over the candidates
262
+ file that `qc-content` wrote, recorded once as a token at
263
+ `.spur/run/<runId>-pool-gate.status`. A token, not a bare dependency, because the engine
264
+ skips a node only when ALL its dependencies were skipped — so a failed gate beside a done
265
+ dependency would still admit the node. The gate itself always exits 0 (0329: an empty pool
266
+ is a no-content day, not a failed run).
267
+ dependsOn: [qc-content]
268
+ action:
269
+ kind: shell
270
+ options:
271
+ command: >-
272
+ OUT=".spur/run/${vars.__runId}-pool-gate.status";
273
+ mkdir -p "$(dirname "$OUT")";
274
+ if kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1;
275
+ then echo PASS > "$OUT";
276
+ else echo EMPTY > "$OUT"; rm -f "${vars.work_dir}/bundle.json"; fi
277
+
246
278
  - id: select
247
279
  description: >-
248
280
  Record today's planned set in the store's topic-scoped `selections` ledger (0228). It runs at
@@ -250,7 +282,7 @@ nodes:
250
282
  depends on the DAG reaching its tail: a run that fails, pauses at review or skips publish still
251
283
  excludes what it planned. Guarded on the QC candidates — a pool that filtered to zero items
252
284
  selects nothing.
253
- dependsOn: [qc-content]
285
+ dependsOn: [qc-content, pool-gate]
254
286
  condition:
255
287
  kind: shell
256
288
  options:
@@ -275,7 +307,7 @@ nodes:
275
307
 
276
308
  - id: plan-translate
277
309
  description: "Translate the needsTranslation plan items into the output plan.json via agent.run; onError: continue — plan-translate-retry below is the loud gate (0238 R2)"
278
- dependsOn: [qc-content]
310
+ dependsOn: [qc-content, pool-gate]
279
311
  condition:
280
312
  kind: shell
281
313
  options:
@@ -315,7 +347,7 @@ nodes:
315
347
  VALID expectFile (missing or contract-violating JSON), retries the same input on a different
316
348
  tier, and carries NO onError — a second miss fails the run loudly instead of letting an
317
349
  untranslated or truncated plan flow downstream.
318
- dependsOn: [plan-translate]
350
+ dependsOn: [plan-translate, pool-gate]
319
351
  condition:
320
352
  kind: shell
321
353
  options:
@@ -335,7 +367,7 @@ nodes:
335
367
 
336
368
  - id: article-write
337
369
  description: "Write the tech-sharing prose article from the translated plan via agent.run (3-format split: surfdash gets prose, not a news list); onError: continue — article-write-retry below is the loud gate (0238 R2)"
338
- dependsOn: [qc-content, plan-translate, plan-translate-retry]
370
+ dependsOn: [qc-content, plan-translate, plan-translate-retry, pool-gate]
339
371
  condition:
340
372
  kind: shell
341
373
  options:
@@ -369,7 +401,7 @@ nodes:
369
401
  Bounded recovery for article-write (0238 R2): runs only when the primary settled without its
370
402
  expectFile, retries the same input on a different tier, and carries NO onError — a second miss
371
403
  fails the run loudly instead of publishing an article nobody wrote.
372
- dependsOn: [article-write]
404
+ dependsOn: [article-write, pool-gate]
373
405
  condition:
374
406
  kind: shell
375
407
  options:
@@ -394,7 +426,7 @@ nodes:
394
426
  this node: a done dependency in the mix keeps it runnable) as well as on both authoring nodes
395
427
  AND their retry siblings, and it carries the same candidates guard — a pool that filtered to
396
428
  zero items must not build an empty article.
397
- dependsOn: [qc-content, plan-translate, plan-translate-retry, article-write, article-write-retry]
429
+ dependsOn: [qc-content, plan-translate, plan-translate-retry, article-write, article-write-retry, pool-gate]
398
430
  condition:
399
431
  kind: shell
400
432
  options:
@@ -411,7 +443,7 @@ nodes:
411
443
  # 0238 R3: reads 21-plan/<date>_03_plan_plan.json, so it must not start before the plan
412
444
  # producers settle. `qc-content` stays in the list: a done dependency in the mix keeps this node
413
445
  # runnable when the plan-authoring nodes are disabled (all-skipped deps skip a node).
414
- dependsOn: [qc-content, plan-translate, plan-translate-retry]
446
+ dependsOn: [qc-content, plan-translate, plan-translate-retry, pool-gate]
415
447
  condition:
416
448
  kind: shell
417
449
  options:
@@ -458,7 +490,7 @@ nodes:
458
490
 
459
491
  - id: script-style
460
492
  description: "Snapshot the shipped zh style contract into the work dir (separate node: it is also an audit input for the revision loop)"
461
- dependsOn: [qc-content]
493
+ dependsOn: [qc-content, pool-gate]
462
494
  condition:
463
495
  kind: shell
464
496
  options:
@@ -475,7 +507,7 @@ nodes:
475
507
  # 0238 R3: reads 21-plan/<date>_03_plan_plan.json, so it must not start before the plan
476
508
  # producers settle. `script-style` stays first in the list: a done dependency in the mix keeps
477
509
  # this node runnable when the plan-authoring nodes are disabled (all-skipped deps skip a node).
478
- dependsOn: [script-style, plan-translate, plan-translate-retry]
510
+ dependsOn: [script-style, plan-translate, plan-translate-retry, article, pool-gate]
479
511
  condition:
480
512
  kind: shell
481
513
  options:
@@ -505,7 +537,7 @@ nodes:
505
537
  Compile the episode plan Doc[] into structured broadcast VoiceScript YAML Content. Depends on
506
538
  `article` so the node still runs when the plan-authoring nodes were disabled or skipped (the
507
539
  builder falls back to templates) — but not when the whole content branch is skipped.
508
- dependsOn: [article, script-style, script-write]
540
+ dependsOn: [article, script-style, script-write, pool-gate]
509
541
  condition:
510
542
  kind: shell
511
543
  options:
@@ -558,7 +590,7 @@ nodes:
558
590
 
559
591
  - id: wrap-docs
560
592
  description: "Wrap VoiceScript into Doc[] input for the voice generator plugin (the generate-side variant input)"
561
- dependsOn: [script, review]
593
+ dependsOn: [script, review, pool-gate]
562
594
  condition:
563
595
  kind: shell
564
596
  options:
@@ -574,7 +606,7 @@ nodes:
574
606
  description: "Voice tier, primary driver (vars.vdriver — voxcpm; its 40 ms lattice is known and monitored, see the vdriver comment). onError: continue — the fallback siblings below are the loud chain (same pattern as 0238 R2)."
575
607
  # wrap-docs is a data dependency, not a sibling: daily-generate reads its _10_wrap-docs_docs.json
576
608
  # as the TTS input. Without this edge the two nodes race in the same wave (0212 dogfood finding).
577
- dependsOn: [script, safety, review, wrap-docs]
609
+ dependsOn: [script, safety, review, wrap-docs, pool-gate]
578
610
  condition:
579
611
  kind: shell
580
612
  options:
@@ -582,8 +614,8 @@ nodes:
582
614
  command: 'test "$(cat ".spur/run/${vars.__runId}-pool-gate.status" 2>/dev/null)" = PASS'
583
615
  action:
584
616
  kind: shell
617
+ onError: continue
585
618
  options:
586
- onError: continue
587
619
  command: >-
588
620
  OMNIVOICE_SPEED=1.06 kk stage daily-generate "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.transcode_mp3}" "${vars.vdriver}" "1" "${vars.episode_max_minutes}" "${vars.episode_min_minutes}"
589
621
 
@@ -595,7 +627,7 @@ nodes:
595
627
 
596
628
  - id: generate-fallback
597
629
  description: "Voice tier fallback 1: gemini (gemini-tts) when the primary driver left no valid Content — the measured-dry escape hatch from voxcpm's lattice"
598
- dependsOn: [generate]
630
+ dependsOn: [generate, pool-gate]
599
631
  condition:
600
632
  kind: shell
601
633
  options:
@@ -605,14 +637,14 @@ nodes:
605
637
  && ! jq -e '(.metadata.generator == "kk:voxcpm-tts" or .metadata.generator == "kk:gemini-tts") and .metadata.qc.passed == true' "${vars.work_dir}/30-audio/${vars.run_date}_11_generate_content.json"
606
638
  action:
607
639
  kind: shell
640
+ onError: continue
608
641
  options:
609
- onError: continue
610
642
  command: >-
611
643
  kk stage daily-generate "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.transcode_mp3}" "gemini" "1" "${vars.episode_max_minutes}" "${vars.episode_min_minutes}"
612
644
 
613
645
  - id: generate-fallback2
614
646
  description: "Voice tier fallback 2 (last, loud): omni-voice-gen when neither voxcpm nor gemini delivered"
615
- dependsOn: [generate-fallback]
647
+ dependsOn: [generate-fallback, pool-gate]
616
648
  condition:
617
649
  kind: shell
618
650
  options:
@@ -626,9 +658,17 @@ nodes:
626
658
  command: >-
627
659
  OMNIVOICE_SPEED=1.06 kk stage daily-generate "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.transcode_mp3}" "ominivoice" "1" "${vars.episode_max_minutes}" "${vars.episode_min_minutes}"
628
660
 
661
+ # 0329 F15: a script-gate refusal produces no audio, but the sting must not fail the day —
662
+ # the refusal artifact admits intro (and duration closes the run as no-content on it).
629
663
  - id: intro
630
664
  description: "Prepend the 8s intro sting (intro_music var; empty = skip), update duration/audioPath"
631
665
  dependsOn: [generate, generate-fallback, generate-fallback2]
666
+ condition:
667
+ kind: shell
668
+ options:
669
+ command: >-
670
+ test -f "${vars.work_dir}/30-audio/${vars.run_date}_11_generate_content.json"
671
+ -o -f "${vars.work_dir}/30-audio/${vars.run_date}_11_generate_content_duration-refusal.json"
632
672
  action:
633
673
  kind: shell
634
674
  options:
@@ -658,32 +698,39 @@ nodes:
658
698
  onError: continue
659
699
 
660
700
  - id: duration
661
- description: "Measured duration gate: under episode_min_minutes the run refuses and closes as no-content (refusal artifact + emptied candidates, exit 0 — 0266); above episode_max_minutes it still fails loud. Duration is authoritative."
701
+ description: "Measured duration gate: under episode_min_minutes the run refuses and closes as no-content (refusal artifact + emptied candidates + publish-path invalidation, exit 0 — 0266/0329); above episode_max_minutes it still fails loud. Duration is authoritative."
662
702
  dependsOn: [intro]
663
703
  action:
664
704
  kind: shell
665
705
  options:
666
706
  command: >-
667
- kk stage daily-episode-duration "${vars.work_dir}" "${vars.run_date}" "${vars.episode_min_minutes}" "${vars.episode_max_minutes}" "${vars.vdriver}"
707
+ kk stage daily-episode-duration "${vars.work_dir}" "${vars.run_date}" "${vars.episode_min_minutes}" "${vars.episode_max_minutes}" "${vars.vdriver}" "${vars.__runId}"
668
708
 
669
709
  - id: quality
670
710
  description: >-
671
- Verify audio quality, speech rate, and silence/repetition QC metrics over the assembled
672
- episode. Depends on the audio tail and wrap-docs: voice3 no longer packages a publish-prep
673
- Content, so the audited bytes are the generated episode itself.
674
- dependsOn: [duration, wrap-docs]
711
+ Print the daily QC table (daily-quality-report) and then gate on it (0329 F23): the print
712
+ alone never gated anything — a 41/100 episode sailed into show-notes, the bundle and the
713
+ publish windows. The verdict stage fails loud when the generator's audio qc verdict is
714
+ missing or `passed` is false, so a bad episode dies here instead of at a tier-0 send.
715
+ dependsOn: [duration, wrap-docs, pool-gate]
716
+ condition:
717
+ kind: shell
718
+ options:
719
+ # 0324 R3: the pool verdict is `pool-gate`'s token, never a repeated `kk store pool-count`.
720
+ command: 'test "$(cat ".spur/run/${vars.__runId}-pool-gate.status" 2>/dev/null)" = PASS'
675
721
  action:
676
722
  kind: shell
677
723
  options:
678
724
  command: >-
679
725
  kk stage daily-quality-report "${vars.work_dir}" "${vars.run_date}"
726
+ && kk stage run daily-quality-verdict "${vars.work_dir}" "${vars.run_date}"
680
727
 
681
728
  - id: show-notes
682
729
  description: >-
683
730
  Render timeline show notes from the episode plan — content, not publishing: the publish half
684
731
  ships them as the episode variant's `metadata.showNotes`, and the link placeholders stay
685
732
  unresolved until a send resolves them."
686
- dependsOn: [quality]
733
+ dependsOn: [quality, pool-gate]
687
734
  condition:
688
735
  kind: shell
689
736
  options:
@@ -694,13 +741,36 @@ nodes:
694
741
  options:
695
742
  command: >-
696
743
  kk stage daily-show-notes "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.title}"
744
+
745
+ # 0329 F02: voice3 lost daily-publish-prep in the migration (voice2 keeps it at its :704), so
746
+ # `31-publish/<date>_13_publish-prep_content.json` — the episode variant's source — was never
747
+ # written by this workflow, and a freeze only succeeded against a stale same-day voice2 prep
748
+ # file. Rebuilt per the voice2 template: after duration (it reads the generate content's
749
+ # mp3Path, and ID3 stamps must land on the final post-intro MP3), pool-gated so a refused
750
+ # episode packages nothing.
751
+ - id: publish-prep
752
+ description: >-
753
+ Package the publish-ready episode Content (ID3 tags on the final post-intro MP3, cover
754
+ path, article link) into 31-publish/<date>_13_publish-prep_content.json — the episode
755
+ variant's source for bundle-variants.
756
+ dependsOn: [duration, article, safety, pool-gate]
757
+ condition:
758
+ kind: shell
759
+ options:
760
+ # 0324 R3: the pool verdict is `pool-gate`'s token, never a repeated `kk store pool-count`.
761
+ command: 'test "$(cat ".spur/run/${vars.__runId}-pool-gate.status" 2>/dev/null)" = PASS'
762
+ action:
763
+ kind: shell
764
+ options:
765
+ command: >-
766
+ kk stage daily-publish-prep "${vars.work_dir}" "${vars.run_date}" "${vars.podcast_artist}" "${vars.podcast_album}"
697
767
  - id: cards-write
698
768
  description: "Plan the card series across the episode's plan items via agent.run; onError: continue — a failed plan skips the card series, never the run"
699
769
  dependsOn: [script, review, pool-gate]
700
770
  condition:
701
771
  kind: shell
702
772
  options:
703
- command: 'test "${vars.cards_enabled}" = "true"'
773
+ command: 'test "${vars.cards_enabled}" = "true" && test "$(cat ".spur/run/${vars.__runId}-pool-gate.status" 2>/dev/null)" = PASS'
704
774
  action:
705
775
  kind: agent.run
706
776
  onError: continue
@@ -720,14 +790,14 @@ nodes:
720
790
 
721
791
  - id: cards-render
722
792
  description: "Render the card series through the pinned xhs-card/xhs-knowledge presets, attach the episode cover last and pair every image with its numbered line (0235 note_scope=episode); onError: continue — the publish node names the missing series and stays soft"
723
- dependsOn: [cards-write]
793
+ dependsOn: [cards-write, pool-gate]
724
794
  # A skipped cards-write still admits its dependents, so each card node keeps its own switch:
725
795
  # without it a run with cards_enabled=false would re-package a stale same-day 25-xhs/ series.
726
796
  # The pool guard is `pool-gate`'s alone in voice3 (0324 R3).
727
797
  condition:
728
798
  kind: shell
729
799
  options:
730
- command: 'test "${vars.cards_enabled}" = "true"'
800
+ command: 'test "${vars.cards_enabled}" = "true" && test "$(cat ".spur/run/${vars.__runId}-pool-gate.status" 2>/dev/null)" = PASS'
731
801
  action:
732
802
  kind: shell
733
803
  onError: continue
@@ -737,31 +807,12 @@ nodes:
737
807
 
738
808
  # ---- generate-side payloads and the frozen bundle (0324 R2/R3) ----
739
809
 
740
- # One guard for the whole pool (0324 R3): every node that used to repeat `kk store pool-count`
741
- # depends on this one, so a refused pool empties the candidates file once and the rest of the
742
- # graph skips through the engine's all-deps-skipped rule.
743
- - id: pool-gate
744
- description: >-
745
- The single pool guard (0324 R3): `kk store pool-count --min 1` over the candidates file that
746
- `qc-content` wrote, recorded once as a token at `.spur/run/<runId>-pool-gate.status`. voice2
747
- repeated the same guard on twenty-one conditions. A token, not a bare dependency, because the
748
- engine skips a node only when ALL its dependencies were skipped — so a failed gate beside a
749
- done dependency would still admit the node. Everything downstream reads the token.
750
- dependsOn: [qc-content]
751
- action:
752
- kind: shell
753
- options:
754
- command: >-
755
- OUT=".spur/run/${vars.__runId}-pool-gate.status";
756
- if kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1;
757
- then echo PASS > "$OUT";
758
- else echo EMPTY > "$OUT"; exit 1; fi
759
-
760
810
  - id: social-write
761
811
  description: >-
762
- Author one short post per social platform (x, weibo, threads) from the approved plan, in zh,
763
- inside each platform's character cap. `daily-bundle-variants` truncates as a backstop, so a
764
- missed cap degrades rather than fails; a missing file simply skips that platform's variant.
812
+ Author one short post per social platform (x, weibo, threads, linkedin) from the approved
813
+ plan, in zh, inside each platform's character cap. `daily-bundle-variants` truncates as a
814
+ backstop, so a missed cap degrades rather than fails; a missing file simply skips that
815
+ platform's variant.
765
816
  dependsOn: [quality]
766
817
  action:
767
818
  kind: agent.run
@@ -771,10 +822,11 @@ nodes:
771
822
  agent: ${vars.agent}
772
823
  input: |
773
824
  Read the approved episode plan at ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json.
774
- Write three short zh posts, one per file:
825
+ Write four short zh posts, one per file:
775
826
  - ${vars.work_dir}/26-social/x.md — at most 280 weighted characters (this feed counts CJK as 2, so ~140 汉字 or 280 Latin letters)
776
827
  - ${vars.work_dir}/26-social/weibo.md — at most 2000 characters
777
828
  - ${vars.work_dir}/26-social/threads.md — at most 500 characters
829
+ - ${vars.work_dir}/26-social/linkedin.md — at most 3000 characters
778
830
  Each post: the day's single most interesting item, one concrete number or name from the
779
831
  plan, no hashtag spam, no emoji, no "点击链接", no invented facts. Plain text, no markdown
780
832
  headings. Deliver with your file-write tool; each file must be non-empty.
@@ -784,35 +836,83 @@ nodes:
784
836
  description: >-
785
837
  Build the channel payloads from the run's own artifacts (0327 builders): `30-variants/<key>`
786
838
  for article / episode / cards plus the channel-specific keys and the social posts, all with
787
- `{{article_url}}` / `{{podcast_url}}` placeholders because nothing is published yet.
788
- dependsOn: [quality, social-write]
839
+ `{{article_url}}` / `{{podcast_url}}` placeholders because nothing is published yet. 0329
840
+ F16: depends on every producer it reads (the 0324 file order alone was not a guarantee),
841
+ wipes stale variants first, and follows the cards/cover switches so a disabled producer's
842
+ variant is a skip, not a stale re-read.
843
+ dependsOn: [quality, social-write, publish-prep, cover-normalize, cards-render, pool-gate]
844
+ condition:
845
+ kind: shell
846
+ options:
847
+ # 0324 R3: the pool verdict is `pool-gate`'s token, never a repeated `kk store pool-count`.
848
+ command: 'test "$(cat ".spur/run/${vars.__runId}-pool-gate.status" 2>/dev/null)" = PASS'
789
849
  action:
790
850
  kind: shell
791
851
  options:
792
852
  command: >-
793
853
  kk stage run daily-bundle-variants "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
854
+ "${vars.cards_enabled}" "${vars.cover_enabled}"
855
+
856
+ - id: safety-variants
857
+ description: >-
858
+ Final-variant safety review through the same nested kk-daily-safety machine (0331): the
859
+ manifest stage registers every `30-variants/*.content.json` first, then the machine runs in
860
+ the variants scope (`safety_scope_dir=4-safety-variants` re-roots its stages, guards and
861
+ prompts; the drafts review above is untouched). On an admitted outcome the machine leaves
862
+ `4-safety-variants/safety.json` — status, exclusion record and the sha256 of every reviewed
863
+ variant's current bytes — which publish-matrix-bundle freezes into the bundle so live
864
+ launches and social edits are gated on exactly the reviewed bytes. Fails closed: a block or
865
+ an integrity failure fails this node (the drafts scope's non-zero = never publish contract).
866
+ dependsOn: [bundle-variants, pool-gate]
867
+ condition:
868
+ kind: shell
869
+ options:
870
+ # 0324 R3: the pool verdict is `pool-gate`'s token, never a repeated `kk store pool-count`.
871
+ command: 'test "$(cat ".spur/run/${vars.__runId}-pool-gate.status" 2>/dev/null)" = PASS'
872
+ action:
873
+ kind: shell
874
+ options:
875
+ command: >-
876
+ kk stage run daily-safety-manifest "${vars.work_dir}" "${vars.run_date}" &&
877
+ env -u SPUR_WORKFLOW_RUN_ACTIVE -u SPUR_EXPECTED_DEFINITION_DIGEST
878
+ spur workflow run plugins/kk/workflows/kk-daily-safety.yaml
879
+ --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}\",\"safety_scope_dir\":\"4-safety-variants\"}"
880
+ --quiet --no-plan
794
881
 
795
882
  - id: bundle-freeze
796
883
  description: >-
797
884
  Freeze `bundle.json` (0321 `BundleSchema`): every `30-variants/*` file with its sha256 plus the
798
885
  episode title, so the publish half can run later, or three times a day, against exactly these
799
886
  bytes. Fails when the article or episode variant is missing — a bundle never advertises what
800
- generate did not produce.
801
- dependsOn: [bundle-variants]
887
+ generate did not produce. 0329 F15: token-gated, so a refused episode never freezes — the
888
+ duration gate also drops this run's bundle.json on a refusal. 0331: after safety-variants, so
889
+ the freeze reads the review's evidence (or the bundle stays without it, on a skipped run).
890
+ dependsOn: [bundle-variants, safety-variants, pool-gate]
891
+ condition:
892
+ kind: shell
893
+ options:
894
+ # 0324 R3: the pool verdict is `pool-gate`'s token, never a repeated `kk store pool-count`.
895
+ command: 'test "$(cat ".spur/run/${vars.__runId}-pool-gate.status" 2>/dev/null)" = PASS'
802
896
  action:
803
897
  kind: shell
804
898
  options:
805
899
  command: >-
806
- kk stage run publish-matrix-bundle "${vars.work_dir}" --run-date "${vars.run_date}" --run-id "${vars.__runId}"
900
+ case "${vars.publish_preset}" in '') ARGS='' ;; *) ARGS="--preset ${vars.publish_preset}" ;; esac;
901
+ kk stage run publish-matrix-bundle "${vars.work_dir}" --run-date "${vars.run_date}" --run-id "${vars.__runId}" $ARGS
807
902
 
808
903
  - id: generation-report
809
- description: "The generate half's report (the publish half writes its own, per window)."
810
- dependsOn: [bundle-freeze]
904
+ description: "The generate half's report (the publish half writes its own, per window). 0329 F15: real run-report flags — the phantom skip flag made every invocation exit 1; the pool/candidates/qc inputs are what let the report record a no-content close. Depends on pool-gate beside bundle-freeze: pool-gate always completes, so the report (and the ledger after it) also land on a no-content close — the design requires the report reachable from extract/QC/no-content, not only bundle-freeze."
905
+ dependsOn: [bundle-freeze, pool-gate]
811
906
  action:
812
907
  kind: shell
813
908
  options:
814
909
  command: >-
815
- kk daily run-report --work-dir "${vars.work_dir}" --run-date "${vars.run_date}" --skip-store
910
+ kk daily run-report --work-dir "${vars.work_dir}" --run-date "${vars.run_date}"
911
+ --pool "${vars.work_dir}/10-collect/pool.json"
912
+ --candidates "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json"
913
+ --plugins-path "${vars.plugins_path}"
914
+ --qc-min-quality "${vars.qc_min_quality}" --qc-min-importance "${vars.qc_min_importance}"
915
+ --qc-min-urgency "${vars.qc_min_urgency}" --qc-min-impact "${vars.qc_min_impact}"
816
916
 
817
917
  - id: publish
818
918
  description: >-
@@ -820,18 +920,18 @@ nodes:
820
920
  window against the bundle this run just froze. The launcher vars are stripped at the nested
821
921
  boundary (the same two voice2 strips for the safety machine), and the nested run's exit code is
822
922
  surfaced — a failed publish window is a failed generate run's last step, never a silent one.
823
- dependsOn: [generation-report]
923
+ dependsOn: [generation-report, pool-gate]
824
924
  condition:
825
925
  kind: shell
826
926
  options:
827
- command: 'test "${vars.auto_publish}" = "true"'
927
+ command: 'test "${vars.auto_publish}" = "true" && test "$(cat ".spur/run/${vars.__runId}-pool-gate.status" 2>/dev/null)" = PASS'
828
928
  action:
829
929
  kind: shell
830
930
  options:
831
931
  command: >-
832
932
  env -u SPUR_WORKFLOW_RUN_ACTIVE -u SPUR_EXPECTED_DEFINITION_DIGEST
833
933
  spur workflow run plugins/kk/workflows/kk-daily-publish3.yaml
834
- --vars "{\"work_dir\":\"${vars.work_dir}\",\"run_date\":\"${vars.run_date}\",\"window\":\"${vars.publish_window}\",\"preset\":\"${vars.publish_preset}\",\"dummy\":\"${vars.publish_dummy}\"}"
934
+ --vars "{\"work_dir\":\"${vars.work_dir}\",\"run_date\":\"${vars.run_date}\",\"window\":\"${vars.publish_window}\",\"preset\":\"${vars.publish_preset}\",\"dummy\":\"${vars.publish_dummy}\",\"live_channels\":\"${vars.publish_live_channels}\"}"
835
935
 
836
936
  - id: ledger
837
937
  description: "Append this run to the workflow ledger; bookkeeping never fails a generation."
@@ -99,6 +99,18 @@ nodes:
99
99
  options:
100
100
  command: 'kk stage run publish-matrix-check "${vars.work_dir}" --window "${vars.window}" || true'
101
101
 
102
+ - id: validate
103
+ description: >-
104
+ Bind the bundle's calendar day before any side effect (0332 R1): normalize the run date
105
+ (compact `YYYYMMDD` and ISO `YYYY-MM-DD` both accepted), refuse a garbage one, and write the
106
+ canonical `4-publish/run-date.txt` every later stage derives its day from.
107
+ dependsOn: []
108
+ action:
109
+ kind: shell
110
+ options:
111
+ command: >-
112
+ kk stage run publish-matrix-validate "${vars.work_dir}" --run-date "${vars.run_date}"
113
+
102
114
  - id: send-confirm
103
115
  description: >-
104
116
  Operator approval of a LIVE send: `pause: true` stops the run and RESUME is the approval;
@@ -119,79 +131,121 @@ nodes:
119
131
  Receipts key every target, so a rerun never resends what was delivered, and a dummy row never
120
132
  spawns a plugin. `check` is listed beside `send-confirm` on purpose: the engine skips a node only
121
133
  when ALL its dependencies were skipped, so a draft window (no live row → `send-confirm` skipped)
122
- still sends, while a gated/blocked plan (both ancestors skipped) still stops here.
134
+ still sends, while a gated/blocked plan (both ancestors skipped) still stops here. A failing
135
+ target does NOT end the run here — `onError: continue` lets the bounded retry and the outcome
136
+ assertion decide, so the evidence (report, ledger, outcome) always survives to the operator.
123
137
  dependsOn: [check, send-confirm]
124
138
  action:
125
139
  kind: shell
140
+ onError: continue
126
141
  options:
127
142
  command: 'kk stage run publish-matrix-send "${vars.work_dir}" --window "${vars.window}"'
128
143
 
144
+ - id: send-retry
145
+ description: >-
146
+ The ONE bounded retry (0333): `--retry` resends only rows whose receipt is a live/draft failure
147
+ with `effect: none` — a succeeded row is skipped, a live row with an unknown effect is never
148
+ resent, and a dummy row never spawns a plugin. No unbounded back-edge: one retry node, then the
149
+ outcome assertion rules. A targeted operator rerun of this node is the recovery path for a
150
+ window that still ends non-full.
151
+ dependsOn: [send]
152
+ condition:
153
+ kind: shell
154
+ options:
155
+ command: 'test "${vars.window}" != "overseas-rss"'
156
+ action:
157
+ kind: shell
158
+ options:
159
+ command: 'kk stage run publish-matrix-send "${vars.work_dir}" --window "${vars.window}" --retry'
160
+
129
161
  - id: classify
130
162
  description: >-
131
163
  Turn the window's receipts into `full|partial|none|dummy` (`4-publish/outcome.<window>.json`),
132
- always exit 0 — routing is this graph's job, not the classifier's.
133
- dependsOn: [send]
164
+ always exit 0 — routing is this graph's job, not the classifier's. The run date comes from the
165
+ plans/`run-date.txt` (0332 R1), so a late `run_date` guard is gone; a provided one must match
166
+ the bundle's own day and the stage refuses a mismatch.
167
+ dependsOn: [send-retry, send]
134
168
  action:
135
169
  kind: shell
136
170
  options:
137
171
  command: >-
138
- test -n "${vars.run_date}" || { echo "publish3: classify needs run_date" >&2; exit 1; };
139
172
  kk stage run publish-matrix-classify "${vars.work_dir}" --window "${vars.window}" --run-date "${vars.run_date}"
140
173
 
141
174
  # ── after-generate only: the voice2-era tail stages keep working on the bridged names ─────
142
- - id: surfdash-patch
175
+ - id: patch-surfdash
143
176
  description: >-
144
- after-generate only: cover and podcast cross-link patching plus the CMS re-sync, whenever tier 0
145
- published something. Reads the legacy `31-publish` names the classifier bridged, so voice2's own
146
- stage body is reused unchanged.
177
+ after-generate only: cover and podcast cross-link patching plus the CMS re-sync, and only when
178
+ a real surfdash receipt exists. The payload is derived from that receipt's aggregate
179
+ `metadata.postPath` — never from legacy aggregate files or any site-root argument: the plugin
180
+ resolves its own checkout. A dummy or missing podcast episode yields
181
+ explicit empty links; there is nothing to patch without a real post. The stage is its own gate:
182
+ with no real tier-0 post evidence it records an explicit skip and exits 0 (the channel matrix
183
+ lives in the preset, so the graph cannot name channels here).
147
184
  dependsOn: [classify]
148
185
  condition:
149
186
  kind: shell
150
187
  options:
151
- command: >-
152
- test "${vars.window}" = "after-generate"
153
- && grep -qxE 'full|partial' "${vars.work_dir}/4-publish/publish-status.after-generate.txt" 2>/dev/null
188
+ command: 'test "${vars.window}" = "after-generate"'
154
189
 
155
190
  action:
156
191
  kind: shell
157
192
  options:
158
- command: >-
159
- kk stage run daily-patch-surfdash "${vars.work_dir}" "${vars.run_date}" "$KK_SURFDASH_ROOT" ""
193
+ command: 'kk stage run publish-matrix-patch-surfdash "${vars.work_dir}" --window "${vars.window}"'
160
194
 
161
195
  - id: partial-marker
162
- description: "after-generate only: record the partial-publish marker when the classifier said partial."
196
+ description: >-
197
+ Record the partial/none recovery marker when the classifier said so: names the exact failed
198
+ target ids from the receipt-derived outcome and points at the receipt-scoped rerun (`--retry`).
199
+ A full window deletes any stale marker, so the report never re-flags recovered targets.
163
200
  dependsOn: [classify]
164
201
  condition:
165
202
  kind: shell
166
203
  options:
167
204
  command: >-
168
- test "${vars.window}" = "after-generate"
169
- && grep -qx 'partial' "${vars.work_dir}/4-publish/publish-status.after-generate.txt" 2>/dev/null
205
+ grep -qx 'partial' "${vars.work_dir}/4-publish/outcome.${vars.window}.json" 2>/dev/null ||
206
+ grep -qx 'none' "${vars.work_dir}/4-publish/outcome.${vars.window}.json" 2>/dev/null
170
207
 
171
208
  action:
172
209
  kind: shell
173
210
  options:
174
- command: 'kk stage run daily-publish-partial "${vars.work_dir}" "${vars.run_date}"'
211
+ command: 'kk stage run publish-matrix-partial "${vars.work_dir}" --window "${vars.window}"'
175
212
 
176
- - id: mark-published
213
+ - id: settle
177
214
  description: >-
178
- after-generate only: mark the run's items published in the store once the owned platforms
179
- delivered. Keyed by the voice3 run id the bundle records, never by this publish run.
215
+ Promote the store selection only on receipt-proven completion (0333): every required real row
216
+ must carry a CURRENT-bundle receipt that is succeeded/confirmed/live — draft preparation and
217
+ dummy rehearsals HOLD the selection (no store call), never claim a publication. Writes
218
+ `4-publish/settlement.<window>.json` naming each unresolved row. Keyed to the bundle's
219
+ parentRunId, never to this publish run. Exit 0 while holding: the outcome assertion owns the
220
+ run's exit code, so this evidence file always survives.
180
221
  dependsOn: [classify]
181
222
  condition:
182
223
  kind: shell
183
224
  options:
184
- command: >-
185
- test "${vars.window}" = "after-generate"
186
- && grep -qxE 'full|partial' "${vars.work_dir}/4-publish/publish-status.after-generate.txt" 2>/dev/null
225
+ command: 'test "${vars.window}" != "overseas-rss"'
226
+ action:
227
+ kind: shell
228
+ options:
229
+ command: 'kk stage run publish-matrix-settle "${vars.work_dir}" --window "${vars.window}" --topic ai-news'
230
+
231
+ - id: outcome-assert
232
+ description: >-
233
+ The run's honest exit (0333): when an outcome exists, every required real row must be
234
+ succeeded — a partial window with failed required targets fails the run AFTER report and ledger
235
+ are written, with the outcome, receipts and incident evidence preserved. Gated-miss windows
236
+ (no outcome file) stay done: the diagnose/repair path already classified them.
237
+ dependsOn: [ledger]
238
+ condition:
239
+ kind: shell
240
+ options:
241
+ command: 'test -f "${vars.work_dir}/4-publish/outcome.${vars.window}.json"'
187
242
 
188
243
  action:
189
244
  kind: shell
190
245
  options:
191
246
  command: >-
192
- PARENT="$(jq -r '.parentRunId // .runId' "${vars.work_dir}/bundle.json" 2>/dev/null)";
193
- test -n "$PARENT" || { echo "publish3: bundle has no run id" >&2; exit 1; };
194
- kk store mark-published --topic ai-news --run "$PARENT"
247
+ jq -e 'all(.rows[]; .required != true or .dummy == true or .status == "succeeded")' "${vars.work_dir}/4-publish/outcome.${vars.window}.json" >/dev/null
248
+ || { echo "publish3: required targets failed — see outcome.${vars.window}.json and 5-report" >&2; exit 1; }
195
249
 
196
250
  # ── the incident path: a gated miss is diagnosed, never silently dropped ─────────────────
197
251
  - id: diagnose
@@ -270,6 +324,8 @@ nodes:
270
324
  printf '# Publish report — %s — %s\n\n- status: %s\n\n| target | tier | status | effect | dummy |\n| --- | --- | --- | --- | --- |\n'
271
325
  "${vars.window}" "${vars.run_date}" "$STATUS" > "$OUT";
272
326
  jq -r '.rows[] | "| \(.id) | \(.tier) | \(.status) | \(.effect // "-") | \(.dummy) |"'
327
+ "${vars.work_dir}/4-publish/outcome.${vars.window}.json" >> "$OUT" 2>/dev/null || true;
328
+ jq -r '.excluded[]? | "- excluded: \(.id) (\(.plugin)) — \(.reason)"'
273
329
  "${vars.work_dir}/4-publish/outcome.${vars.window}.json" >> "$OUT" 2>/dev/null || true
274
330
 
275
331
  - id: ledger