@gobing-ai/knowledge-kit 0.1.4 → 0.1.5

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 (68) 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 +475 -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 +12 -12
  13. package/plugins/generations/dailynews-gen/dist/index.js +9 -9
  14. package/plugins/generations/episode-plan-gen/dist/index.js +12 -12
  15. package/plugins/generations/gemini-tts/dist/index.js +9 -9
  16. package/plugins/generations/image-gen/dist/index.js +28 -12
  17. package/plugins/generations/image-gen/src/compose/card.ts +19 -3
  18. package/plugins/generations/news-report-gen/dist/index.js +12 -12
  19. package/plugins/generations/voice-gen/dist/index.js +9 -9
  20. package/plugins/generations/voxcpm-tts/dist/index.js +9 -9
  21. package/plugins/ingestions/aihot-ingest/dist/index.js +12 -12
  22. package/plugins/ingestions/horizon-ingest/dist/index.js +12 -12
  23. package/plugins/ingestions/karakeep-local/dist/index.js +13 -13
  24. package/plugins/ingestions/last30days-ingest/dist/index.js +12 -12
  25. package/plugins/ingestions/web-search/dist/index.js +10 -10
  26. package/plugins/kk/decisions/kk-podcast.yaml +44 -0
  27. package/plugins/kk/decisions/kk-safety.yaml +46 -0
  28. package/plugins/kk/plugin.json +1 -1
  29. package/plugins/kk/scripts/daily-stages.ts +117 -0
  30. package/plugins/kk/scripts/kk-workflow-stages.ts +10 -0
  31. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +6 -3
  32. package/plugins/kk/workflows/kk-daily-safety.yaml +37 -10
  33. package/plugins/kk/workflows/kk-solo-podcast.yaml +65 -1
  34. package/plugins/publishings/bilibili-pub/dist/index.js +22 -22
  35. package/plugins/publishings/bilibili-pub/dist/profile-check.js +22 -22
  36. package/plugins/publishings/cnblogs-pub/dist/index.js +22 -22
  37. package/plugins/publishings/cnblogs-pub/dist/profile-check.js +22 -22
  38. package/plugins/publishings/csdn-pub/dist/index.js +22 -22
  39. package/plugins/publishings/csdn-pub/dist/profile-check.js +22 -22
  40. package/plugins/publishings/emdash-pub/dist/index.js +9 -9
  41. package/plugins/publishings/infoq-pub/dist/index.js +22 -22
  42. package/plugins/publishings/infoq-pub/dist/profile-check.js +22 -22
  43. package/plugins/publishings/jike-pub/dist/index.js +22 -22
  44. package/plugins/publishings/jike-pub/dist/profile-check.js +22 -22
  45. package/plugins/publishings/juejin-pub/dist/index.js +22 -22
  46. package/plugins/publishings/juejin-pub/dist/profile-check.js +22 -22
  47. package/plugins/publishings/medium-pub/dist/index.js +9 -9
  48. package/plugins/publishings/podcast-pub/dist/index.js +9 -9
  49. package/plugins/publishings/qiita-pub/dist/index.js +9 -9
  50. package/plugins/publishings/shipinhao-pub/dist/index.js +22 -22
  51. package/plugins/publishings/shipinhao-pub/dist/profile-check.js +22 -22
  52. package/plugins/publishings/sspai-pub/dist/index.js +22 -22
  53. package/plugins/publishings/sspai-pub/dist/profile-check.js +22 -22
  54. package/plugins/publishings/substack-pub/dist/index.js +22 -22
  55. package/plugins/publishings/substack-pub/dist/profile-check.js +22 -22
  56. package/plugins/publishings/surfdash-pub/dist/index.js +9 -9
  57. package/plugins/publishings/threads-pub/dist/index.js +9 -9
  58. package/plugins/publishings/wechatmp-pub/dist/index.js +22 -22
  59. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +22 -22
  60. package/plugins/publishings/weibo-pub/dist/index.js +22 -22
  61. package/plugins/publishings/weibo-pub/dist/profile-check.js +22 -22
  62. package/plugins/publishings/x-pub/dist/index.js +22 -22
  63. package/plugins/publishings/x-pub/dist/profile-check.js +22 -22
  64. package/plugins/publishings/xhs-pub/dist/index.js +22 -22
  65. package/plugins/publishings/xhs-pub/dist/profile-check.js +22 -22
  66. package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +22 -22
  67. package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +22 -22
  68. package/plugins/publishings/zenn-pub/dist/index.js +9 -9
@@ -4827,7 +4827,7 @@ var init_zod = __esm(() => {
4827
4827
  init_external();
4828
4828
  });
4829
4829
 
4830
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.30/node_modules/@gobing-ai/ts-utils/dist/errors.js
4830
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.32/node_modules/@gobing-ai/ts-utils/dist/errors.js
4831
4831
  var ErrorCode;
4832
4832
  var init_errors3 = __esm(() => {
4833
4833
  ErrorCode = {
@@ -4838,7 +4838,7 @@ var init_errors3 = __esm(() => {
4838
4838
  };
4839
4839
  });
4840
4840
 
4841
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.30/node_modules/@gobing-ai/ts-utils/dist/api-response.js
4841
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.32/node_modules/@gobing-ai/ts-utils/dist/api-response.js
4842
4842
  var API_ERROR_CODES, ERROR_CODE_TO_HTTP, CLIENT_SAFE_CODES;
4843
4843
  var init_api_response = __esm(() => {
4844
4844
  init_errors3();
@@ -4861,13 +4861,13 @@ var init_api_response = __esm(() => {
4861
4861
  CLIENT_SAFE_CODES = new Set([ErrorCode.NotFound, ErrorCode.Validation, ErrorCode.Conflict]);
4862
4862
  });
4863
4863
 
4864
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.30/node_modules/@gobing-ai/ts-utils/dist/cursor.js
4864
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.32/node_modules/@gobing-ai/ts-utils/dist/cursor.js
4865
4865
  var init_cursor = () => {};
4866
4866
 
4867
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.30/node_modules/@gobing-ai/ts-utils/dist/event-severity.js
4867
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.32/node_modules/@gobing-ai/ts-utils/dist/event-severity.js
4868
4868
  var init_event_severity = () => {};
4869
4869
 
4870
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.30/node_modules/@gobing-ai/ts-utils/dist/output.js
4870
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.32/node_modules/@gobing-ai/ts-utils/dist/output.js
4871
4871
  function processStream(name) {
4872
4872
  const proc = globalThis.process;
4873
4873
  const stream = proc?.[name];
@@ -4885,7 +4885,7 @@ function echoError(message, target = defaultStderrTarget ?? processStream("stder
4885
4885
  }
4886
4886
  var defaultStderrTarget;
4887
4887
 
4888
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.30/node_modules/@gobing-ai/ts-utils/dist/index.js
4888
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.32/node_modules/@gobing-ai/ts-utils/dist/index.js
4889
4889
  var init_dist = __esm(() => {
4890
4890
  init_api_response();
4891
4891
  init_cursor();
@@ -11883,7 +11883,7 @@ var init_dist2 = __esm(() => {
11883
11883
  $visitAsync = visit.visitAsync;
11884
11884
  });
11885
11885
 
11886
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.30+fa3ca66e1fba4d51/node_modules/@gobing-ai/ts-runtime/dist/config.js
11886
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.32+180a6e424da77181/node_modules/@gobing-ai/ts-runtime/dist/config.js
11887
11887
  var configSchema;
11888
11888
  var init_config = __esm(() => {
11889
11889
  init_dist2();
@@ -11907,7 +11907,7 @@ var init_config = __esm(() => {
11907
11907
  });
11908
11908
  });
11909
11909
 
11910
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.30+fa3ca66e1fba4d51/node_modules/@gobing-ai/ts-runtime/dist/file-system-node.js
11910
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.32+180a6e424da77181/node_modules/@gobing-ai/ts-runtime/dist/file-system-node.js
11911
11911
  import { appendFileSync, cpSync, createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
11912
11912
  import { dirname, resolve as resolvePath } from "path";
11913
11913
  function createNodeFileSystem(root, paths) {
@@ -12269,7 +12269,7 @@ var PluginManifestSchema = PluginManifestSchemaBase.superRefine((manifest, ctx)
12269
12269
  });
12270
12270
  }
12271
12271
  });
12272
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.30+fa3ca66e1fba4d51/node_modules/@gobing-ai/ts-runtime/dist/index.js
12272
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.32+180a6e424da77181/node_modules/@gobing-ai/ts-runtime/dist/index.js
12273
12273
  init_file_system_node();
12274
12274
  init_config();
12275
12275
 
@@ -14400,7 +14400,7 @@ var ConfigError = class extends Error {
14400
14400
  }
14401
14401
  };
14402
14402
 
14403
- // ../../node_modules/.bun/@gobing-ai+ts-infra@0.5.30+72430983864a6768/node_modules/@gobing-ai/ts-infra/dist/logger.js
14403
+ // ../../node_modules/.bun/@gobing-ai+ts-infra@0.5.32+7037256b2d91fd75/node_modules/@gobing-ai/ts-infra/dist/logger.js
14404
14404
  var ROOT_CATEGORY = "app";
14405
14405
  function toLogTapeLevel(level) {
14406
14406
  return level === "warn" ? "warning" : level;
@@ -0,0 +1,44 @@
1
+ # Solo-podcast outline binding (task 0317 R7).
2
+ #
3
+ # The outline gate in `kk-solo-podcast` (`outline-select`) always paused. Under an auto profile the
4
+ # pipeline now asks this catalog decision over the written option files first and only pauses when
5
+ # the decision cannot answer confidently — the same pattern `kk-itc` uses for its outline slot
6
+ # (`plugins/kk/workflows/kk-itc-author.yaml` → `outline-autopick` → `itc-decision`).
7
+ #
8
+ # The fallback is a declared label because the catalog format requires one, but the workflow never
9
+ # treats a fallback as a pick: its projection reads `degraded` and defers to the operator gate when
10
+ # the answer did not come from a maker.
11
+ #
12
+ # Resolution: registered through `decisions.paths` in `.spur/config.yaml`
13
+ # (project-relative `plugins/kk/decisions`).
14
+ $schema: "@gobing-ai/spur/schemas/decision-catalog.schema.json"
15
+ version: 1
16
+
17
+ defaults:
18
+ # A low-confidence answer still names a slot; the workflow's own `degraded` check is what decides
19
+ # whether the answer is usable, so folding it into the fallback would lose the distinction.
20
+ minConfidence: 0
21
+
22
+ decisions:
23
+ kk-podcast-outline:
24
+ type: choice
25
+ description: >-
26
+ Which written outline option should this episode follow, given the briefing and the
27
+ option files as evidence?
28
+ parameters:
29
+ briefing:
30
+ type: string
31
+ description: >-
32
+ The briefing summary the options were written from (thesis, listener takeaways,
33
+ audience, target spoken duration, tone).
34
+ criteria:
35
+ a: >-
36
+ The narrative / storytelling arc — the option that leads with a story or a single
37
+ thread and earns its conclusion.
38
+ b: >-
39
+ The analytical / explainer breakdown — the option that decomposes the topic into
40
+ ordered mechanics a listener can follow.
41
+ c: >-
42
+ The third written alternative, when the option files include one; never chosen when
43
+ no `outline-option-c.md` exists.
44
+ fallback: a
@@ -0,0 +1,46 @@
1
+ # Publish-verdict decision for the daily content-safety gate (task 0317 R6).
2
+ #
3
+ # `kk-daily-safety` used to read the judge's self-reported `confidence` field in a shell `jq`
4
+ # filter on the accept edge. The verdict is now a catalog decision over the SAME artifacts as
5
+ # evidence, so the outcome is calibrated, ledgered (`decision_logs`) and carries a declared
6
+ # fallback. The fallback is `escalate`, never `pass`: a missing, unreadable or garbled report has
7
+ # to land on a recorded human decision, not on a silent publish.
8
+ #
9
+ # Resolution: registered through `decisions.paths` in `.spur/config.yaml`
10
+ # (project-relative `plugins/kk/decisions`). No maker is named here on purpose — the project
11
+ # (`decisions.makers` / `decisions.maker`) decides which backend answers, and spur degrades to this
12
+ # catalog's fallback when no maker is configured or available.
13
+ $schema: "@gobing-ai/spur/schemas/decision-catalog.schema.json"
14
+ version: 1
15
+
16
+ defaults:
17
+ # The gate's own review already screened the drafts; this decision only files the publish
18
+ # verdict, so a low-confidence model answer keeps its own value (an explicit pass/escalate/
19
+ # exclude) instead of being folded into the fallback.
20
+ minConfidence: 0
21
+
22
+ decisions:
23
+ kk-safety-verdict:
24
+ type: choice
25
+ description: >-
26
+ Final publish verdict for the reviewed article + VoiceScript pair, from the
27
+ content-safety report and the content-judge verdict as evidence.
28
+ parameters:
29
+ report:
30
+ type: string
31
+ description: >-
32
+ Summary of the safety report and the judge verdict the gate consumed (per-platform
33
+ decisions, judge findings, and the gate's own pass/escalate/exclude/revise/block
34
+ classification).
35
+ criteria:
36
+ pass: >-
37
+ Every platform decision is OK and the judge verdict is PASS on unchanged drafts — the
38
+ pair is publishable as it stands.
39
+ escalate: >-
40
+ Admitted only through a recorded human decision, or the evidence is missing, garbled or
41
+ contradicts itself. The safe landing for anything the evidence cannot settle.
42
+ exclude: >-
43
+ Publishable but not on every target — admission-only findings (per-platform rules,
44
+ formal fits) mean named platforms are dropped while the remaining targets publish at
45
+ recorded risk.
46
+ fallback: escalate
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "kk",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "knowledge-kit Claude Code plugin: skills, commands, subagents, hooks, and rules"
5
5
  }
@@ -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,49 @@ export function dailySafetyGate(args: string[]): void {
1907
1981
  });
1908
1982
  }
1909
1983
 
1984
+ /**
1985
+ * Task 0317 R6 — fold the gate's own classification, its stamp and the `kk-safety-verdict` catalog
1986
+ * decision into ONE accept/refuse token the `judge -> clean` guard reads as a single predicate
1987
+ * (ADR-115 caps that guard at 5 logical commands, so the logic lives here).
1988
+ *
1989
+ * The gate stays authoritative on `block`/`revise`; the decision can only refuse an outcome the gate
1990
+ * already admitted. The decision replaced the judge's self-reported `confidence` field, which the
1991
+ * accept guard used to re-parse in shell: its fallback is `escalate`, so missing or garbled evidence
1992
+ * lands on a recorded human decision instead of a silent publish.
1993
+ *
1994
+ * Writes `24-script/<date>_09_safety-verdict.status` = `accept`, or `refuse` after naming the
1995
+ * offending value on stderr. Never exits non-zero: routing belongs to the transitions.
1996
+ */
1997
+ export function dailySafetyVerdict(args: string[]): void {
1998
+ const [workDir = '', runDate = ''] = args;
1999
+ const statusFile = safetyPath(workDir, runDate, 'safety-judge_status.txt');
2000
+ const stampFile = safetyPath(workDir, runDate, 'safety-judge_stamp.json');
2001
+ const decisionFile = safetyPath(workDir, runDate, 'safety-verdict.decision');
2002
+ const token = safetyPath(workDir, runDate, 'safety-verdict.status');
2003
+ const ADMITTED = new Set(['pass', 'escalate', 'exclude']);
2004
+ timed(workDir, 'safety-verdict-check', () => {
2005
+ const status = isFile(statusFile) ? readFileSync(statusFile, 'utf-8').trim() : '';
2006
+ const stamp = isFile(stampFile) ? statSync(stampFile).size : 0;
2007
+ // A missing/unreadable decision row is a refusal, never an approval: `onError: continue` on
2008
+ // the decide action is what can leave it absent.
2009
+ let verdict = '';
2010
+ if (isFile(decisionFile)) {
2011
+ const row = JSON.parse(readFileSync(decisionFile, 'utf-8')) as { value?: unknown };
2012
+ verdict = String(row.value ?? '');
2013
+ }
2014
+ let refusal = '';
2015
+ if (!ADMITTED.has(status)) {
2016
+ refusal = `kk-daily-safety.judge.verdict-refused: gate status [${status}] is not an admitted outcome, so these drafts must not publish`;
2017
+ } else if (stamp === 0) {
2018
+ refusal = `kk-daily-safety.judge.verdict-refused: gate stamp missing for status ${status}, so these drafts must not publish`;
2019
+ } else if (!ADMITTED.has(verdict)) {
2020
+ refusal = `kk-daily-safety.judge.decision-refused: kk-safety-verdict [${verdict}] must admit pass|escalate|exclude, so these drafts must not publish`;
2021
+ }
2022
+ if (refusal !== '') console.error(refusal);
2023
+ sted('write', token, () => writeRaw(token, refusal === '' ? 'accept' : 'refuse'));
2024
+ });
2025
+ }
2026
+
1910
2027
  /**
1911
2028
  * Post-remediation bookkeeping for the `safety-revise` state: the agent edited the drafts, this
1912
2029
  * stage checks they are still the artifacts the rest of the pipeline consumes and spends one unit
@@ -1593,6 +1593,11 @@ if (import.meta.main)
1593
1593
  case 'collect-prepare':
1594
1594
  collectPrepare(rest);
1595
1595
  break;
1596
+ case 'daily-extract': {
1597
+ const { dailyExtract } = await loadDailyStages();
1598
+ dailyExtract(rest);
1599
+ break;
1600
+ }
1596
1601
  case 'daily-plan': {
1597
1602
  const { dailyPlan } = await loadDailyStages();
1598
1603
  dailyPlan(rest);
@@ -1643,6 +1648,11 @@ if (import.meta.main)
1643
1648
  dailySafetyGate(rest);
1644
1649
  break;
1645
1650
  }
1651
+ case 'daily-safety-verdict': {
1652
+ const { dailySafetyVerdict } = await loadDailyStages();
1653
+ dailySafetyVerdict(rest);
1654
+ break;
1655
+ }
1646
1656
  case 'daily-safety-revise': {
1647
1657
  const { dailySafetyRevise } = await loadDailyStages();
1648
1658
  dailySafetyRevise(rest);
@@ -181,14 +181,17 @@ nodes:
181
181
  kk-collect wrote, minus every story already selected or published for the ai-news topic (the
182
182
  store's `selections` ledger, written by the `select` node below). Writes
183
183
  <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.
184
+ pool writes [] and exits 0 — the run's no-content path, not a failure. The `daily-extract`
185
+ stage widens the window (×2, ×3, capped at 72h) only when the configured window's pool cannot
186
+ reach `episode_min_minutes` (2026-10-19): a published episode consumes more stories than a day
187
+ of collection supplies, so the tight window alone closed 2026-10-18/19 as no-content while
188
+ older unselected items sat in the store.
185
189
  dependsOn: [read-run-date]
186
190
  action:
187
191
  kind: shell
188
192
  options:
189
193
  command: >-
190
- kk store extract --topic ai-news --window "${vars.window}"
191
- --out "${vars.work_dir}/10-collect/pool.json"
194
+ kk stage daily-extract "${vars.work_dir}" ai-news "${vars.window}" "${vars.episode_min_minutes}" "${vars.vdriver}"
192
195
 
193
196
  - id: plan
194
197
  description: >-
@@ -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: