@gobing-ai/knowledge-kit 0.1.4 → 0.1.6

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 (75) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/board/collect/index.css +447 -0
  3. package/dist/board/collect/index.js +2505 -453
  4. package/dist/board/daily-voice/index.css +447 -0
  5. package/dist/board/daily-voice/index.js +415 -8
  6. package/dist/board/itc/index.css +447 -0
  7. package/dist/board/itc/index.js +415 -8
  8. package/dist/index.js +494 -136
  9. package/package.json +6 -5
  10. package/plugins/generations/content-gen/dist/index.js +9 -9
  11. package/plugins/generations/core-facts-gen/dist/index.js +12 -12
  12. package/plugins/generations/daily-article-gen/dist/index.js +15 -13
  13. package/plugins/generations/daily-article-gen/src/index.ts +5 -3
  14. package/plugins/generations/dailynews-gen/dist/index.js +9 -9
  15. package/plugins/generations/episode-plan-gen/dist/index.js +12 -12
  16. package/plugins/generations/gemini-tts/dist/index.js +9 -9
  17. package/plugins/generations/image-gen/dist/index.js +28 -12
  18. package/plugins/generations/image-gen/src/compose/card.ts +19 -3
  19. package/plugins/generations/news-report-gen/dist/index.js +12 -12
  20. package/plugins/generations/voice-gen/dist/index.js +9 -9
  21. package/plugins/generations/voxcpm-tts/README.md +31 -0
  22. package/plugins/generations/voxcpm-tts/dist/index.js +9 -9
  23. package/plugins/generations/voxcpm-tts/scripts/probe-long-render.py +134 -0
  24. package/plugins/ingestions/aihot-ingest/dist/index.js +12 -12
  25. package/plugins/ingestions/horizon-ingest/dist/index.js +12 -12
  26. package/plugins/ingestions/karakeep-local/dist/index.js +13 -13
  27. package/plugins/ingestions/last30days-ingest/dist/index.js +12 -12
  28. package/plugins/ingestions/web-search/dist/index.js +10 -10
  29. package/plugins/kk/decisions/kk-podcast.yaml +44 -0
  30. package/plugins/kk/decisions/kk-safety.yaml +46 -0
  31. package/plugins/kk/plugin.json +1 -1
  32. package/plugins/kk/scripts/audio-quality.ts +635 -0
  33. package/plugins/kk/scripts/daily-stages.ts +163 -0
  34. package/plugins/kk/scripts/kk-workflow-stages.ts +34 -0
  35. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +25 -9
  36. package/plugins/kk/workflows/kk-daily-safety.yaml +37 -10
  37. package/plugins/kk/workflows/kk-solo-podcast.yaml +65 -1
  38. package/plugins/publishings/bilibili-pub/dist/index.js +22 -22
  39. package/plugins/publishings/bilibili-pub/dist/profile-check.js +22 -22
  40. package/plugins/publishings/cnblogs-pub/dist/index.js +22 -22
  41. package/plugins/publishings/cnblogs-pub/dist/profile-check.js +22 -22
  42. package/plugins/publishings/csdn-pub/dist/index.js +22 -22
  43. package/plugins/publishings/csdn-pub/dist/profile-check.js +22 -22
  44. package/plugins/publishings/emdash-pub/dist/index.js +9 -9
  45. package/plugins/publishings/infoq-pub/dist/index.js +22 -22
  46. package/plugins/publishings/infoq-pub/dist/profile-check.js +22 -22
  47. package/plugins/publishings/jike-pub/dist/index.js +22 -22
  48. package/plugins/publishings/jike-pub/dist/profile-check.js +22 -22
  49. package/plugins/publishings/juejin-pub/dist/index.js +22 -22
  50. package/plugins/publishings/juejin-pub/dist/profile-check.js +22 -22
  51. package/plugins/publishings/medium-pub/dist/index.js +9 -9
  52. package/plugins/publishings/podcast-pub/dist/index.js +9 -9
  53. package/plugins/publishings/qiita-pub/dist/index.js +9 -9
  54. package/plugins/publishings/shipinhao-pub/dist/index.js +22 -22
  55. package/plugins/publishings/shipinhao-pub/dist/profile-check.js +22 -22
  56. package/plugins/publishings/sspai-pub/dist/index.js +22 -22
  57. package/plugins/publishings/sspai-pub/dist/profile-check.js +22 -22
  58. package/plugins/publishings/substack-pub/dist/index.js +22 -22
  59. package/plugins/publishings/substack-pub/dist/profile-check.js +22 -22
  60. package/plugins/publishings/surfdash-pub/dist/index.js +39 -12
  61. package/plugins/publishings/surfdash-pub/src/index.ts +33 -6
  62. package/plugins/publishings/surfdash-pub/src/patch.ts +4 -0
  63. package/plugins/publishings/threads-pub/dist/index.js +9 -9
  64. package/plugins/publishings/wechatmp-pub/dist/index.js +22 -22
  65. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +22 -22
  66. package/plugins/publishings/weibo-pub/dist/index.js +22 -22
  67. package/plugins/publishings/weibo-pub/dist/profile-check.js +22 -22
  68. package/plugins/publishings/x-pub/dist/index.js +22 -22
  69. package/plugins/publishings/x-pub/dist/profile-check.js +22 -22
  70. package/plugins/publishings/xhs-pub/dist/index.js +22 -22
  71. package/plugins/publishings/xhs-pub/dist/profile-check.js +22 -22
  72. package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +22 -22
  73. package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +22 -22
  74. package/plugins/publishings/zenn-pub/dist/index.js +9 -9
  75. package/plugins/kk/workflows/feature-verification.yaml +0 -89
@@ -60,6 +60,7 @@ import {
60
60
  readFileSync,
61
61
  renameSync,
62
62
  rmSync,
63
+ statSync,
63
64
  unlinkSync,
64
65
  writeFileSync,
65
66
  } from 'node:fs';
@@ -284,6 +285,79 @@ function applyFloorGate(
284
285
  );
285
286
  }
286
287
 
288
+ /** Widest window the extract ladder may reach; a "daily" show must not import a week-old story. */
289
+ const LADDER_MAX_MS = 72 * 3_600_000;
290
+
291
+ const WINDOW_UNIT_MS: Record<string, number> = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 };
292
+
293
+ /** `'24h'` → milliseconds, mirroring `kk store extract --window`'s grammar. null when unparsable. */
294
+ function parseWindowMs(window: string): number | null {
295
+ const match = /^(\d+)([smhd])$/.exec(window.trim());
296
+ if (match === null) return null;
297
+ const unit = WINDOW_UNIT_MS[match[2] as string];
298
+ const ms = Number(match[1]) * (unit ?? 0);
299
+ return ms > 0 ? ms : null;
300
+ }
301
+
302
+ /** Milliseconds → the hour form the store CLI takes. Whole hours only: every ladder step is ×2/×3. */
303
+ function formatWindowMs(ms: number): string {
304
+ return `${Math.max(1, Math.round(ms / 3_600_000))}h`;
305
+ }
306
+
307
+ /**
308
+ * Pool extraction with a conditional window ladder (2026-10-19 operator decision, superseding the
309
+ * 0266 D1 blanket rejection). `kk store extract` reads one first_seen window and `applyFloorGate`
310
+ * then refuses a pool that cannot reach `episode_min_minutes`. Together they ate whole days: an
311
+ * episode consumes 6-12 stories while a day of collection adds ~3-13 qualifying ones, so the 04:30
312
+ * runs of 2026-10-18 and 2026-10-19 saw 2- and 3-item pools and closed as no-content while older
313
+ * unselected items sat in the store. The ladder keeps the configured window as the first choice and
314
+ * widens (×2, ×3, capped at 72h) ONLY when that pool's estimate misses the floor — a rich day is
315
+ * extracted exactly once, and a thin one is filled from the backlog instead of dropped.
316
+ *
317
+ * The store's `selections` ledger still excludes every story a previous run took, so a widened
318
+ * window can only add items no episode has used. The estimate here runs on the raw pool (before QC
319
+ * and dedup), so it is an upper bound: the ladder can stop one step early, never one late.
320
+ */
321
+ export function dailyExtract(args: string[]): void {
322
+ const [workDir = '', topic = '', window = '', minMinutes = '', vdriver = ''] = args;
323
+ if (!workDir) fail('missing work_dir');
324
+ if (!topic) fail('missing topic');
325
+ const poolPath = join(workDir, '10-collect', 'pool.json');
326
+ const base = parseWindowMs(window === '' ? '24h' : window);
327
+ if (base === null) fail(`extract: invalid window: ${window || '(empty)'}`);
328
+ const floor = Number(minMinutes);
329
+ const gated = minMinutes !== '' && Number.isFinite(floor) && floor > 0;
330
+ const windows: number[] = [base];
331
+ for (const factor of gated ? [2, 3] : []) {
332
+ const ms = base * factor;
333
+ if (ms > LADDER_MAX_MS) break;
334
+ windows.push(ms);
335
+ }
336
+ timed(workDir, 'extract', () => {
337
+ mkdirSync(join(workDir, '10-collect'), { recursive: true });
338
+ let chosen: { window: string; docs: unknown[] } | null = null;
339
+ for (const ms of windows) {
340
+ const label = formatWindowMs(ms);
341
+ const attempt = join(workDir, '10-collect', `.pool-${label}.json`);
342
+ kk(['store', 'extract', '--topic', topic, '--window', label, '--out', attempt]);
343
+ const parsed = readJson(attempt);
344
+ const docs = Array.isArray(parsed) ? (parsed as unknown[]) : [];
345
+ const estimate = estimateSpokenMinutes(docs, vdriver);
346
+ chosen = { window: label, docs };
347
+ const verdict = gated
348
+ ? `estimated ${estimate.minutes} min — ${estimate.seconds >= floor * 60 ? 'clears' : 'below'} the ${floor} min floor`
349
+ : 'floor gate off';
350
+ console.error(`extract: ${label} pool ${docs.length} item(s), ${verdict}`);
351
+ if (!gated || estimate.seconds >= floor * 60) break;
352
+ rmSync(attempt, { force: true });
353
+ }
354
+ const picked = chosen ?? { window: formatWindowMs(base), docs: [] as unknown[] };
355
+ sted('write', poolPath, () => writeJsonText(poolPath, picked.docs));
356
+ for (const ms of windows)
357
+ rmSync(join(workDir, '10-collect', `.pool-${formatWindowMs(ms)}.json`), { force: true });
358
+ });
359
+ }
360
+
287
361
  export function dailyArticle(args: string[]): void {
288
362
  const [workDir = '', runDate = '', pluginsPath = ''] = args;
289
363
  const articleMd = join(workDir, '22-article', `${runDate}_06_article_article.md`);
@@ -1907,6 +1981,95 @@ export function dailySafetyGate(args: string[]): void {
1907
1981
  });
1908
1982
  }
1909
1983
 
1984
+ /**
1985
+ * `daily-preflight <workflow.yaml>…` — assert the stage runner that `kk stage` just loaded
1986
+ * advertises every `kk stage <name>` these workflow definitions invoke.
1987
+ *
1988
+ * WHY (2026-10-20): the scheduler runs the CHECKOUT's workflow YAML while `kk stage` resolves the
1989
+ * runner from the INSTALLED package (`apps/cli/src/stage.ts`). A commit that adds a stage therefore
1990
+ * outruns the global install silently, and the mismatch only surfaces where the new stage is first
1991
+ * reached. That day's 04:30 run died 29 minutes in at `safety` with `unknown stage:
1992
+ * daily-safety-verdict` (present in the checkout, absent from the installed 0.1.4) and published
1993
+ * nothing. This runs as the first command of the run's `prepare` node, so the same skew costs one
1994
+ * cheap step and names both the missing stages and the refresh command.
1995
+ *
1996
+ * The known-name set is the dispatch switch of the sibling `kk-workflow-stages.ts` — the file beside
1997
+ * this one, i.e. the copy actually in force, never a checkout-vs-install guess.
1998
+ */
1999
+ export function dailyPreflight(args: string[]): void {
2000
+ const files = args.filter((arg) => arg !== '');
2001
+ timed('', 'preflight', () => {
2002
+ if (files.length === 0) failWith(2, 'daily-preflight: pass at least one workflow YAML path');
2003
+ const runner = join(import.meta.dir, 'kk-workflow-stages.ts');
2004
+ if (!isFile(runner)) failWith(1, `daily-preflight.runner-missing: ${runner} is not beside this stage body`);
2005
+ const known = new Set(
2006
+ [...readFileSync(runner, 'utf-8').matchAll(/^\s*case '([a-z][a-z0-9-]*)':/gm)].map((match) =>
2007
+ String(match[1]),
2008
+ ),
2009
+ );
2010
+ const missing: string[] = [];
2011
+ for (const file of files) {
2012
+ if (!isFile(file)) failWith(1, `daily-preflight.workflow-missing: ${file}`);
2013
+ for (const match of readFileSync(file, 'utf-8').matchAll(/\bkk stage ([a-z][a-z0-9-]*)/g)) {
2014
+ const name = String(match[1]);
2015
+ if (!known.has(name)) missing.push(`${name} (needed by ${basename(file)})`);
2016
+ }
2017
+ }
2018
+ if (missing.length > 0) {
2019
+ failWith(
2020
+ 1,
2021
+ `daily-preflight.runner-stale: the installed stage runner ${runner} does not know ${missing.join(', ')} — refresh it (install the released kk package, or rsync plugins/kk/scripts/ per AGENTS.md) before the scheduled run`,
2022
+ );
2023
+ }
2024
+ console.log(
2025
+ `preflight: stage runner advertises every stage ${files.map((file) => basename(file)).join(' + ')} invokes`,
2026
+ );
2027
+ });
2028
+ }
2029
+
2030
+ /**
2031
+ * Task 0317 R6 — fold the gate's own classification, its stamp and the `kk-safety-verdict` catalog
2032
+ * decision into ONE accept/refuse token the `judge -> clean` guard reads as a single predicate
2033
+ * (ADR-115 caps that guard at 5 logical commands, so the logic lives here).
2034
+ *
2035
+ * The gate stays authoritative on `block`/`revise`; the decision can only refuse an outcome the gate
2036
+ * already admitted. The decision replaced the judge's self-reported `confidence` field, which the
2037
+ * accept guard used to re-parse in shell: its fallback is `escalate`, so missing or garbled evidence
2038
+ * lands on a recorded human decision instead of a silent publish.
2039
+ *
2040
+ * Writes `24-script/<date>_09_safety-verdict.status` = `accept`, or `refuse` after naming the
2041
+ * offending value on stderr. Never exits non-zero: routing belongs to the transitions.
2042
+ */
2043
+ export function dailySafetyVerdict(args: string[]): void {
2044
+ const [workDir = '', runDate = ''] = args;
2045
+ const statusFile = safetyPath(workDir, runDate, 'safety-judge_status.txt');
2046
+ const stampFile = safetyPath(workDir, runDate, 'safety-judge_stamp.json');
2047
+ const decisionFile = safetyPath(workDir, runDate, 'safety-verdict.decision');
2048
+ const token = safetyPath(workDir, runDate, 'safety-verdict.status');
2049
+ const ADMITTED = new Set(['pass', 'escalate', 'exclude']);
2050
+ timed(workDir, 'safety-verdict-check', () => {
2051
+ const status = isFile(statusFile) ? readFileSync(statusFile, 'utf-8').trim() : '';
2052
+ const stamp = isFile(stampFile) ? statSync(stampFile).size : 0;
2053
+ // A missing/unreadable decision row is a refusal, never an approval: `onError: continue` on
2054
+ // the decide action is what can leave it absent.
2055
+ let verdict = '';
2056
+ if (isFile(decisionFile)) {
2057
+ const row = JSON.parse(readFileSync(decisionFile, 'utf-8')) as { value?: unknown };
2058
+ verdict = String(row.value ?? '');
2059
+ }
2060
+ let refusal = '';
2061
+ if (!ADMITTED.has(status)) {
2062
+ refusal = `kk-daily-safety.judge.verdict-refused: gate status [${status}] is not an admitted outcome, so these drafts must not publish`;
2063
+ } else if (stamp === 0) {
2064
+ refusal = `kk-daily-safety.judge.verdict-refused: gate stamp missing for status ${status}, so these drafts must not publish`;
2065
+ } else if (!ADMITTED.has(verdict)) {
2066
+ refusal = `kk-daily-safety.judge.decision-refused: kk-safety-verdict [${verdict}] must admit pass|escalate|exclude, so these drafts must not publish`;
2067
+ }
2068
+ if (refusal !== '') console.error(refusal);
2069
+ sted('write', token, () => writeRaw(token, refusal === '' ? 'accept' : 'refuse'));
2070
+ });
2071
+ }
2072
+
1910
2073
  /**
1911
2074
  * Post-remediation bookkeeping for the `safety-revise` state: the agent edited the drafts, this
1912
2075
  * stage checks they are still the artifacts the rest of the pipeline consumes and spends one unit
@@ -159,6 +159,20 @@ async function loadDailyStages(): Promise<typeof import('./daily-stages.ts')> {
159
159
  );
160
160
  }
161
161
  }
162
+ /**
163
+ * Load the audio-quality diagnostic lazily: it shells out to ffprobe/ffmpeg and is only needed when an
164
+ * operator asks for it, so the runner must not pay for it on every stage (and an install without
165
+ * ffmpeg on PATH still runs every other stage).
166
+ */
167
+ async function loadAudioQuality(): Promise<typeof import('./audio-quality.ts')> {
168
+ try {
169
+ return await import('./audio-quality.ts');
170
+ } catch {
171
+ return fail(
172
+ 'audio-quality.ts not found next to kk-workflow-stages.ts — install or refresh the kk package (bun link in a checkout)',
173
+ );
174
+ }
175
+ }
162
176
  export const ERRNO_REASONS: Record<string, string> = {
163
177
  EACCES: 'Permission denied',
164
178
  EROFS: 'Read-only file system',
@@ -1593,6 +1607,11 @@ if (import.meta.main)
1593
1607
  case 'collect-prepare':
1594
1608
  collectPrepare(rest);
1595
1609
  break;
1610
+ case 'daily-extract': {
1611
+ const { dailyExtract } = await loadDailyStages();
1612
+ dailyExtract(rest);
1613
+ break;
1614
+ }
1596
1615
  case 'daily-plan': {
1597
1616
  const { dailyPlan } = await loadDailyStages();
1598
1617
  dailyPlan(rest);
@@ -1633,6 +1652,16 @@ if (import.meta.main)
1633
1652
  dailyScript(rest);
1634
1653
  break;
1635
1654
  }
1655
+ case 'audio-quality': {
1656
+ const { runAudioQuality } = await loadAudioQuality();
1657
+ await runAudioQuality(rest);
1658
+ break;
1659
+ }
1660
+ case 'daily-preflight': {
1661
+ const { dailyPreflight } = await loadDailyStages();
1662
+ dailyPreflight(rest);
1663
+ break;
1664
+ }
1636
1665
  case 'daily-safety-prep': {
1637
1666
  const { dailySafetyPrep } = await loadDailyStages();
1638
1667
  dailySafetyPrep(rest);
@@ -1643,6 +1672,11 @@ if (import.meta.main)
1643
1672
  dailySafetyGate(rest);
1644
1673
  break;
1645
1674
  }
1675
+ case 'daily-safety-verdict': {
1676
+ const { dailySafetyVerdict } = await loadDailyStages();
1677
+ dailySafetyVerdict(rest);
1678
+ break;
1679
+ }
1646
1680
  case 'daily-safety-revise': {
1647
1681
  const { dailySafetyRevise } = await loadDailyStages();
1648
1682
  dailySafetyRevise(rest);
@@ -25,9 +25,15 @@ vars:
25
25
  # Voice driver switch — voxcpm (voxcpm-tts) | gemini (gemini-tts) | voicebox (voice-gen)
26
26
  # | ominivoice (omni-voice-gen). The generate tier below mirrors the fallback order, and the
27
27
  # fit/duration gates take their spoken-char rate from this var — switching drivers here is enough.
28
- # 2026-10-15 production test (operator decision): voxcpm first, then gemini, then omni. voxcpm
29
- # rendered that episode in 21.8 min at a measured 5.19 字/秒 (fitted at 5.2); if the audio verdict
30
- # is good this is the standing order, otherwise revert this default to "gemini".
28
+ # 2026-10-20 (operator decision): voxcpm STAYS the primary. It is audibly echoey - a 40 ms periodic
29
+ # reflection on renders longer than ~30 s plus a pause floor only 16-22 dB below speech, measured by
30
+ # `kk stage audio-quality` on every episode's raw model output since 10-15 (cepstrum 40 ms @ 26-31 dB
31
+ # / pauseFloor -16..-22 dB, versus gemini's 20-25 ms @ 19-20 dB / -34..-37 dB) - and no exposed knob
32
+ # (cfg_value, inference_timesteps, normalize, denoise) or usable checkpoint fixes it (VoxCPM1.5:
33
+ # 80 ms @ 31 dB; VoxCPM-0.5B loses the lattice but measures -15.5 dB at 16 kHz). The operator keeps
34
+ # the local driver for its speed and cost and will monitor it episode by episode until a proper fix
35
+ # exists; flipping this one var to "gemini" is the escape hatch (measured dry: 20-25 ms / -34..-37 dB)
36
+ # at the price of a 1h47m-2h21m remote render.
31
37
  vdriver: "voxcpm"
32
38
  intro_music: ".works/kk-daily-ai-voice/AI每日资讯_8秒播客片头.mp3"
33
39
  surfdash_root: "/Users/robin/xprojects/surfdash"
@@ -113,11 +119,18 @@ nodes:
113
119
  the artifact directories and the safety revise budget. `kk ledger sync` records runs that
114
120
  already finished without a ledger row (failed ones included) — a fail-fast DAG never reaches
115
121
  its own `ledger` node, so the next run's prepare is the backfill (design §7).
122
+ The `daily-preflight` stage runs FIRST: the scheduler executes this checkout's YAML while
123
+ `kk stage` resolves its stage runner from the installed package, so a run whose YAML names a
124
+ stage the installed runner lacks must fail here, naming the stage, instead of 29 minutes later
125
+ inside the nested safety machine (2026-10-20: `unknown stage: daily-safety-verdict`, nothing
126
+ published).
116
127
  action:
117
128
  kind: shell
118
129
  options:
119
130
  command: >-
120
- kk daily prepare --workflow kk-daily-ai-voice2 --run-date "${vars.run_date}" --ledger-sync
131
+ kk stage daily-preflight plugins/kk/workflows/kk-daily-ai-voice2.yaml
132
+ plugins/kk/workflows/kk-daily-safety.yaml
133
+ && kk daily prepare --workflow kk-daily-ai-voice2 --run-date "${vars.run_date}" --ledger-sync
121
134
  --run "${vars.__runId}" --definition-digest "${vars.__definitionDigest}"
122
135
  --setting window="${vars.window}" --setting language="${vars.language}" --setting vdriver="${vars.vdriver}"
123
136
  --setting skip_review="${vars.skip_review}" --setting transcode_mp3="${vars.transcode_mp3}"
@@ -181,14 +194,17 @@ nodes:
181
194
  kk-collect wrote, minus every story already selected or published for the ai-news topic (the
182
195
  store's `selections` ledger, written by the `select` node below). Writes
183
196
  <work_dir>/10-collect/pool.json (the fan-in blended.json shape) and prints {count}. An empty
184
- pool writes [] and exits 0 — the run's no-content path, not a failure.
197
+ pool writes [] and exits 0 — the run's no-content path, not a failure. The `daily-extract`
198
+ stage widens the window (×2, ×3, capped at 72h) only when the configured window's pool cannot
199
+ reach `episode_min_minutes` (2026-10-19): a published episode consumes more stories than a day
200
+ of collection supplies, so the tight window alone closed 2026-10-18/19 as no-content while
201
+ older unselected items sat in the store.
185
202
  dependsOn: [read-run-date]
186
203
  action:
187
204
  kind: shell
188
205
  options:
189
206
  command: >-
190
- kk store extract --topic ai-news --window "${vars.window}"
191
- --out "${vars.work_dir}/10-collect/pool.json"
207
+ kk stage daily-extract "${vars.work_dir}" ai-news "${vars.window}" "${vars.episode_min_minutes}" "${vars.vdriver}"
192
208
 
193
209
  - id: plan
194
210
  description: >-
@@ -545,7 +561,7 @@ nodes:
545
561
  kk stage daily-wrap-docs "${vars.work_dir}" "${vars.run_date}" "${vars.voice_profile}"
546
562
 
547
563
  - id: generate
548
- description: "Voice tier, primary driver (vars.vdriver — voxcpm while the 2026-10-15 production test runs). onError: continue — the fallback siblings below are the loud chain (same pattern as 0238 R2)."
564
+ 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)."
549
565
  # wrap-docs is a data dependency, not a sibling: daily-generate reads its _10_wrap-docs_docs.json
550
566
  # as the TTS input. Without this edge the two nodes race in the same wave (0212 dogfood finding).
551
567
  dependsOn: [script, safety, review, wrap-docs]
@@ -567,7 +583,7 @@ nodes:
567
583
  # its plugin id and the cache ignores foreign-driver sections, so a failed attempt can neither be
568
584
  # relabeled nor mix voices). Non-final siblings continue on error; the last one is loud.
569
585
  - id: generate-fallback
570
- description: "Voice tier fallback 1: gemini (gemini-tts) when the primary driver left no valid Content"
586
+ 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"
571
587
  dependsOn: [generate]
572
588
  condition:
573
589
  kind: shell
@@ -93,6 +93,31 @@ states:
93
93
  options:
94
94
  command: >-
95
95
  kk stage daily-safety-gate "${vars.work_dir}" "${vars.run_date}" "${vars.safety_max_revise}"
96
+ # 0317 R6: the publish verdict is a catalog DECISION over the same artifacts the gate read,
97
+ # not the judge's self-reported `confidence` re-parsed in a shell jq filter. `onError:
98
+ # continue` keeps a catalog/maker problem from aborting the machine — the accept guard below
99
+ # then refuses an absent row, which is the fail-closed direction.
100
+ - kind: decide
101
+ onError: continue
102
+ options:
103
+ id: safety-verdict
104
+ decision: kk-safety-verdict
105
+ params:
106
+ report: >-
107
+ Safety report ${vars.work_dir}/24-script/${vars.run_date}_09_safety-review_report.json,
108
+ judge verdict ${vars.work_dir}/24-script/${vars.run_date}_09_safety-judge_verdict.json,
109
+ and the gate's own classification in
110
+ ${vars.work_dir}/24-script/${vars.run_date}_09_safety-judge_status.txt.
111
+ evidence:
112
+ - ${vars.work_dir}/24-script/${vars.run_date}_09_safety-review_report.json
113
+ - ${vars.work_dir}/24-script/${vars.run_date}_09_safety-judge_verdict.json
114
+ - ${vars.work_dir}/24-script/${vars.run_date}_09_safety-judge_status.txt
115
+ resultFile: ${vars.work_dir}/24-script/${vars.run_date}_09_safety-verdict.decision
116
+ # Fold gate status + stamp + decision into the single accept token the accept guard reads.
117
+ - kind: shell
118
+ options:
119
+ command: >-
120
+ kk stage daily-safety-verdict "${vars.work_dir}" "${vars.run_date}"
96
121
  - kind: file.read.into-var
97
122
  options:
98
123
  path: ${vars.work_dir}/24-script/${vars.run_date}_09_safety-judge_status.txt
@@ -139,20 +164,22 @@ transitions:
139
164
  - from: judge
140
165
  to: clean
141
166
  description: >-
142
- An admitted outcome: an explicit PASS (confidence HIGH|MEDIUM|LOW) with a stamp, or a
143
- NEEDS_REVISION the gate has already classified `exclude` after the revise budget (admission-only
144
- residue publishes at recorded risk — 2026-10-08). The gate writes safety-judge_stamp.json for
145
- exactly pass|escalate|exclude. The verdict read is the same confidence-aware jq filter the itc/example judges
146
- use (byte-identical, so the one contract cannot drift): a `verdict: PASS` whose `confidence`
147
- is missing or outside HIGH|MEDIUM|LOW reads as `PASS!`, which this accept guard never matches,
148
- and the refusal names the offending value on stderr first. The gate already blocks such a
149
- verdict, so this guard is the belt-and-braces admission check on the accept edge, and it can
150
- never strand the run: the unconditional `judge -> block` below always matches.
167
+ An admitted outcome: the gate classified the run pass|escalate|exclude and wrote its stamp,
168
+ AND the `kk-safety-verdict` decision (task 0317 R6) admits the same pair. The decision replaced
169
+ the judge's self-reported `confidence` re-parsed in a shell jq filter: it reads the report, the
170
+ verdict and the gate status as evidence and declares `escalate` as its fallback, so a missing
171
+ or garbled evidence set lands on a recorded human decision instead of a silent publish. The
172
+ gate stays authoritative on `block`/`revise`, which is why the status check is not redundant:
173
+ a decision cannot overrule the gate. An absent or out-of-vocabulary decision row refuses the
174
+ edge (fail-closed), and the unconditional `judge -> block` below always matches, so the loop
175
+ can never strand.
151
176
  guard:
152
177
  kind: shell
153
178
  options:
179
+ # One predicate over one result file (ADR-115): the three tokens are folded by
180
+ # `kk stage daily-safety-verdict`, which names any refusal on stderr.
154
181
  command: >-
155
- jq -n -e --arg v "$(jq -r 'if (.verdict // "") == "PASS" then (.verdict + (if ((.confidence // "") | test("^(HIGH|MEDIUM|LOW)$")) then "" else "!" end)) else (.verdict // "") end' "${vars.work_dir}/24-script/${vars.run_date}_09_safety-judge_verdict.json" 2>/dev/null)" --arg s "$(jq -r '.status // ""' "${vars.work_dir}/24-script/${vars.run_date}_09_safety-judge_stamp.json" 2>/dev/null)" --arg bytes "$(wc -c < "${vars.work_dir}/24-script/${vars.run_date}_09_safety-judge_stamp.json" 2>/dev/null)" 'if (($v == "PASS" or ($v == "NEEDS_REVISION" and $s == "exclude")) and (($bytes | gsub("[^0-9]"; "") | tonumber? // 0) > 0)) then true else ("kk-daily-safety.judge.confidence-refused: verdict " + $v + " — a judge PASS is only usable with confidence HIGH|MEDIUM|LOW, so these drafts must not publish" | halt_error(1)) end' >/dev/null
182
+ test "$(cat "${vars.work_dir}/24-script/${vars.run_date}_09_safety-verdict.status" 2>/dev/null)" = accept
156
183
 
157
184
  - from: judge
158
185
  to: revise
@@ -18,6 +18,9 @@ vars:
18
18
  work_dir: ""
19
19
  force: "false"
20
20
  outline: ""
21
+ # Auto profile (0317 R7): bind the outline from the option evidence instead of pausing, and fall
22
+ # back to the operator gate whenever the decision is not confident.
23
+ auto: "false"
21
24
  script_approved: ""
22
25
  target_duration_min: "8"
23
26
  words_per_min: "150"
@@ -102,6 +105,43 @@ states:
102
105
  decision:
103
106
  mode: never
104
107
 
108
+ - id: outline-autopick
109
+ description: "Auto profile — bind the outline from the written options; defer to the operator gate on low confidence (0317 R7)"
110
+ onEnter:
111
+ # ONE evidence file: option c is optional, and a missing `--evidence` path fails the decide action.
112
+ - kind: shell
113
+ options:
114
+ command: >-
115
+ mkdir -p "${vars.work_dir}/2-outline"; ev="${vars.work_dir}/2-outline/.outline-evidence.md"; : > "$ev";
116
+ for s in a b c; do f="${vars.work_dir}/2-outline/outline-option-$s.md"; if [ -f "$f" ]; then printf '## option %s\n%s\n' "$s" "$(cat "$f")" >> "$ev"; fi; done;
117
+ exit 0
118
+ - kind: decide
119
+ onError: continue
120
+ options:
121
+ id: podcast-outline
122
+ decision: kk-podcast-outline
123
+ params:
124
+ briefing: >-
125
+ Episode briefing at ${vars.work_dir}/1-briefing-briefing.md; target spoken duration
126
+ ${vars.target_duration_min} minutes; language ${vars.language}.
127
+ evidence:
128
+ - ${vars.work_dir}/2-outline/.outline-evidence.md
129
+ resultFile: ${vars.work_dir}/2-outline/.outline-decision.json
130
+ # Project the row into the ONE token `outline-commit` reads. A degraded (fallback) row is not a
131
+ # pick — the same rule the kk-itc outline slot uses — so the operator gate still decides.
132
+ - kind: shell
133
+ options:
134
+ command: >-
135
+ pick="${vars.work_dir}/2-outline/.auto-pick"; : > "$pick";
136
+ if ! jq -e '.degraded == false' "${vars.work_dir}/2-outline/.outline-decision.json" >/dev/null 2>&1; then exit 0; fi;
137
+ v="$(jq -r '.value // ""' "${vars.work_dir}/2-outline/.outline-decision.json")";
138
+ if [ -f "${vars.work_dir}/2-outline/outline-option-$v.md" ]; then printf '%s\n' "$v" > "$pick"; fi;
139
+ exit 0
140
+ - kind: file.read.into-var
141
+ options:
142
+ path: ${vars.work_dir}/2-outline/.auto-pick
143
+ var: outline
144
+
105
145
  - id: outline-commit
106
146
  description: "Copy selected outline to outline-approved.md"
107
147
  onEnter:
@@ -239,8 +279,32 @@ transitions:
239
279
  kind: always
240
280
 
241
281
  - from: outline-write
282
+ to: outline-autopick
283
+ description: "Auto profile, no pre-selected outline -> bind the outline from the option evidence"
284
+ guard:
285
+ kind: shell
286
+ options:
287
+ command: 'test -z "${vars.outline}" && test "${vars.auto}" = "true"'
288
+
289
+ - from: outline-write
290
+ to: outline-select
291
+ description: "Interactive profile, no pre-selected outline -> pause for operator selection"
292
+ guard:
293
+ kind: shell
294
+ options:
295
+ command: 'test -z "${vars.outline}" && test "${vars.auto}" != "true"'
296
+
297
+ - from: outline-autopick
298
+ to: outline-commit
299
+ description: "Decision bound a confident option -> commit it"
300
+ guard:
301
+ kind: shell
302
+ options:
303
+ command: 'test -n "${vars.outline}"'
304
+
305
+ - from: outline-autopick
242
306
  to: outline-select
243
- description: "Outline not pre-selected -> pause for operator selection"
307
+ description: "Decision deferred (fallback or unconfident) -> pause for operator selection"
244
308
  guard:
245
309
  kind: shell
246
310
  options: