@gobing-ai/knowledge-kit 0.0.12 → 0.0.14

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 (152) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +120 -26
  3. package/package.json +1 -1
  4. package/plugins/generations/content-gen/dist/index.js +22187 -0
  5. package/plugins/generations/content-gen/plugin.json +1 -1
  6. package/plugins/generations/core-facts-gen/dist/index.js +22068 -0
  7. package/plugins/generations/core-facts-gen/plugin.json +1 -1
  8. package/plugins/generations/daily-article-gen/dist/index.js +22050 -0
  9. package/plugins/generations/daily-article-gen/plugin.json +1 -1
  10. package/plugins/generations/daily-article-gen/src/index.ts +13 -1
  11. package/plugins/generations/dailynews-gen/dist/index.js +22344 -0
  12. package/plugins/generations/dailynews-gen/plugin.json +1 -1
  13. package/plugins/generations/episode-plan-gen/dist/index.js +22503 -0
  14. package/plugins/generations/episode-plan-gen/plugin.json +1 -1
  15. package/plugins/generations/episode-plan-gen/src/index.ts +11 -0
  16. package/plugins/generations/image-gen/config.example.yaml +75 -0
  17. package/plugins/generations/image-gen/dist/index.js +24862 -0
  18. package/plugins/generations/image-gen/package.json +17 -0
  19. package/plugins/generations/image-gen/plugin.json +7 -0
  20. package/plugins/generations/image-gen/presets/formats/cover.yaml +57 -0
  21. package/plugins/generations/image-gen/presets/formats/free.yaml +46 -0
  22. package/plugins/generations/image-gen/presets/formats/illustration.yaml +48 -0
  23. package/plugins/generations/image-gen/presets/styles/clean-webapp-ui.yaml +28 -0
  24. package/plugins/generations/image-gen/presets/styles/cute.yaml +3 -0
  25. package/plugins/generations/image-gen/presets/styles/editorial.yaml +3 -0
  26. package/plugins/generations/image-gen/presets/styles/fresh.yaml +3 -0
  27. package/plugins/generations/image-gen/presets/styles/minimalist.yaml +3 -0
  28. package/plugins/generations/image-gen/presets/styles/photorealistic.yaml +3 -0
  29. package/plugins/generations/image-gen/presets/styles/sketch.yaml +3 -0
  30. package/plugins/generations/image-gen/presets/styles/technical-diagram.yaml +3 -0
  31. package/plugins/generations/image-gen/presets/styles/vibrant.yaml +3 -0
  32. package/plugins/generations/image-gen/presets/styles/warm.yaml +3 -0
  33. package/plugins/generations/image-gen/src/bytes.ts +19 -0
  34. package/plugins/generations/image-gen/src/index.ts +319 -0
  35. package/plugins/generations/image-gen/src/job.ts +143 -0
  36. package/plugins/generations/image-gen/src/paths.ts +31 -0
  37. package/plugins/generations/image-gen/src/presets.ts +344 -0
  38. package/plugins/generations/image-gen/src/providers/agnes.ts +110 -0
  39. package/plugins/generations/image-gen/src/providers/azure.ts +153 -0
  40. package/plugins/generations/image-gen/src/providers/codex-cli.ts +170 -0
  41. package/plugins/generations/image-gen/src/providers/dashscope.ts +485 -0
  42. package/plugins/generations/image-gen/src/providers/google.ts +268 -0
  43. package/plugins/generations/image-gen/src/providers/huggingface.ts +59 -0
  44. package/plugins/generations/image-gen/src/providers/jimeng.ts +259 -0
  45. package/plugins/generations/image-gen/src/providers/minimax.ts +171 -0
  46. package/plugins/generations/image-gen/src/providers/openai.ts +319 -0
  47. package/plugins/generations/image-gen/src/providers/openrouter.ts +257 -0
  48. package/plugins/generations/image-gen/src/providers/refs.ts +24 -0
  49. package/plugins/generations/image-gen/src/providers/replicate.ts +279 -0
  50. package/plugins/generations/image-gen/src/providers/seedream.ts +128 -0
  51. package/plugins/generations/image-gen/src/providers/types.ts +286 -0
  52. package/plugins/generations/image-gen/src/providers/zai.ts +237 -0
  53. package/plugins/generations/image-gen/tsconfig.json +8 -0
  54. package/plugins/generations/news-report-gen/dist/index.js +22193 -0
  55. package/plugins/generations/news-report-gen/package.json +17 -0
  56. package/plugins/generations/news-report-gen/plugin.json +7 -0
  57. package/plugins/generations/news-report-gen/src/index.ts +308 -0
  58. package/plugins/generations/news-report-gen/tsconfig.json +4 -0
  59. package/plugins/generations/omni-voice-gen/Makefile +14 -0
  60. package/plugins/generations/omni-voice-gen/README.md +112 -0
  61. package/plugins/generations/omni-voice-gen/bin/omni-voice-gen +2 -0
  62. package/plugins/generations/omni-voice-gen/dist/omni-voice-gen-prr8skpb. +2 -0
  63. package/plugins/generations/omni-voice-gen/dist/omni-voice-gen.js +6 -0
  64. package/plugins/generations/omni-voice-gen/plugin.json +6 -0
  65. package/plugins/generations/omni-voice-gen/profiles.json +12 -0
  66. package/plugins/generations/omni-voice-gen/pyproject.toml +25 -0
  67. package/plugins/generations/omni-voice-gen/scripts/coverage_gate.py +74 -0
  68. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__init__.py +1 -0
  69. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__main__.py +39 -0
  70. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/audio.py +190 -0
  71. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/backend.py +150 -0
  72. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/contract.py +76 -0
  73. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/mp3.py +60 -0
  74. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/pipeline.py +289 -0
  75. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/profiles.py +100 -0
  76. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/qc.py +234 -0
  77. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/voicescript.py +352 -0
  78. package/plugins/generations/omni-voice-gen/uv.lock +3510 -0
  79. package/plugins/generations/voice-gen/dist/index.js +23055 -0
  80. package/plugins/generations/voice-gen/plugin.json +1 -1
  81. package/plugins/generations/voice-gen/src/index.ts +16 -1
  82. package/plugins/generations/voice-gen/src/voicebox-client.ts +3 -1
  83. package/plugins/ingestions/aihot-ingest/dist/index.js +22378 -0
  84. package/plugins/ingestions/aihot-ingest/plugin.json +1 -1
  85. package/plugins/ingestions/horizon-ingest/dist/index.js +22125 -0
  86. package/plugins/ingestions/horizon-ingest/plugin.json +1 -1
  87. package/plugins/ingestions/karakeep-local/dist/index.js +24204 -0
  88. package/plugins/ingestions/karakeep-local/plugin.json +1 -1
  89. package/plugins/ingestions/last30days-ingest/dist/index.js +22070 -0
  90. package/plugins/ingestions/last30days-ingest/plugin.json +1 -1
  91. package/plugins/ingestions/web-search/dist/index.js +24399 -0
  92. package/plugins/ingestions/web-search/plugin.json +1 -1
  93. package/plugins/kk/commands/image-extract.md +40 -0
  94. package/plugins/kk/commands/image-generate.md +32 -0
  95. package/plugins/kk/config.example.yaml +80 -0
  96. package/plugins/kk/plugin.json +1 -1
  97. package/plugins/kk/skills/image-authoring/SKILL.md +257 -0
  98. package/plugins/kk/skills/image-authoring/references/format-drafting.md +57 -0
  99. package/plugins/kk/skills/image-authoring/references/illustration-positions.md +87 -0
  100. package/plugins/kk/skills/image-authoring/references/migrating-from-wt.md +31 -0
  101. package/plugins/kk/skills/image-authoring/references/providers.md +52 -0
  102. package/plugins/kk/skills/image-authoring/references/style-extraction.md +139 -0
  103. package/plugins/kk/workflows/kk-daily-ai-voice.yaml +130 -30
  104. package/plugins/publishings/emdash-pub/dist/index.js +22263 -0
  105. package/plugins/publishings/emdash-pub/plugin.json +1 -1
  106. package/plugins/publishings/podcast-pub/dist/index.js +22650 -0
  107. package/plugins/publishings/podcast-pub/plugin.json +8 -2
  108. package/plugins/publishings/podcast-pub/src/index.ts +18 -2
  109. package/plugins/publishings/podcast-pub/src/show-notes.ts +56 -9
  110. package/plugins/publishings/qiita-pub/dist/index.js +22101 -0
  111. package/plugins/publishings/qiita-pub/plugin.json +1 -1
  112. package/plugins/publishings/surfdash-pub/dist/index.js +22323 -0
  113. package/plugins/publishings/surfdash-pub/plugin.json +1 -1
  114. package/plugins/publishings/surfdash-pub/src/index.ts +109 -9
  115. package/plugins/publishings/zenn-pub/dist/index.js +22142 -0
  116. package/plugins/publishings/zenn-pub/plugin.json +1 -1
  117. package/plugins/sp/scripts/batch-preflight.mjs +346 -0
  118. package/plugins/sp/scripts/batch-preflight.ts +459 -0
  119. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +615 -0
  120. package/plugins/sp/scripts/daily-summary/daily-summary.ts +846 -0
  121. package/plugins/sp/scripts/daily-summary/logger.ts +28 -0
  122. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +223 -0
  123. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +367 -0
  124. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +132 -0
  125. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +169 -0
  126. package/plugins/sp/scripts/feature-dev-precheck.mjs +171 -0
  127. package/plugins/sp/scripts/feature-dev-precheck.ts +238 -0
  128. package/plugins/sp/scripts/feature-sync-bounded.mjs +285 -0
  129. package/plugins/sp/scripts/feature-sync-bounded.ts +478 -0
  130. package/plugins/sp/scripts/history-anatomy-cache.mjs +902 -0
  131. package/plugins/sp/scripts/history-anatomy-cache.ts +1028 -0
  132. package/plugins/sp/scripts/idea-handoff.mjs +22 -0
  133. package/plugins/sp/scripts/idea-handoff.ts +44 -0
  134. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +185 -0
  135. package/plugins/sp/scripts/inline-run-setup.ts +198 -0
  136. package/plugins/sp/scripts/pr-reviewing.mjs +769 -0
  137. package/plugins/sp/scripts/pr-reviewing.ts +925 -0
  138. package/plugins/sp/scripts/quality-gate.mjs +179 -0
  139. package/plugins/sp/scripts/quality-gate.ts +217 -0
  140. package/plugins/sp/scripts/script-contract-check.ts +319 -0
  141. package/plugins/sp/scripts/stage-registry-adapter.ts +1533 -0
  142. package/plugins/sp/scripts/surface-drift-inventory.ts +929 -0
  143. package/plugins/sp/scripts/task-evidence-precheck.ts +181 -0
  144. package/plugins/sp/scripts/task-size-precheck.ts +175 -0
  145. package/plugins/sp/scripts/transition-shim-check.ts +238 -0
  146. package/plugins/sp/scripts/validate-commands.ts +689 -0
  147. package/plugins/sp/scripts/validate-flag-contracts.ts +878 -0
  148. package/plugins/sp/scripts/verify-answer-lint.ts +530 -0
  149. package/plugins/sp/scripts/workflow-step-profile.mjs +316 -0
  150. package/plugins/sp/scripts/workflow-step-profile.ts +456 -0
  151. package/plugins/sp/scripts/wrapup-steps.mjs +373 -0
  152. package/plugins/sp/scripts/wrapup-steps.ts +466 -0
@@ -0,0 +1,52 @@
1
+ # Provider facts for image generation
2
+
3
+ The operator-facing digest of 0103's provider inventory. Source of truth:
4
+ `plugins/generations/image-gen/src/providers/types.ts` (the frozen registry) —
5
+ if this file and the registry ever disagree, the registry wins and this file
6
+ gets fixed. Credential *values* live only in
7
+ `plugins.generations.image-gen.env` (`plugins/kk/config.example.yaml`); they
8
+ are read by `invoke.ts` at spawn, never by this skill.
9
+
10
+ ## The rules
11
+
12
+ - **Readiness is env presence.** A provider is available when its env var(s)
13
+ are non-empty. A stale key still passes readiness and surfaces as the
14
+ adapter's own auth error at generate time.
15
+ - **Auto-selection** walks `AUTO_ORDER` — google, openai, replicate,
16
+ openrouter, dashscope, seedream, zai, minimax, jimeng, azure, agnes — and
17
+ takes the first available. `IMAGE_GEN_PROVIDER` names a provider explicitly;
18
+ `--provider` outranks it.
19
+ - **`codex-cli` is never auto-selected.** Its readiness (the `codex` CLI on
20
+ PATH with an active login) is not env-expressible; pin it explicitly.
21
+ - **Reference images** ride `--ref` only when `supportsReferenceImages(model)`
22
+ is true for the resolved model — a substring test over the registry's
23
+ `refModels` fragments (no `refModels` = every model; empty list = none).
24
+ When it is false, say so and offer `extract-style`; never drop the
25
+ reference silently.
26
+
27
+ ## The inventory
28
+
29
+ | Provider | Env var(s) that enable it | Default model | Reference images |
30
+ | --- | --- | --- | --- |
31
+ | agnes | `AGNES_API_KEY` | `agnes-image-2.1-flash` | yes |
32
+ | azure | `AZURE_OPENAI_API_KEY` (+ `AZURE_OPENAI_BASE_URL`, `AZURE_OPENAI_DEPLOYMENT` to run) | `gpt-image-2` | yes (no `refModels` declared → every model passes `supportsReferenceImages`) |
33
+ | codex-cli | not env-expressible — `codex` CLI + login | `codex-image-gen` | never auto-selected |
34
+ | dashscope | `DASHSCOPE_API_KEY` | `qwen-image-2.0-pro` | only `wan2.7` models |
35
+ | google | `GOOGLE_API_KEY` or `GEMINI_API_KEY` | `gemini-3-pro-image` | the gemini image models (`gemini-3-pro-image`, `*-flash-image`, previews) |
36
+ | jimeng | `JIMENG_ACCESS_KEY_ID` **and** `JIMENG_SECRET_ACCESS_KEY` (both halves) | `jimeng_t2i_v40` | yes |
37
+ | minimax | `MINIMAX_API_KEY` | `image-01` | yes (jpg/png <10MB, subject reference) |
38
+ | openai | `OPENAI_API_KEY` | `gpt-image-2` | `gpt-image` models |
39
+ | openrouter | `OPENROUTER_API_KEY` | `google/gemini-3.1-flash-image` | yes (no `refModels` declared → treated as supporting; real capability depends on the routed model) |
40
+ | replicate | `REPLICATE_API_TOKEN` | `google/nano-banana-2` | yes |
41
+ | seedream | `ARK_API_KEY` (not `SEEDREAM_*`) | `doubao-seedream-5-0-260128` | `doubao-seedream-5-0` / `4-5-` / `4-0-` models |
42
+ | zai | `ZAI_API_KEY` or `BIGMODEL_API_KEY` | `glm-image` | **none** |
43
+
44
+ ## Deciding with references in hand
45
+
46
+ 1. Resolve the provider per Preference chain (argument → `IMAGE_GEN_PROVIDER`
47
+ → config → `AUTO_ORDER`).
48
+ 2. Check `supportsReferenceImages` for the resolved model. False → the
49
+ pass-through path is unavailable: state it and offer style extraction
50
+ (0105), which works with every provider.
51
+ 3. Name the chosen provider and model in the spend preview — the operator
52
+ approves the pair, not just the prompt.
@@ -0,0 +1,139 @@
1
+ # Style extraction from reference images
2
+
3
+ Loaded by the image-authoring skill when the operator hands over one or more
4
+ reference images and wants the look captured as a reusable style preset.
5
+ Extraction is a **skill operation** — the running agent reads the image(s) with
6
+ its own vision and writes a YAML file. No plugin code, no vision API call, no
7
+ provider credential is involved.
8
+
9
+ There are two mechanisms for using a reference image, and this file owns the
10
+ boundary between them:
11
+
12
+ | baoyu usage | kk mechanism | What happens |
13
+ | --- | --- | --- |
14
+ | `direct` | pass-through | The file rides `ImageJobSpec.referenceImages` (0104) to a provider whose `supportsReferenceImages(model)` is true (0103). Highest fidelity; produces one image, nothing reusable. |
15
+ | `style` | extraction (this file) | The look is analysed and written as a `styles/<name>.yaml`. Works with every provider; reusable and editable. |
16
+ | `palette` | extraction, partial | Same procedure, but only `palette`, `name` and `description` are filled; `modifiers` stays a one-line summary. |
17
+
18
+ ## Where the preset goes
19
+
20
+ Write to the **user root**: `~/.config/kk/image-presets/styles/<name>.yaml`. A cloned
21
+ look is usually the operator's, not the repository's. An operator who wants the
22
+ preset shared with a team writes to the project root
23
+ (`<KNOWLEDGE_KIT_PROJECT_ROOT>/.kk/image-presets/styles/`) instead — it ranks
24
+ higher in the loader chain. No registration step exists or is needed: the
25
+ loader discovers presets by scanning these directories (0102). If the chosen
26
+ name matches a shipped preset, the new file **shadows** it whole-file and the
27
+ loader emits a `shadowed` diagnostic naming both paths — that is the intended
28
+ way to adjust a shipped style, not an accident.
29
+
30
+ ## Input rules
31
+
32
+ - **One to five reference images.** Count them before reading anything.
33
+ - **Cap refusal (exact wording):** if the operator supplies more than 5, refuse
34
+ before reading any of them:
35
+ `Refusing extraction: <N> reference images requested; the cap is 5. Split the set into coherent groups, or drop the least representative references and re-run.`
36
+ The cap is a constant, not a design commitment; changing it means changing
37
+ this number and this message only.
38
+
39
+ ## Output rules — the schema is frozen
40
+
41
+ Emit **YAML only** — no prose, no code fence. The document fills **exactly**
42
+ the `StylePreset` fields of `plugins/generations/image-gen/src/presets.ts` and
43
+ nothing else. **Inventing a field outside this list is a defect, not an
44
+ enhancement.** Field-by-field:
45
+
46
+ | Field | Required | Rule |
47
+ | --- | --- | --- |
48
+ | `name` | yes | kebab-case, derived from the look, never from the filename. |
49
+ | `description` | yes | One or two sentences naming the look and where it works. |
50
+ | `modifiers` | yes | A comma-separated prompt fragment in the same voice as the shipped styles under `plugins/generations/image-gen/presets/styles/` — `composePrompt` concatenates it as segment 3 of a comma-joined prompt. Read two shipped styles before writing one; a style in the wrong voice produces a prompt in two voices. |
51
+ | `palette` | no | 3–6 entries of `{role, name, hex}`. `role` from `{primary, secondary, accent, background, text}`. `hex` uppercase `#RRGGBB`, sampled from the image, never invented. (A merged set may legitimately keep fewer than 3 — only colours common to every reference survive; see merge rules.) |
52
+ | `typography` | no | Only when the image evidences it. Omit rather than guess. |
53
+ | `visualElements` | no | Array of strings naming recurring visual devices (borders, banding, grain, iconography). Only what the image shows. |
54
+ | `bestFor` | no | The content types this look suits. Inferred from the image's own subject matter. |
55
+ | `negative` | no | What this look must avoid — inferred from what is conspicuously absent. |
56
+ | `source` | yes for extraction | `kind: extracted`, `refImages` = the absolute input paths **in the order given**, `extractedAt` = ISO-8601 timestamp, `confidence` = the judgement below. |
57
+
58
+ ### The `confidence` decision rule (human-judged, not computed)
59
+
60
+ - `high` — a single clean image with an obvious, consistent look.
61
+ - `medium` — mixed or noisy references, or a merged set where the references
62
+ disagree on any of the four fidelity axes.
63
+ - `low` — the look is ambiguous, or the image is photographic content rather
64
+ than a designed style (a photo *of* something is not a style).
65
+
66
+ ## Multi-image merge rules
67
+
68
+ With N references (1 ≤ N ≤ 5) the output is **one merged preset**, not N
69
+ presets and not an average:
70
+
71
+ - `palette` — colours **common to the set**. A colour appearing in one image
72
+ only is dropped, not averaged into a muddy midpoint.
73
+ - `modifiers` — traits present in **every** image. Divergent traits are
74
+ dropped, and each discarded trait is **named in the extraction report** so
75
+ the operator sees exactly what was thrown away.
76
+ - `confidence` — capped at `medium` when the set disagrees on any fidelity axis.
77
+ - `source.refImages` — all N absolute paths, in the order given.
78
+
79
+ ## What extraction writes
80
+
81
+ **Style by default; format drafting is opt-in.** A `FormatPreset` carries `template`,
82
+ `variables`, `keywords`, `defaultStyle`, `outputFilename` — none of which is
83
+ *recoverable* from a picture, so extraction alone never writes a `formats/*.yaml`.
84
+ What an image *does* yield is arithmetic: the extraction report **states the observed
85
+ pixel dimensions and the nearest named aspect ratio** of each reference, chosen from
86
+ `21:9`, `4:3`, `1:1`, `16:9`, `3:4`, `9:16`.
87
+
88
+ When the operator passes `--emit-format <name> --job <job>` alongside `--extract-style`,
89
+ that arithmetic plus the same visual analysis feeds a **draft** format preset — measured
90
+ fields taken, visual defaults authored, structural fields reused from the shipped
91
+ skeleton, operator fine-tuning expected. That procedure lives in
92
+ [format-drafting.md](format-drafting.md); this file stays style-only.
93
+
94
+ ## Pass-through degradation (visible, never silent)
95
+
96
+ When the operator wants `direct` (pass-through) and the selected provider's
97
+ `supportsReferenceImages(model)` returns `false`, the skill must state it in
98
+ these terms and offer the alternative:
99
+
100
+ `Provider <name> cannot accept reference images for model <model> (supportsReferenceImages: false). Pass-through is unavailable; offering style extraction instead.`
101
+
102
+ **Never silently proceed without the reference.** A plausible image generated
103
+ from a dropped reference is the worst outcome available — worse than an error.
104
+
105
+ ## Validate after writing
106
+
107
+ After writing the YAML, run the dry run: `KK_IMAGE_DRY_RUN=1` `image-gen`
108
+ (0104) with a job whose `style` is the new preset's name. A malformed or
109
+ unloadable file fails there loudly, naming the file and the schema violation,
110
+ before any provider spend. No validator is written; 0102's loader and the dry
111
+ run already are one.
112
+
113
+ ## Fidelity check (v1 method: human eyeball)
114
+
115
+ To answer "did the clone work": extract from reference **I**, regenerate with
116
+ the extracted preset using a prompt describing **I's own subject** (so subject
117
+ variance does not contaminate the comparison), then compare **I** and the
118
+ regeneration on exactly four axes — **palette, rendering technique,
119
+ composition density, typographic treatment**. The v1 comparison method is a
120
+ human eyeballing the pair; no metric is computed. Record the verdict in the
121
+ preset's `source.confidence`; if the fidelity verdict disagrees with the
122
+ extraction-side judgement, take the lower.
123
+
124
+ ## Worked output shape
125
+
126
+ ```yaml
127
+ name: example-look
128
+ description: Flat colour-band composition with deep teal over warm cream.
129
+ modifiers: flat colour-band composition, deep teal and warm cream palette, hard edges, no gradients, minimal detail
130
+ palette:
131
+ - { role: primary, name: deep teal, hex: '#1D5C63' }
132
+ - { role: background, name: warm cream, hex: '#F5EFE0' }
133
+ source:
134
+ kind: extracted
135
+ refImages:
136
+ - /absolute/path/to/reference.png
137
+ extractedAt: '2026-09-09T18:00:00Z'
138
+ confidence: high
139
+ ```
@@ -19,6 +19,7 @@ vars:
19
19
  limit: "10"
20
20
  language: "zh"
21
21
  voice_profile: "robin-news"
22
+ voice_generator: "voice-gen"
22
23
  title: ""
23
24
  cursor: ""
24
25
  state_file: ""
@@ -54,6 +55,7 @@ states:
54
55
  - kind: shell
55
56
  options:
56
57
  command: >-
58
+ __STAMP_T0="$(date -u +%FT%TZ)"; trap '[ -n "${vars.work_dir}" ] && printf "{\"step\":\"prepare\",\"startedAt\":\"%s\",\"endedAt\":\"%s\"}\n" "$__STAMP_T0" "$(date -u +%FT%TZ)" >> "${vars.work_dir}/step-timing.json"' EXIT;
57
59
  test -n "${vars.work_dir}" || { echo "missing required variable: work_dir" >&2 && exit 1; };
58
60
  mkdir -p "${vars.work_dir}/1-ingest" "${vars.work_dir}/2-facts" "${vars.work_dir}/2-plan" "${vars.work_dir}/2-article" "${vars.work_dir}/2-cover" "${vars.work_dir}/2-script" "${vars.work_dir}/3-audio" "${vars.work_dir}/3-publish";
59
61
  date +%Y%m%d > "${vars.work_dir}/run-date.txt" &&
@@ -75,11 +77,13 @@ states:
75
77
  # fan-in 30m per-source default and making empty episodes stochastic — horizon gets a
76
78
  # 45m leash; aihot/last30days keep the default.
77
79
  command: >-
80
+ __STAMP_T0="$(date -u +%FT%TZ)"; trap '[ -n "${vars.work_dir}" ] && printf "{\"step\":\"collect\",\"startedAt\":\"%s\",\"endedAt\":\"%s\"}\n" "$__STAMP_T0" "$(date -u +%FT%TZ)" >> "${vars.work_dir}/step-timing.json"' EXIT;
78
81
  if [ -f "${vars.work_dir}/1-ingest/blended.json" ]; then echo "collect cached: ${vars.work_dir}/1-ingest/blended.json"; exit 0; fi;
79
82
  kk executor fan-in --source "aihot-ingest:${vars.work_dir}/1-ingest/source.json" --source "last30days-ingest:${vars.work_dir}/1-ingest/last30days.json" --source "horizon-ingest:${vars.work_dir}/1-ingest/horizon.json" --source-timeout-override "horizon-ingest:2700000" --out "${vars.work_dir}/1-ingest/blended.json"
80
83
  - kind: shell
81
84
  options:
82
85
  command: >-
86
+ __STAMP_T0="$(date -u +%FT%TZ)"; trap '[ -n "${vars.work_dir}" ] && printf "{\"step\":\"collect\",\"startedAt\":\"%s\",\"endedAt\":\"%s\"}\n" "$__STAMP_T0" "$(date -u +%FT%TZ)" >> "${vars.work_dir}/step-timing.json"' EXIT;
83
87
  set --;
84
88
  if [ -n "${vars.plugins_path}" ]; then set -- --plugins-path "${vars.plugins_path}"; fi;
85
89
  if [ -f "${vars.work_dir}/2-facts/content.json" ] && [ -f "${vars.work_dir}/2-facts/core-facts.md" ]; then echo "core facts cached: ${vars.work_dir}/2-facts/core-facts.md"; exit 0; fi;
@@ -92,6 +96,7 @@ states:
92
96
  - kind: shell
93
97
  options:
94
98
  command: >-
99
+ __STAMP_T0="$(date -u +%FT%TZ)"; trap '[ -n "${vars.work_dir}" ] && printf "{\"step\":\"plan\",\"startedAt\":\"%s\",\"endedAt\":\"%s\"}\n" "$__STAMP_T0" "$(date -u +%FT%TZ)" >> "${vars.work_dir}/step-timing.json"' EXIT;
95
100
  set --;
96
101
  if [ -n "${vars.plugins_path}" ]; then set -- --plugins-path "${vars.plugins_path}"; fi;
97
102
  PLAN_CONTENT="${vars.work_dir}/2-plan/content.json"; PLAN_DOCS="${vars.work_dir}/2-plan/plan.json";
@@ -106,6 +111,7 @@ states:
106
111
  - kind: shell
107
112
  options:
108
113
  command: >-
114
+ __STAMP_T0="$(date -u +%FT%TZ)"; trap '[ -n "${vars.work_dir}" ] && printf "{\"step\":\"quality-control-content\",\"startedAt\":\"%s\",\"endedAt\":\"%s\"}\n" "$__STAMP_T0" "$(date -u +%FT%TZ)" >> "${vars.work_dir}/step-timing.json"' EXIT;
109
115
  set --;
110
116
  if [ -n "${vars.plugins_path}" ]; then set -- --plugins-path "${vars.plugins_path}"; fi;
111
117
  if [ -f "${vars.work_dir}/2-plan/candidates.json" ]; then echo "candidates cached: ${vars.work_dir}/2-plan/candidates.json"; exit 0; fi;
@@ -136,23 +142,30 @@ states:
136
142
  - kind: shell
137
143
  options:
138
144
  command: >-
145
+ __STAMP_T0="$(date -u +%FT%TZ)"; trap '[ -n "${vars.work_dir}" ] && printf "{\"step\":\"article\",\"startedAt\":\"%s\",\"endedAt\":\"%s\"}\n" "$__STAMP_T0" "$(date -u +%FT%TZ)" >> "${vars.work_dir}/step-timing.json"' EXIT;
139
146
  set --;
140
147
  if [ -n "${vars.plugins_path}" ]; then set -- --plugins-path "${vars.plugins_path}"; fi;
141
148
  CONTENT_OUT="${vars.work_dir}/2-article/content-${vars.run_date}.json"; ARTICLE_MD="${vars.work_dir}/2-article/article-${vars.run_date}.md";
142
149
  if [ -f "$ARTICLE_MD" ]; then echo "article cached: $ARTICLE_MD"; else PLAN_IN="${vars.work_dir}/2-plan/plan.json"; PLAN_OK=1; bun -e 'try { JSON.parse(await Bun.file(process.argv[1]).text()); process.exit(0); } catch (e) { console.warn("article: plan.json invalid JSON — replacing with untranslated plan (downstream states read this file): " + e.message); const docs = JSON.parse(await Bun.file(process.argv[2]).text()).metadata?.docs; if (!Array.isArray(docs) || docs.length === 0) { console.error("article: candidates.json metadata.docs missing"); process.exit(2); } await Bun.write(process.argv[1], JSON.stringify(docs, null, 2)); process.exit(1); }' "$PLAN_IN" "${vars.work_dir}/2-plan/candidates.json"; case $? in 0|1) PLAN_OK=0;; esac; test "$PLAN_OK" -eq 0 || { echo "article: no usable plan input"; exit 1; }; ARTICLE_DATE="${vars.run_date}" kk executor run daily-article-gen --in "$PLAN_IN" --out "$CONTENT_OUT" "$@" && bun -e 'await Bun.write(process.argv[2], (await Bun.file(process.argv[1]).json()).body)' "$CONTENT_OUT" "$ARTICLE_MD"; fi
143
150
 
144
151
  - id: cover
145
- description: "Generate the article cover image via the wt-image-cover skill (agent.run)"
152
+ description: "Generate the article cover image via the kk:image-authoring skill (agent.run)"
146
153
  onEnter:
147
154
  - kind: agent.run
148
155
  options:
149
156
  role: scribe
150
157
  agent: ${vars.agent}
151
158
  input: |
152
- Invoke the wt-image-cover skill.
153
- Generate a 21:9 cover image for the article at ${vars.work_dir}/2-article/article-${vars.run_date}.md and save it to ${vars.work_dir}/2-cover/cover-${vars.run_date}.png.
154
- Use style "news". If no image-generation backend is available, a procedurally rendered cover (e.g. PIL: news palette, headline typography) is an acceptable, expected deliverable — not a fake; generate it and write the file. Do not wait for operator confirmation.
155
- expectFile: ${vars.work_dir}/2-cover/cover-${vars.run_date}.png
159
+ Invoke the kk:image-authoring skill (generate operation).
160
+ This node is a non-interactive workflow step: run every image command with KK_NONINTERACTIVE=1 and KK_IMAGE_AUTO_CONFIRM=1 in the environment (this workflow approves the image spend via cover_enabled) — never prompt, never open a question.
161
+ Command: --format cover --content ${vars.work_dir}/2-article/article-${vars.run_date}.md --out ${vars.work_dir}/2-cover
162
+ Derive the cover template variables (title, subtitle, topics) from the article as the skill owns; style is the format's default — do not pass --style.
163
+ The plugin writes ${vars.work_dir}/2-cover/content.json plus the artifact ${vars.work_dir}/2-cover/content-cover-01.<ext>, where <ext> follows the elected provider's native output (payload honesty: minimax returns JPEG, google/ark return PNG). If generation fails, report the error verbatim and stop — no procedural fallback.
164
+ Run 20260911 context: only MINIMAX_API_KEY is credentialed (ARK out of token, google key unset), so the elected provider is minimax and the artifact lands as content-cover-01.jpg.
165
+ expectFile: ${vars.work_dir}/2-cover/content-cover-01.jpg
166
+ - kind: shell
167
+ options:
168
+ command: 'cd "${vars.work_dir}/2-cover" && src="content-cover-01.png"; test -f "$src" || src="content-cover-01.jpg"; test -f "$src" || { echo "cover normalize: no content-cover-01.{png,jpg} artifact" >&2; exit 1; }; case "$src" in *.jpg) command -v sips >/dev/null 2>&1 || { echo "cover normalize: sips not found; cannot transcode jpg -> png" >&2; exit 1; }; sips -s format png "$src" --out "cover-${vars.run_date}.png" >/dev/null;; *) mv "$src" "cover-${vars.run_date}.png";; esac; rm -f content-cover-01.jpg content-cover-01.png; echo "cover normalized: cover-${vars.run_date}.png"'
156
169
 
157
170
  - id: script
158
171
  description: "Compile the episode plan Doc[] into structured broadcast VoiceScript YAML Content"
@@ -160,6 +173,7 @@ states:
160
173
  - kind: shell
161
174
  options:
162
175
  command: >-
176
+ __STAMP_T0="$(date -u +%FT%TZ)"; trap '[ -n "${vars.work_dir}" ] && printf "{\"step\":\"script\",\"startedAt\":\"%s\",\"endedAt\":\"%s\"}\n" "$__STAMP_T0" "$(date -u +%FT%TZ)" >> "${vars.work_dir}/step-timing.json"' EXIT;
163
177
  set --;
164
178
  if [ -n "${vars.plugins_path}" ]; then set -- --plugins-path "${vars.plugins_path}"; fi;
165
179
  SCRIPT_CONTENT_OUT="${vars.work_dir}/2-script/content-${vars.run_date}.json"; VOICESCRIPT_YAML="${vars.work_dir}/2-script/voicescript-${vars.run_date}.yaml";
@@ -180,18 +194,20 @@ states:
180
194
  - kind: shell
181
195
  options:
182
196
  command: >-
197
+ __STAMP_T0="$(date -u +%FT%TZ)"; trap '[ -n "${vars.work_dir}" ] && printf "{\"step\":\"wrap-docs\",\"startedAt\":\"%s\",\"endedAt\":\"%s\"}\n" "$__STAMP_T0" "$(date -u +%FT%TZ)" >> "${vars.work_dir}/step-timing.json"' EXIT;
183
198
  bun -e 'const scriptUrl = new URL(process.argv[1], "file://" + process.cwd() + "/"); const scriptPath = decodeURIComponent(scriptUrl.pathname); const doc = { id: new Bun.CryptoHasher("sha256").update(scriptUrl.href).digest("hex").slice(0, 16), title: "Daily AI News VoiceScript", sourceUri: scriptUrl.href, body: await Bun.file(scriptPath).text(), mediaType: "application/yaml", metadata: { voiceProfile: process.argv[3] || process.env.VOICEBOX_DEFAULT_PROFILE || "robin-news" } }; await Bun.write(process.argv[2], JSON.stringify([doc], null, 2))' "${vars.work_dir}/2-script/voicescript-${vars.run_date}.yaml" "${vars.work_dir}/3-audio/docs.json" "${vars.voice_profile}"
184
199
 
185
200
  - id: generate
186
- description: "Invoke voice-gen generator plugin to generate audio"
201
+ description: "Invoke voice generator plugin (voice_generator var) to generate audio"
187
202
  onEnter:
188
203
  - kind: shell
189
204
  options:
190
205
  command: >-
206
+ __STAMP_T0="$(date -u +%FT%TZ)"; trap '[ -n "${vars.work_dir}" ] && printf "{\"step\":\"generate\",\"startedAt\":\"%s\",\"endedAt\":\"%s\"}\n" "$__STAMP_T0" "$(date -u +%FT%TZ)" >> "${vars.work_dir}/step-timing.json"' EXIT;
191
207
  set --;
192
208
  if [ -n "${vars.plugins_path}" ]; then set -- --plugins-path "${vars.plugins_path}"; fi;
193
209
  if [ -f "${vars.work_dir}/3-audio/content.json" ] && jq -e '.metadata.qc.passed == true' "${vars.work_dir}/3-audio/content.json" > /dev/null 2>&1; then echo "audio cached: ${vars.work_dir}/3-audio/content.json (QC passed)"; exit 0; fi;
194
- VOICE_GEN_MP3="${vars.transcode_mp3}" kk executor run voice-gen --in "${vars.work_dir}/3-audio/docs.json" --out "${vars.work_dir}/3-audio/content.json" "$@"
210
+ VOICE_GEN_MP3="${vars.transcode_mp3}" kk executor run ${vars.voice_generator} --in "${vars.work_dir}/3-audio/docs.json" --out "${vars.work_dir}/3-audio/content.json" "$@"
195
211
 
196
212
  - id: quality-control
197
213
  description: "Verify audio quality, speech rate, and silence/repetition QC metrics"
@@ -199,63 +215,117 @@ states:
199
215
  - kind: shell
200
216
  options:
201
217
  command: >-
218
+ __STAMP_T0="$(date -u +%FT%TZ)"; trap '[ -n "${vars.work_dir}" ] && printf "{\"step\":\"quality-control\",\"startedAt\":\"%s\",\"endedAt\":\"%s\"}\n" "$__STAMP_T0" "$(date -u +%FT%TZ)" >> "${vars.work_dir}/step-timing.json"' EXIT;
202
219
  bun -e 'const content = JSON.parse(await Bun.file(process.argv[1]).text()); const qc = content.metadata?.qc; console.log("=== Voice Quality Control Report ==="); console.log("Overall Score: " + (qc ? qc.overallScore : 100) + "/100 | Passed: " + (qc ? qc.passed : true)); if (qc && qc.criticalIssues && qc.criticalIssues.length) { console.warn("QC Issues:\n" + qc.criticalIssues.map(function(i) { return " - " + i; }).join("\n")); } else { console.log("All audio segments passed quality control checks cleanly."); }' "${vars.work_dir}/3-audio/content.json"
203
220
 
221
+ - id: publish-prep
222
+ description: "Merge article Content with audio (podcast assembly facts ride the content options channel for podcast-pub's absorbed assembly; runDate kept for surfdash slug)"
223
+ onEnter:
224
+ - kind: shell
225
+ options:
226
+ command: >-
227
+ __STAMP_T0="$(date -u +%FT%TZ)"; trap '[ -n "${vars.work_dir}" ] && printf "{\"step\":\"publish-prep\",\"startedAt\":\"%s\",\"endedAt\":\"%s\"}\n" "$__STAMP_T0" "$(date -u +%FT%TZ)" >> "${vars.work_dir}/step-timing.json"' EXIT;
228
+ bun -e 'const article = JSON.parse(await Bun.file(process.argv[1]).text()); const audio = JSON.parse(await Bun.file(process.argv[2]).text()); const audioMeta = audio.metadata ?? {}; const rawFile = audioMeta.mp3Path ?? audioMeta.audioPath; if (!rawFile) { console.error("publish-prep: voice content carries no mp3Path/audioPath"); process.exit(1); } const { resolve } = await import("node:path"); const { existsSync } = await import("node:fs"); const audioFile = resolve(rawFile); const coverPath = resolve(process.argv[4]); const merged = { ...article, metadata: { ...(article.metadata ?? {}), audioFile, runDate: process.argv[5] }, options: { audioFile, durationSec: audioMeta.duration, coverPath: existsSync(coverPath) ? coverPath : undefined } }; await Bun.write(process.argv[3], JSON.stringify(merged, null, 2))' "${vars.work_dir}/2-article/content-${vars.run_date}.json" "${vars.work_dir}/3-audio/content.json" "${vars.work_dir}/3-publish/content.json" "${vars.work_dir}/2-cover/cover-${vars.run_date}.png" "${vars.run_date}"
229
+
230
+ - id: publish-surfdash
231
+ description: "Publish the merged Content to surfdash (single target); result lands in result.json — surfdash publishes first so show-notes can link the article (task 0118 seam reorder)"
232
+ onEnter:
233
+ - kind: shell
234
+ options:
235
+ command: >-
236
+ __STAMP_T0="$(date -u +%FT%TZ)"; trap '[ -n "${vars.work_dir}" ] && printf "{\"step\":\"publish-surfdash\",\"startedAt\":\"%s\",\"endedAt\":\"%s\"}\n" "$__STAMP_T0" "$(date -u +%FT%TZ)" >> "${vars.work_dir}/step-timing.json"' EXIT;
237
+ set --;
238
+ if [ -n "${vars.plugins_path}" ]; then set -- --plugins-path "${vars.plugins_path}"; fi;
239
+ RESULT="${vars.work_dir}/3-publish/result-surfdash.json";
240
+ if jq -e '.targets[]? | select(.target == "surfdash-pub" and .ok == true and .status == "published")' "$RESULT" > /dev/null 2>&1; then echo "publish-surfdash cached: surfdash target ok in $RESULT"; exit 0; fi;
241
+ env -u SPUR_WORKFLOW_RUN_ACTIVE kk executor fan-out --content "${vars.work_dir}/3-publish/content.json" --target surfdash-pub --out "$RESULT" "$@" || true;
242
+ echo "publish-surfdash: surfdash target dispatched; classification happens after podcast publish"
243
+
204
244
  - id: show-notes
205
- description: "Render uniform show notes from the episode plan (podcast-pub show-notes operation; PODCAST_PUB_TITLE carries the effective title vars.title || article.title)"
245
+ description: "Render timeline show notes from the episode plan (podcast-pub show-notes op, briefMode; article URL from the surfdash result postPath — surfdash failure degrades to legacy full-body layout)"
206
246
  onEnter:
207
247
  - kind: shell
208
248
  options:
209
249
  command: >-
250
+ __STAMP_T0="$(date -u +%FT%TZ)"; trap '[ -n "${vars.work_dir}" ] && printf "{\"step\":\"show-notes\",\"startedAt\":\"%s\",\"endedAt\":\"%s\"}\n" "$__STAMP_T0" "$(date -u +%FT%TZ)" >> "${vars.work_dir}/step-timing.json"' EXIT;
210
251
  set --;
211
252
  if [ -n "${vars.plugins_path}" ]; then set -- --plugins-path "${vars.plugins_path}"; fi;
212
253
  TITLE_ARG="${vars.title}";
213
254
  EFFECTIVE_TITLE="$(jq -r '.title // ""' "${vars.work_dir}/2-article/content-${vars.run_date}.json")";
214
255
  if [ -z "$TITLE_ARG" ]; then TITLE_ARG="$EFFECTIVE_TITLE"; fi;
215
- PODCAST_PUB_TITLE="$TITLE_ARG" PODCAST_PUB_RUN_DATE="${vars.run_date}" kk executor run podcast-pub --in "${vars.work_dir}/2-plan/plan.json" --out "${vars.work_dir}/3-publish/show-notes.md" "$@"
256
+ RESULT="${vars.work_dir}/3-publish/result.json";
257
+ POST_PATH="$(jq -r '[(.targets // [])[] | select(.target == "surfdash-pub" and .ok == true) | .metadata.postPath // empty] | first // ""' "$RESULT" 2>/dev/null || true)";
258
+ PODCAST_PUB_TITLE="$TITLE_ARG" PODCAST_PUB_RUN_DATE="${vars.run_date}" PODCAST_PUB_BRIEF_MODE=true PODCAST_PUB_ARTICLE_POST_PATH="$POST_PATH" kk executor run podcast-pub --in "${vars.work_dir}/2-plan/plan.json" --out "${vars.work_dir}/3-publish/show-notes.md" "$@"
216
259
 
217
- - id: publish-prep
218
- description: "Merge article Content with audio (podcast assembly facts ride the content options channel for podcast-pub's absorbed assembly; runDate kept for surfdash slug)"
260
+ - id: publish-podcast
261
+ description: "Publish the episode to podcast-pub with the rendered show notes (metadata.showNotes carries the brief/timeline markdown; map.ts falls back to content.body when absent — task 0118 R3)"
219
262
  onEnter:
220
263
  - kind: shell
221
264
  options:
222
265
  command: >-
223
- bun -e 'const article = JSON.parse(await Bun.file(process.argv[1]).text()); const audio = JSON.parse(await Bun.file(process.argv[2]).text()); const audioMeta = audio.metadata ?? {}; const rawFile = audioMeta.mp3Path ?? audioMeta.audioPath; if (!rawFile) { console.error("publish-prep: voice content carries no mp3Path/audioPath"); process.exit(1); } const { resolve } = await import("node:path"); const { existsSync } = await import("node:fs"); const audioFile = resolve(rawFile); const coverPath = resolve(process.argv[4]); const merged = { ...article, metadata: { ...(article.metadata ?? {}), audioFile, runDate: process.argv[5] }, options: { audioFile, durationSec: audioMeta.duration, coverPath: existsSync(coverPath) ? coverPath : undefined } }; await Bun.write(process.argv[3], JSON.stringify(merged, null, 2))' "${vars.work_dir}/2-article/content-${vars.run_date}.json" "${vars.work_dir}/3-audio/content.json" "${vars.work_dir}/3-publish/content.json" "${vars.work_dir}/2-cover/cover-${vars.run_date}.png" "${vars.run_date}"
266
+ __STAMP_T0="$(date -u +%FT%TZ)"; trap '[ -n "${vars.work_dir}" ] && printf "{\"step\":\"publish-podcast\",\"startedAt\":\"%s\",\"endedAt\":\"%s\"}\n" "$__STAMP_T0" "$(date -u +%FT%TZ)" >> "${vars.work_dir}/step-timing.json"' EXIT;
267
+ set --;
268
+ if [ -n "${vars.plugins_path}" ]; then set -- --plugins-path "${vars.plugins_path}"; fi;
269
+ RESULT="${vars.work_dir}/3-publish/result-podcast.json";
270
+ if jq -e '.targets[]? | select(.target == "podcast-pub" and .ok == true and .status == "published")' "$RESULT" > /dev/null 2>&1; then echo "publish-podcast cached: podcast target ok in $RESULT"; exit 0; fi;
271
+ bun -e 'const content = JSON.parse(await Bun.file(process.argv[1]).text()); const notes = await Bun.file(process.argv[2]).text(); content.metadata = { ...(content.metadata ?? {}), showNotes: notes }; await Bun.write(process.argv[1], JSON.stringify(content, null, 2))' "${vars.work_dir}/3-publish/content.json" "${vars.work_dir}/3-publish/show-notes.md" &&
272
+ env -u SPUR_WORKFLOW_RUN_ACTIVE kk executor fan-out --content "${vars.work_dir}/3-publish/content.json" --target podcast-pub --out "$RESULT" "$@" || true;
273
+ echo "publish-podcast: podcast target dispatched"
224
274
 
225
275
  - id: publish
226
- description: "Fan out the merged Content to surfdash-pub and podcast-pub; classify the outcome full|partial|none into publish-status.txt (exit 0 in all three — routing is transition-owned)"
276
+ description: "Classify the sequenced surfdash → podcast outcomes full|partial|none into publish-status.txt (exit 0 in all three — routing is transition-owned; task 0118: per-target classification over the merged result.json)"
227
277
  onEnter:
228
278
  - kind: shell
229
279
  options:
230
280
  command: >-
231
- set --;
232
- if [ -n "${vars.plugins_path}" ]; then set -- --plugins-path "${vars.plugins_path}"; fi;
281
+ __STAMP_T0="$(date -u +%FT%TZ)"; trap '[ -n "${vars.work_dir}" ] && printf "{\"step\":\"publish\",\"startedAt\":\"%s\",\"endedAt\":\"%s\"}\n" "$__STAMP_T0" "$(date -u +%FT%TZ)" >> "${vars.work_dir}/step-timing.json"' EXIT;
233
282
  RESULT="${vars.work_dir}/3-publish/result.json"; STATUS_FILE="${vars.work_dir}/3-publish/publish-status.txt";
234
- if jq -e '.ok == true and ([.targets[]? | select(.ok == true and .status == "published")] | length) == 2' "$RESULT" > /dev/null 2>&1; then echo "publish cached: $RESULT (2 targets ok)"; printf 'full' > "$STATUS_FILE"; exit 0; fi;
235
- OK_COUNT=0; TOTAL=0; STATUS="none";
236
- env -u SPUR_WORKFLOW_RUN_ACTIVE kk executor fan-out --content "${vars.work_dir}/3-publish/content.json" --target surfdash-pub --target podcast-pub --out "$RESULT" "$@" || true;
237
- if [ -f "$RESULT" ]; then OK_COUNT="$(jq '[.targets[]? | select(.ok == true and .status == "published")] | length' "$RESULT")"; TOTAL="$(jq '[.targets[]?] | length' "$RESULT")"; if [ "$OK_COUNT" -gt 0 ] && [ "$OK_COUNT" -eq "$TOTAL" ]; then STATUS="full"; elif [ "$OK_COUNT" -gt 0 ]; then STATUS="partial"; fi; else echo "publish: fan-out wrote no result.json — routing to none" >&2; fi;
283
+ TARGETS="$(jq -s '[(.[0].targets // []), (.[1].targets // [])] | add' "${vars.work_dir}/3-publish/result-surfdash.json" "${vars.work_dir}/3-publish/result-podcast.json" 2>/dev/null || echo '[]')";
284
+ OK_COUNT="$(printf '%s' "$TARGETS" | jq '[.[]? | select(.ok == true and .status == "published")] | length')";
285
+ TOTAL="$(printf '%s' "$TARGETS" | jq 'length')";
286
+ if [ "$OK_COUNT" -gt 0 ] && [ "$OK_COUNT" -eq "$TOTAL" ]; then STATUS="full"; elif [ "$OK_COUNT" -gt 0 ]; then STATUS="partial"; else STATUS="none"; fi;
287
+ jq -n --argjson targets "$TARGETS" --argjson ok "$OK_COUNT" --argjson total "$TOTAL" '{ok: ($ok == $total and $total > 0), targets: $targets}' > "$RESULT";
238
288
  printf '%s' "$STATUS" > "$STATUS_FILE";
239
289
  echo "publish: status=$STATUS ($OK_COUNT/$TOTAL targets published); routing is transition-owned"
240
290
 
241
291
  - id: publish-partial
242
- 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"
292
+ 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)"
243
293
  onEnter:
244
294
  - kind: shell
245
295
  options:
246
296
  command: >-
297
+ __STAMP_T0="$(date -u +%FT%TZ)"; trap '[ -n "${vars.work_dir}" ] && printf "{\"step\":\"publish-partial\",\"startedAt\":\"%s\",\"endedAt\":\"%s\"}\n" "$__STAMP_T0" "$(date -u +%FT%TZ)" >> "${vars.work_dir}/step-timing.json"' EXIT;
247
298
  FAILED="$(jq -r '[.targets[]? | select(.ok != true or .status != "published") | .target] | join(",")' "${vars.work_dir}/3-publish/result.json")";
248
299
  if [ -z "$FAILED" ]; then FAILED="(unparsed)"; fi;
249
300
  echo "WARNING: partial publish — failed targets: $FAILED" >&2;
250
301
  mkdir -p .spur/run &&
251
302
  printf '%s partial publish: failed targets: %s (result.json preserved for kk executor fan-out --retry-from)\n' "${vars.run_date}" "$FAILED" > ".spur/run/publish-partial-${vars.run_date}.txt"
252
303
 
304
+ - id: run-report
305
+ description: "Assemble the per-run report (scores+aggregates+rejected counts+step timing) from existing artifacts — news-report-gen; output-only, never throws; 0117"
306
+ onEnter:
307
+ - kind: shell
308
+ options:
309
+ command: >-
310
+ __STAMP_T0="$(date -u +%FT%TZ)"; trap '[ -n "${vars.work_dir}" ] && printf "{\"step\":\"run-report\",\"startedAt\":\"%s\",\"endedAt\":\"%s\"}\n" "$__STAMP_T0" "$(date -u +%FT%TZ)" >> "${vars.work_dir}/step-timing.json"' EXIT;
311
+ REPORT_MD="${vars.work_dir}/run-report-${vars.run_date}.md";
312
+ if [ -f "$REPORT_MD" ]; then echo "run report cached: $REPORT_MD"; exit 0; fi;
313
+ CANDS="${vars.work_dir}/2-plan/episode-plan.json";
314
+ test -f "${vars.work_dir}/2-plan/candidates.json" || { echo "run-report: no candidates.json (run ended below plan step) — skip"; exit 0; };
315
+ jq '.metadata.docs // []' "${vars.work_dir}/2-plan/candidates.json" > "$CANDS";
316
+ set --;
317
+ if [ -n "${vars.plugins_path}" ]; then set -- --plugins-path "${vars.plugins_path}"; fi;
318
+ QC_MIN_QUALITY="${vars.qc_min_quality}" QC_MIN_IMPORTANCE="${vars.qc_min_importance}" QC_MIN_URGENCY="${vars.qc_min_urgency}" QC_MIN_IMPACT="${vars.qc_min_impact}" NEWS_REPORT_REJECTED_FILE="${vars.work_dir}/2-plan/candidates.rejected.json" NEWS_REPORT_TIMING_FILE="${vars.work_dir}/step-timing.json" NEWS_REPORT_DATE="${vars.run_date}" kk executor run news-report-gen --in "$CANDS" --out "${vars.work_dir}/run-report.json" "$@" &&
319
+ bun -e 'await Bun.write(process.argv[2], (await Bun.file(process.argv[1]).json()).body)' "${vars.work_dir}/run-report.json" "$REPORT_MD" &&
320
+ echo "run report written: $REPORT_MD"
321
+
253
322
  - id: translate-en
254
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)"
255
324
  onEnter:
256
325
  - kind: shell
257
326
  options:
258
327
  command: >-
328
+ __STAMP_T0="$(date -u +%FT%TZ)"; trap '[ -n "${vars.work_dir}" ] && printf "{\"step\":\"translate-en\",\"startedAt\":\"%s\",\"endedAt\":\"%s\"}\n" "$__STAMP_T0" "$(date -u +%FT%TZ)" >> "${vars.work_dir}/step-timing.json"' EXIT;
259
329
  RESULT="${vars.work_dir}/3-publish/result.json";
260
330
  ZH="$(jq -r '[(.targets // [])[] | select(.target == "surfdash-pub" and .ok == true) | .metadata.postPath // empty] | first // ""' "$RESULT")";
261
331
  case "$ZH" in *index.md) ;; *) ZH="";; esac;
@@ -298,6 +368,7 @@ states:
298
368
  - kind: shell
299
369
  options:
300
370
  command: >-
371
+ __STAMP_T0="$(date -u +%FT%TZ)"; trap '[ -n "${vars.work_dir}" ] && printf "{\"step\":\"translate-ja\",\"startedAt\":\"%s\",\"endedAt\":\"%s\"}\n" "$__STAMP_T0" "$(date -u +%FT%TZ)" >> "${vars.work_dir}/step-timing.json"' EXIT;
301
372
  ZH="$(cat "${vars.work_dir}/3-publish/zh-post-path.txt" 2>/dev/null || printf '')";
302
373
  JA="$(printf '%s' "$ZH" | sed 's|/zh/|/ja/|')";
303
374
  printf '%s' "$JA" > "${vars.work_dir}/3-publish/ja-post-path.txt";
@@ -334,6 +405,7 @@ states:
334
405
  - kind: shell
335
406
  options:
336
407
  command: >-
408
+ __STAMP_T0="$(date -u +%FT%TZ)"; trap '[ -n "${vars.work_dir}" ] && printf "{\"step\":\"translate-sync\",\"startedAt\":\"%s\",\"endedAt\":\"%s\"}\n" "$__STAMP_T0" "$(date -u +%FT%TZ)" >> "${vars.work_dir}/step-timing.json"' EXIT;
337
409
  test -n "${vars.surfdash_post_path}" || { echo "no zh post path — skip translate-sync"; exit 0; };
338
410
  set --;
339
411
  if [ -n "${vars.plugins_path}" ]; then set -- --plugins-path "${vars.plugins_path}"; fi;
@@ -461,8 +533,8 @@ transitions:
461
533
  kind: always
462
534
 
463
535
  - from: quality-control
464
- to: show-notes
465
- description: "publish enabled AND QC passed -> render show notes, then publish"
536
+ to: publish-prep
537
+ description: "publish enabled AND QC passed -> merge article + audio for the publish seam"
466
538
  guard:
467
539
  kind: shell
468
540
  options:
@@ -482,21 +554,33 @@ transitions:
482
554
  guard:
483
555
  kind: always
484
556
 
557
+ - from: publish-prep
558
+ to: publish-surfdash
559
+ description: "Merged content ready -> surfdash publishes first (task 0118 seam reorder: article URLs must exist before show-notes render)"
560
+ guard:
561
+ kind: always
562
+
563
+ - from: publish-surfdash
564
+ to: show-notes
565
+ description: "Surfdash dispatched (ok or failed — failure degrades to no-URL notes) -> render timeline show notes"
566
+ guard:
567
+ kind: always
568
+
485
569
  - from: show-notes
486
- to: publish-prep
487
- description: "Show notes rendered -> merge article + audio for the publish seam"
570
+ to: publish-podcast
571
+ description: "Show notes rendered (briefMode timeline or legacy fallback) -> podcast publish carries them via metadata.showNotes"
488
572
  guard:
489
573
  kind: always
490
574
 
491
- - from: publish-prep
575
+ - from: publish-podcast
492
576
  to: publish
493
- description: "Merged content ready -> fan out to surfdash-pub + podcast-pub"
577
+ description: "Podcast dispatched -> classify the per-target outcomes full|partial|none"
494
578
  guard:
495
579
  kind: always
496
580
 
497
581
  - from: publish
498
- to: translate-en
499
- description: "Publish full (all targets published) -> scaffold + translate en locale"
582
+ to: run-report
583
+ description: "Publish full (all targets published) -> assemble run report first"
500
584
  guard:
501
585
  kind: shell
502
586
  options:
@@ -519,11 +603,27 @@ transitions:
519
603
  command: 'test "$(cat "${vars.work_dir}/3-publish/publish-status.txt")" = "none"'
520
604
 
521
605
  - from: publish-partial
522
- to: done
523
- description: "Partial publish recorded (warning + .spur/run marker) -> done; retry failed targets later via kk executor fan-out --retry-from"
606
+ to: run-report
607
+ description: "Partial publish recorded (warning + .spur/run marker) -> still assemble the run report"
524
608
  guard:
525
609
  kind: always
526
610
 
611
+ - from: run-report
612
+ to: translate-en
613
+ description: "Run report assembled after full publish -> scaffold + translate en locale"
614
+ guard:
615
+ kind: shell
616
+ options:
617
+ command: 'test "$(cat "${vars.work_dir}/3-publish/publish-status.txt")" = "full"'
618
+
619
+ - from: run-report
620
+ to: done
621
+ description: "Run report assembled after partial publish -> done; retry failed targets later via kk executor fan-out --retry-from"
622
+ guard:
623
+ kind: shell
624
+ options:
625
+ command: 'test "$(cat "${vars.work_dir}/3-publish/publish-status.txt")" = "partial"'
626
+
527
627
  - from: translate-en
528
628
  to: translate-en-agent
529
629
  description: "zh post published and en draft scaffolded -> agent translates en draft in place"