@gobing-ai/knowledge-kit 0.0.30 → 0.0.32

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 (66) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +10 -10
  3. package/package.json +4 -4
  4. package/plugins/generations/content-gen/dist/index.js +9 -9
  5. package/plugins/generations/core-facts-gen/dist/index.js +12 -12
  6. package/plugins/generations/daily-article-gen/dist/index.js +12 -12
  7. package/plugins/generations/dailynews-gen/dist/index.js +10 -10
  8. package/plugins/generations/dailynews-gen/src/script-builder.ts +4 -2
  9. package/plugins/generations/episode-plan-gen/dist/index.js +23 -13
  10. package/plugins/generations/episode-plan-gen/src/index.ts +23 -2
  11. package/plugins/generations/gemini-tts/dist/index.js +9 -9
  12. package/plugins/generations/image-gen/dist/index.js +9 -9
  13. package/plugins/generations/news-report-gen/dist/index.js +12 -12
  14. package/plugins/generations/voice-gen/dist/index.js +9 -9
  15. package/plugins/ingestions/aihot-ingest/dist/index.js +12 -12
  16. package/plugins/ingestions/horizon-ingest/dist/index.js +12 -12
  17. package/plugins/ingestions/karakeep-local/dist/index.js +13 -13
  18. package/plugins/ingestions/last30days-ingest/dist/index.js +12 -12
  19. package/plugins/ingestions/web-search/dist/index.js +10 -10
  20. package/plugins/kk/plugin.json +1 -1
  21. package/plugins/kk/scripts/kk-workflow-stages.ts +173 -15
  22. package/plugins/kk/skills/content-judge/SKILL.md +16 -4
  23. package/plugins/kk/skills/content-judge/references/rubrics.md +8 -2
  24. package/plugins/kk/skills/content-judge/references/workflow-integration.md +2 -1
  25. package/plugins/kk/skills/content-safety/SKILL.md +24 -13
  26. package/plugins/kk/workflows/kk-daily-ai-voice.yaml +65 -8
  27. package/plugins/publishings/emdash-pub/dist/index.js +9 -9
  28. package/plugins/publishings/infoq-pub/dist/index.js +8602 -211
  29. package/plugins/publishings/infoq-pub/package.json +3 -1
  30. package/plugins/publishings/infoq-pub/plugin.json +1 -1
  31. package/plugins/publishings/infoq-pub/src/index.ts +246 -29
  32. package/plugins/publishings/infoq-pub/src/profile-check.ts +19 -0
  33. package/plugins/publishings/juejin-pub/dist/index.js +8665 -211
  34. package/plugins/publishings/juejin-pub/package.json +3 -1
  35. package/plugins/publishings/juejin-pub/plugin.json +1 -1
  36. package/plugins/publishings/juejin-pub/src/index.ts +305 -29
  37. package/plugins/publishings/juejin-pub/src/profile-check.ts +19 -0
  38. package/plugins/publishings/medium-pub/dist/index.js +9 -9
  39. package/plugins/publishings/podcast-pub/dist/index.js +9 -9
  40. package/plugins/publishings/qiita-pub/dist/index.js +9 -9
  41. package/plugins/publishings/substack-pub/dist/index.js +8610 -211
  42. package/plugins/publishings/substack-pub/package.json +3 -1
  43. package/plugins/publishings/substack-pub/plugin.json +1 -1
  44. package/plugins/publishings/substack-pub/src/index.ts +271 -29
  45. package/plugins/publishings/substack-pub/src/profile-check.ts +74 -0
  46. package/plugins/publishings/surfdash-pub/dist/index.js +9 -9
  47. package/plugins/publishings/wechatmp-pub/dist/index.js +1227 -348
  48. package/plugins/publishings/wechatmp-pub/src/enrich.ts +229 -0
  49. package/plugins/publishings/wechatmp-pub/src/flow.ts +896 -0
  50. package/plugins/publishings/wechatmp-pub/src/index.ts +186 -516
  51. package/plugins/publishings/wechatmp-pub/src/modes.ts +403 -0
  52. package/plugins/publishings/wechatmp-pub/src/page.ts +260 -0
  53. package/plugins/publishings/wechatmp-pub/src/profile-check.ts +7 -5
  54. package/plugins/publishings/x-pub/dist/index.js +8626 -221
  55. package/plugins/publishings/x-pub/package.json +3 -1
  56. package/plugins/publishings/x-pub/plugin.json +1 -1
  57. package/plugins/publishings/x-pub/src/index.ts +258 -20
  58. package/plugins/publishings/x-pub/src/profile-check.ts +19 -0
  59. package/plugins/publishings/xhs-pub/dist/index.js +843 -346
  60. package/plugins/publishings/xhs-pub/plugin.json +1 -1
  61. package/plugins/publishings/xhs-pub/src/flow.ts +572 -0
  62. package/plugins/publishings/xhs-pub/src/index.ts +101 -593
  63. package/plugins/publishings/xhs-pub/src/modes.ts +300 -0
  64. package/plugins/publishings/xhs-pub/src/page.ts +267 -0
  65. package/plugins/publishings/xhs-pub/src/profile-check.ts +2 -3
  66. package/plugins/publishings/zenn-pub/dist/index.js +9 -9
@@ -14642,7 +14642,7 @@ var init_zod = __esm(() => {
14642
14642
  init_external();
14643
14643
  });
14644
14644
 
14645
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/errors.js
14645
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/errors.js
14646
14646
  var ErrorCode;
14647
14647
  var init_errors3 = __esm(() => {
14648
14648
  ErrorCode = {
@@ -14653,7 +14653,7 @@ var init_errors3 = __esm(() => {
14653
14653
  };
14654
14654
  });
14655
14655
 
14656
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/api-response.js
14656
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/api-response.js
14657
14657
  var API_ERROR_CODES, ERROR_CODE_TO_HTTP, CLIENT_SAFE_CODES;
14658
14658
  var init_api_response = __esm(() => {
14659
14659
  init_errors3();
@@ -14676,13 +14676,13 @@ var init_api_response = __esm(() => {
14676
14676
  CLIENT_SAFE_CODES = new Set([ErrorCode.NotFound, ErrorCode.Validation, ErrorCode.Conflict]);
14677
14677
  });
14678
14678
 
14679
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/cursor.js
14679
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/cursor.js
14680
14680
  var init_cursor = () => {};
14681
14681
 
14682
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/event-severity.js
14682
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/event-severity.js
14683
14683
  var init_event_severity = () => {};
14684
14684
 
14685
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/output.js
14685
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/output.js
14686
14686
  function processStream(name) {
14687
14687
  const proc = globalThis.process;
14688
14688
  const stream = proc?.[name];
@@ -14700,7 +14700,7 @@ function echoError(message, target = defaultStderrTarget ?? processStream("stder
14700
14700
  }
14701
14701
  var defaultStderrTarget;
14702
14702
 
14703
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/index.js
14703
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/index.js
14704
14704
  var init_dist = __esm(() => {
14705
14705
  init_api_response();
14706
14706
  init_cursor();
@@ -21698,7 +21698,7 @@ var init_dist2 = __esm(() => {
21698
21698
  $visitAsync = visit.visitAsync;
21699
21699
  });
21700
21700
 
21701
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.11/node_modules/@gobing-ai/ts-runtime/dist/config.js
21701
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.15/node_modules/@gobing-ai/ts-runtime/dist/config.js
21702
21702
  var configSchema;
21703
21703
  var init_config = __esm(() => {
21704
21704
  init_dist2();
@@ -21722,7 +21722,7 @@ var init_config = __esm(() => {
21722
21722
  });
21723
21723
  });
21724
21724
 
21725
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.11/node_modules/@gobing-ai/ts-runtime/dist/file-system-node.js
21725
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.15/node_modules/@gobing-ai/ts-runtime/dist/file-system-node.js
21726
21726
  import { appendFileSync, cpSync, createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
21727
21727
  import { dirname, resolve as resolvePath } from "path";
21728
21728
  function createNodeFileSystem(root, paths) {
@@ -21902,14 +21902,14 @@ var PluginManifestSchema = exports_external.looseObject({
21902
21902
  description: exports_external.string().optional(),
21903
21903
  operations: exports_external.record(exports_external.string(), PluginOperationSchema).optional()
21904
21904
  });
21905
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.11/node_modules/@gobing-ai/ts-runtime/dist/index.js
21905
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.15/node_modules/@gobing-ai/ts-runtime/dist/index.js
21906
21906
  init_file_system_node();
21907
21907
  init_config();
21908
21908
 
21909
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.11/node_modules/@gobing-ai/ts-runtime/dist/fs.js
21909
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.15/node_modules/@gobing-ai/ts-runtime/dist/fs.js
21910
21910
  init_file_system_node();
21911
21911
 
21912
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.11/node_modules/@gobing-ai/ts-runtime/dist/path.js
21912
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.15/node_modules/@gobing-ai/ts-runtime/dist/path.js
21913
21913
  function normalizeSeparators(path) {
21914
21914
  return path.replaceAll("\\", "/");
21915
21915
  }
@@ -21943,7 +21943,7 @@ function dirnamePath(path) {
21943
21943
  return normalized.slice(0, index);
21944
21944
  }
21945
21945
 
21946
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.11/node_modules/@gobing-ai/ts-runtime/dist/fs.js
21946
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.15/node_modules/@gobing-ai/ts-runtime/dist/fs.js
21947
21947
  async function ensureDirForFile(path, fs = createNodeFileSystem()) {
21948
21948
  await fs.ensureDir(dirnamePath(path));
21949
21949
  }
@@ -14642,7 +14642,7 @@ var init_zod = __esm(() => {
14642
14642
  init_external();
14643
14643
  });
14644
14644
 
14645
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/errors.js
14645
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/errors.js
14646
14646
  var ErrorCode;
14647
14647
  var init_errors3 = __esm(() => {
14648
14648
  ErrorCode = {
@@ -14653,7 +14653,7 @@ var init_errors3 = __esm(() => {
14653
14653
  };
14654
14654
  });
14655
14655
 
14656
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/api-response.js
14656
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/api-response.js
14657
14657
  var API_ERROR_CODES, ERROR_CODE_TO_HTTP, CLIENT_SAFE_CODES;
14658
14658
  var init_api_response = __esm(() => {
14659
14659
  init_errors3();
@@ -14676,13 +14676,13 @@ var init_api_response = __esm(() => {
14676
14676
  CLIENT_SAFE_CODES = new Set([ErrorCode.NotFound, ErrorCode.Validation, ErrorCode.Conflict]);
14677
14677
  });
14678
14678
 
14679
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/cursor.js
14679
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/cursor.js
14680
14680
  var init_cursor = () => {};
14681
14681
 
14682
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/event-severity.js
14682
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/event-severity.js
14683
14683
  var init_event_severity = () => {};
14684
14684
 
14685
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/output.js
14685
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/output.js
14686
14686
  function processStream(name) {
14687
14687
  const proc = globalThis.process;
14688
14688
  const stream = proc?.[name];
@@ -14700,7 +14700,7 @@ function echoError(message, target = defaultStderrTarget ?? processStream("stder
14700
14700
  }
14701
14701
  var defaultStderrTarget;
14702
14702
 
14703
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/index.js
14703
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/index.js
14704
14704
  var init_dist = __esm(() => {
14705
14705
  init_api_response();
14706
14706
  init_cursor();
@@ -21698,7 +21698,7 @@ var init_dist2 = __esm(() => {
21698
21698
  $visitAsync = visit.visitAsync;
21699
21699
  });
21700
21700
 
21701
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.11/node_modules/@gobing-ai/ts-runtime/dist/config.js
21701
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.15/node_modules/@gobing-ai/ts-runtime/dist/config.js
21702
21702
  var configSchema;
21703
21703
  var init_config = __esm(() => {
21704
21704
  init_dist2();
@@ -21722,7 +21722,7 @@ var init_config = __esm(() => {
21722
21722
  });
21723
21723
  });
21724
21724
 
21725
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.11/node_modules/@gobing-ai/ts-runtime/dist/file-system-node.js
21725
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.15/node_modules/@gobing-ai/ts-runtime/dist/file-system-node.js
21726
21726
  import { appendFileSync, cpSync, createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
21727
21727
  import { dirname, resolve as resolvePath } from "path";
21728
21728
  function createNodeFileSystem(root, paths) {
@@ -21901,7 +21901,7 @@ var PluginManifestSchema = exports_external.looseObject({
21901
21901
  description: exports_external.string().optional(),
21902
21902
  operations: exports_external.record(exports_external.string(), PluginOperationSchema).optional()
21903
21903
  });
21904
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.11/node_modules/@gobing-ai/ts-runtime/dist/index.js
21904
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.15/node_modules/@gobing-ai/ts-runtime/dist/index.js
21905
21905
  init_file_system_node();
21906
21906
  init_config();
21907
21907
 
@@ -24036,7 +24036,7 @@ var ConfigError = class extends Error {
24036
24036
  }
24037
24037
  };
24038
24038
 
24039
- // ../../node_modules/.bun/@gobing-ai+ts-infra@0.5.11+b4fe47d5f279554c/node_modules/@gobing-ai/ts-infra/dist/logger.js
24039
+ // ../../node_modules/.bun/@gobing-ai+ts-infra@0.5.15+c7cc99588d9d7f09/node_modules/@gobing-ai/ts-infra/dist/logger.js
24040
24040
  var ROOT_CATEGORY = "app";
24041
24041
  function toLogTapeLevel(level) {
24042
24042
  return level === "warn" ? "warning" : level;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "kk",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
4
4
  "description": "knowledge-kit Claude Code plugin: skills, commands, subagents, hooks, and rules"
5
5
  }
@@ -66,6 +66,17 @@ const RENDER_SIDECAR = 'render-md.ts';
66
66
  // $KK_WORKFLOWS_DIR -> package copy -> $HOME.
67
67
  const STYLE_SIDECAR = 'daily-voice-style.zh.md';
68
68
  const REVISE_COUNT = '.revise-count';
69
+ /** Bounded draft-remediation counter for the daily safety loop (20261005). */
70
+ const SAFETY_REVISE_COUNT = '.safety-revise-count';
71
+ /**
72
+ * Verdict criteria that describe *platform admission* (the daily's CN 资质口径 / 播报形态 rules) rather
73
+ * than a defect in the draft itself. Only these can be answered by dropping a publish target; every
74
+ * other `fail` is draft integrity, which would ship to the remaining targets too (20261005).
75
+ */
76
+ const ADMISSION_CRITERIA = new Set(['jurisdictional-rules']);
77
+ /** content-safety category 3 (可识别个人信息): a draft property, so no target choice answers it. */
78
+ const PII_FINDING_CATEGORY = 3;
79
+ const SAFETY_PLATFORM_DECISIONS = new Set(['OK', 'RISK', 'BLOCK']);
69
80
  // Post-translation title-dedup threshold (task 0138 R3). ponytail: the 2026-09-16 Gemini 3.8 Live
70
81
  // pair measures 0.417 zh title-only Jaccard, the nearest distinct pair 0.1 (4x margin). Character
71
82
  // bigrams need same-language input — pre-translate dedup over the mixed-language blend cannot work.
@@ -221,6 +232,14 @@ function bumpReviseCount(workDir: string): void {
221
232
  sted('write', path, () => writeFileSync(path, `${(Number.isNaN(previous) ? 0 : previous) + 1}\n`));
222
233
  }
223
234
 
235
+ /** `0` when `.safety-revise-count` is absent or unparsable: a fresh run revises, it never blocks on state. */
236
+ function readSafetyReviseCount(workDir: string): number {
237
+ const path = join(workDir, SAFETY_REVISE_COUNT);
238
+ if (!isFile(path)) return 0;
239
+ const value = Number.parseInt(readFileSync(path, 'utf-8'), 10);
240
+ return Number.isNaN(value) ? 0 : value;
241
+ }
242
+
224
243
  interface PrepareOptions {
225
244
  readonly workDirArg: string;
226
245
  readonly topic: string;
@@ -612,6 +631,11 @@ function dailyPrepare(args: string[]): void {
612
631
  sted('write', join(workDir, 'run-date.txt'), () =>
613
632
  writeFileSync(join(workDir, 'run-date.txt'), `${dirDate}\n`),
614
633
  );
634
+ // The safety remediation budget belongs to the attempt, not to the day: a fresh run (or a
635
+ // same-day retry) starts with the full revise allowance instead of inheriting a spent one.
636
+ sted('write', join(workDir, SAFETY_REVISE_COUNT), () =>
637
+ writeFileSync(join(workDir, SAFETY_REVISE_COUNT), '0\n'),
638
+ );
615
639
  // Anchor for the workflow: shell actions cannot set vars, so the YAML reads the
616
640
  // effective work_dir back into vars.work_dir from this cwd-relative file.
617
641
  const anchorDir = join('.spur', 'runs', 'kk-daily-ai-voice');
@@ -1005,7 +1029,11 @@ function dailyArticle(args: string[]): void {
1005
1029
  console.error(`article: dropped ${dropped.length} near-duplicate plan item(s): ${dropped.join(', ')}`);
1006
1030
  }
1007
1031
  }
1008
- if (isFile(articleMd)) {
1032
+ // The cache pair must be COMPLETE: markdown without its content.json means the generator
1033
+ // half-ran (or the json was pruned), and returning here starves every downstream stage
1034
+ // that reads content.json — publish-prep refuses to merge a draft it cannot read. Requiring
1035
+ // both artifacts makes the pair self-healing instead of permanently stuck.
1036
+ if (isFile(articleMd) && isFile(articleContent)) {
1009
1037
  console.log(`article cached: ${articleMd}`);
1010
1038
  return;
1011
1039
  }
@@ -1401,6 +1429,14 @@ function dailyPublishWechat(args: string[]): void {
1401
1429
  console.log(`publish-wechat cached: ${want} already recorded in ${result}`);
1402
1430
  return;
1403
1431
  }
1432
+ // 20261005: a platform the content-safety gate excluded never gets a draft. Skipping here
1433
+ // keeps the run alive for the targets that did pass, which is the point of the gate.
1434
+ const safety = readJson(safetyPath(workDir, runDate, 'safety-judge_stamp.json'));
1435
+ const excluded = Array.isArray(safety?.excludedPlatforms) ? (safety.excludedPlatforms as string[]) : [];
1436
+ if (excluded.includes('wechatmp')) {
1437
+ console.log(`publish-wechat: SKIPPED — content-safety excluded wechatmp for this run`);
1438
+ return;
1439
+ }
1404
1440
  if (!isFile(article)) {
1405
1441
  console.log(`publish-wechat: no article at ${article} — skip`);
1406
1442
  return;
@@ -1570,13 +1606,26 @@ function dailySafetyPrep(args: string[]): void {
1570
1606
  }
1571
1607
 
1572
1608
  /**
1573
- * Consume the content-safety report and the content-judge verdict into pass|escalate|block (exit 0
1574
- * in all three — routing is transition-owned). Fail-closed: only an explicit PASS verdict on the
1575
- * unchanged drafts, with a well-formed report carrying no BLOCK, unblocks; a RISK decision or
1576
- * `escalate: true` forces the human review regardless of the verdict score.
1609
+ * Consume the content-safety report and the content-judge verdict into a publish decision (exit 0 in
1610
+ * every case — routing is transition-owned). The gate's job is to *adjust*, not to kill the run
1611
+ * (20261005 operator decision): a non-PASS verdict first routes back through one bounded
1612
+ * draft-remediation loop (`revise`), and what survives the loop is classified by what it is:
1613
+ *
1614
+ * - `revise` — non-PASS and the remediation budget is not spent: re-review after the drafts change.
1615
+ * - `exclude` — admission-only residue (the CN 资质口径 / 播报形态 findings): the platforms that
1616
+ * BLOCKed are dropped from this run and every other target publishes normally.
1617
+ * - `escalate` — a RISK decision or `escalate: true` with an otherwise clean pass: human review.
1618
+ * - `pass` — explicit PASS verdict, unchanged drafts, no BLOCK/RISK.
1619
+ * - `block` — draft integrity: PII reported on a blocked platform, or a judge must-pass `fail`
1620
+ * outside `jurisdictional-rules` (prohibited claims / disclosure / data-handling);
1621
+ * also an unreadable or out-of-contract report/verdict, a non-PASS that documents
1622
+ * nothing, or drafts edited after the review. Never published.
1623
+ *
1624
+ * Fail-closed where it matters: a missing report, verdict or confidence still blocks.
1577
1625
  */
1578
1626
  function dailySafetyGate(args: string[]): void {
1579
- const [workDir = '', runDate = ''] = args;
1627
+ const [workDir = '', runDate = '', maxReviseArg = '0'] = args;
1628
+ const maxRevise = Number.parseInt(maxReviseArg, 10);
1580
1629
  const statusFile = safetyPath(workDir, runDate, 'safety-judge_status.txt');
1581
1630
  const stampFile = safetyPath(workDir, runDate, 'safety-judge_stamp.json');
1582
1631
  timed(workDir, 'safety-judge', () => {
@@ -1586,6 +1635,45 @@ function dailySafetyGate(args: string[]): void {
1586
1635
  const platforms = Array.isArray(report?.platforms) ? (report.platforms as Record<string, unknown>[]) : [];
1587
1636
  const decisions = platforms.map((platform) => platform?.decision);
1588
1637
  const digest = safetyDigest(workDir, runDate);
1638
+ const revisions = readSafetyReviseCount(workDir);
1639
+ // A RISK decision or an explicit escalation is a human decision even on a clean verdict.
1640
+ const risky = decisions.includes('RISK') || report?.escalate === true;
1641
+ const verdictFindings = Array.isArray(verdict?.findings) ? (verdict.findings as Record<string, unknown>[]) : [];
1642
+ const verdictFails = verdictFindings.filter((finding) => finding?.status === 'fail');
1643
+ // What a BLOCK *is* decides who can answer it. Dropping a blocked platform answers the
1644
+ // platform's own admission rules (资质口径/播报形态 …) — but the drafts' bytes ship to every
1645
+ // other target too, so a *draft* defect reported on that platform survives the drop. The
1646
+ // one finding class that is unambiguously a draft property is category 3 (可识别个人信息):
1647
+ // PII cannot be answered by choosing targets. The rest of the draft-integrity class is owned
1648
+ // by the judge's must-pass criteria below (no-prohibited-claims / disclosure-presence /
1649
+ // data-handling), which is where an independent reviewer can grade it.
1650
+ const platformFindings = (platform: Record<string, unknown>): Record<string, unknown>[] =>
1651
+ Array.isArray(platform?.findings) ? (platform.findings as Record<string, unknown>[]) : [];
1652
+ const blockedPlatforms = platforms.filter((platform) => platform?.decision === 'BLOCK');
1653
+ const excludedPlatforms = blockedPlatforms.map((platform) => String(platform?.platform ?? '')).filter(Boolean);
1654
+ const piiBlock = blockedPlatforms.some((platform) =>
1655
+ platformFindings(platform).some((finding) => Number(finding?.category) === PII_FINDING_CATEGORY),
1656
+ );
1657
+ // A BLOCK that names no finding is a contract violation, not a signal: there is nothing to
1658
+ // remediate and nothing to exclude, so it blocks (fail-closed).
1659
+ const unnamedBlock =
1660
+ blockedPlatforms.length > 0 && blockedPlatforms.every((p) => platformFindings(p).length === 0);
1661
+ const malformed =
1662
+ decisions.length === 0 || decisions.some((decision) => !SAFETY_PLATFORM_DECISIONS.has(String(decision)));
1663
+ const confidence = verdict?.confidence;
1664
+ const confidenceOk = confidence === 'HIGH' || confidence === 'MEDIUM' || confidence === 'LOW';
1665
+ const verdictValue = String(verdict?.verdict ?? '');
1666
+ const outOfContract =
1667
+ (verdictValue !== 'PASS' && verdictValue !== 'FAIL' && verdictValue !== 'NEEDS_REVISION') || !confidenceOk;
1668
+ const nonPass = verdictValue !== 'PASS' || !confidenceOk;
1669
+ // Integrity = the finding survives any target choice: PII reported on a blocked platform, an
1670
+ // out-of-contract verdict, a non-PASS that documents nothing, or a `fail` outside the
1671
+ // admission criteria.
1672
+ const integrity =
1673
+ piiBlock ||
1674
+ unnamedBlock ||
1675
+ (nonPass && verdictFindings.length === 0) ||
1676
+ verdictFails.some((finding) => !ADMISSION_CRITERIA.has(String(finding?.criterion ?? '')));
1589
1677
  let status = 'block';
1590
1678
  let reason = 'verdict PASS on unchanged drafts, no BLOCK/RISK';
1591
1679
  // A digest-mismatch block means the drafts were deliberately edited (HITL): keep them so
@@ -1595,18 +1683,44 @@ function dailySafetyGate(args: string[]): void {
1595
1683
  if (request?.digest !== digest) {
1596
1684
  reason = 'drafts changed after the review was requested';
1597
1685
  edited = true;
1598
- } else if (decisions.length === 0 || decisions.some((d) => d !== 'OK' && d !== 'RISK' && d !== 'BLOCK'))
1686
+ } else if (malformed) {
1599
1687
  reason = 'content-safety report missing or malformed';
1600
- else if (decisions.includes('BLOCK')) reason = 'content-safety report carries a BLOCK decision';
1601
- else if (verdict?.verdict !== 'PASS')
1602
- reason = `content-judge verdict is ${typeof verdict?.verdict === 'string' ? verdict.verdict : 'missing or unreadable'}`;
1603
- else if (verdict?.confidence !== 'HIGH' && verdict?.confidence !== 'MEDIUM' && verdict?.confidence !== 'LOW')
1604
- reason = 'content-judge verdict is missing a HIGH/MEDIUM/LOW confidence level';
1605
- else if (decisions.includes('RISK') || report?.escalate === true) {
1688
+ } else if (outOfContract) {
1689
+ // 20261003 hardening, unchanged: a verdict outside PASS|FAIL|NEEDS_REVISION, or any
1690
+ // verdict without a HIGH/MEDIUM/LOW confidence, is not a verdict — block, never revise
1691
+ // against it (there is no contract to read findings from).
1692
+ reason = `content-judge verdict out of contract (${verdictValue || 'missing'}/${confidenceOk ? 'ok' : 'no confidence'})`;
1693
+ } else if (nonPass || blockedPlatforms.length > 0) {
1694
+ const trigger =
1695
+ blockedPlatforms.length > 0
1696
+ ? `content-safety BLOCK on ${blockedPlatforms.map((p) => String(p?.platform ?? '')).join(', ')}`
1697
+ : `content-judge verdict ${verdictValue}`;
1698
+ if (revisions < maxRevise) {
1699
+ status = 'revise';
1700
+ reason = `${trigger} — remediation pass ${revisions + 1}/${maxRevise} (drafts re-reviewed after the edit)`;
1701
+ } else if (integrity) {
1702
+ status = 'block';
1703
+ const named = blockedPlatforms
1704
+ .flatMap((platform) => platformFindings(platform))
1705
+ .filter((finding) => Number(finding?.category) === PII_FINDING_CATEGORY)
1706
+ .map((finding) => `${String(finding?.item ?? '?')}(category ${String(finding?.category)})`)
1707
+ .join(', ');
1708
+ const criteria = verdictFails.map((finding) => String(finding?.criterion)).join(', ');
1709
+ reason = `${trigger} — draft-integrity findings survive ${revisions} remediation passes (${named || criteria || 'the verdict names no criterion'}); exclusion cannot fix these`;
1710
+ } else {
1711
+ status = 'exclude';
1712
+ reason =
1713
+ excludedPlatforms.length > 0
1714
+ ? `${trigger} — admission-only, ${revisions} remediation passes spent: ${excludedPlatforms.join(', ')} dropped from this run, other targets publish${risky ? ' (RISK/escalation recorded)' : ''}`
1715
+ : `${trigger} — admission-only, no platform BLOCK: submitting to the remaining targets at recorded risk`;
1716
+ }
1717
+ } else if (risky) {
1606
1718
  status = 'escalate';
1607
1719
  reason = 'RISK decision or escalation — human review required';
1608
1720
  } else status = 'pass';
1609
- if (status !== 'block') sted('write', stampFile, () => writeJsonText(stampFile, { digest, status }));
1721
+ if (status === 'pass' || status === 'escalate' || status === 'exclude') {
1722
+ sted('write', stampFile, () => writeJsonText(stampFile, { digest, status, excludedPlatforms }));
1723
+ }
1610
1724
  if (status === 'block' && !edited) {
1611
1725
  // A quality block poisons the isFile() draft cache: every same-day retry would
1612
1726
  // re-fail on unchanged bytes (20261002 dogfood — two judge-fail runs replayed until
@@ -1626,6 +1740,32 @@ function dailySafetyGate(args: string[]): void {
1626
1740
  });
1627
1741
  }
1628
1742
 
1743
+ /**
1744
+ * Post-remediation bookkeeping for the `safety-revise` state: the agent edited the drafts, this
1745
+ * stage checks they are still the artifacts the rest of the pipeline consumes and spends one unit
1746
+ * of the remediation budget (`daily-safety-gate` reads it to choose revise-vs-exclude). Validating
1747
+ * here fails inside the loop instead of at `wrap-docs`, after the run has moved on. A broken
1748
+ * revision spends the budget too: an unparsable draft must not be retried forever.
1749
+ */
1750
+ function dailySafetyRevise(args: string[]): void {
1751
+ const [workDir = '', runDate = ''] = args;
1752
+ timed(workDir, 'safety-revise', () => {
1753
+ const [articlePath, scriptPath] = safetyDraftPaths(workDir, runDate);
1754
+ const article = requireJson(articlePath, 'safety-revise');
1755
+ for (const field of ['title', 'body'] as const) {
1756
+ const value = article[field];
1757
+ if (typeof value !== 'string' || value.trim() === '') {
1758
+ failWith(1, `safety-revise: article ${field} must be a non-empty string in ${articlePath}`);
1759
+ }
1760
+ }
1761
+ assertScriptContent(scriptPath);
1762
+ const path = join(workDir, SAFETY_REVISE_COUNT);
1763
+ const next = readSafetyReviseCount(workDir) + 1;
1764
+ sted('write', path, () => writeFileSync(path, `${next}\n`));
1765
+ console.log(`safety-revise: remediation pass ${next} applied; drafts re-validated`);
1766
+ });
1767
+ }
1768
+
1629
1769
  function dailyWrapDocs(args: string[]): void {
1630
1770
  const [workDir = '', runDate = '', voiceProfile = ''] = args;
1631
1771
  const scriptPath = join(workDir, '24-script', `${runDate}_08_script_voicescript.yaml`);
@@ -1953,10 +2093,25 @@ function dailyPublishPrep(args: string[]): void {
1953
2093
  // stamp issued on different bytes (edited after PASS, or an older draft), never publishes.
1954
2094
  const stamp = readJson(safetyPath(workDir, runDate, 'safety-judge_stamp.json'));
1955
2095
  if (stamp === null) failWith(1, 'publish-prep: no content-safety verdict stamp for this run — not publishing');
2096
+ // 20261005: pass/escalate/exclude all stamp the drafts as publishable; only `block` (draft
2097
+ // integrity) has no stamp. An `exclude` stamp carries the admission-blocked platforms so
2098
+ // the per-target stages can skip those targets instead of failing the run.
2099
+ if (stamp.status !== 'pass' && stamp.status !== 'escalate' && stamp.status !== 'exclude') {
2100
+ failWith(1, `publish-prep: content-safety status ${String(stamp.status)} does not publish`);
2101
+ }
1956
2102
  if (stamp.digest !== safetyDigest(workDir, runDate)) {
1957
2103
  failWith(1, 'publish-prep: drafts changed after the content-safety verdict — re-run the review');
1958
2104
  }
1959
- sted('write', prepPath, () => writeJsonText(prepPath, merged));
2105
+ const excludedPlatforms = Array.isArray(stamp.excludedPlatforms) ? (stamp.excludedPlatforms as string[]) : [];
2106
+ if (excludedPlatforms.length > 0) {
2107
+ console.log(`publish-prep: content-safety excluded platforms: ${excludedPlatforms.join(', ')}`);
2108
+ }
2109
+ sted('write', prepPath, () =>
2110
+ writeJsonText(prepPath, {
2111
+ ...merged,
2112
+ metadata: { ...metadataOf(merged), safetyExcludedPlatforms: excludedPlatforms },
2113
+ }),
2114
+ );
1960
2115
  });
1961
2116
  }
1962
2117
 
@@ -2640,6 +2795,9 @@ if (import.meta.main)
2640
2795
  case 'daily-safety-gate':
2641
2796
  dailySafetyGate(rest);
2642
2797
  break;
2798
+ case 'daily-safety-revise':
2799
+ dailySafetyRevise(rest);
2800
+ break;
2643
2801
  case 'daily-wrap-docs':
2644
2802
  dailyWrapDocs(rest);
2645
2803
  break;
@@ -55,8 +55,12 @@ Every evaluation writes a JSON verdict file. The default path is
55
55
  | `FAIL` | Content violates a must-pass criterion (safety, accuracy, compliance) | Block; surface findings; do not publish |
56
56
  | `NEEDS_REVISION` | Content is salvageable but below bar on quality criteria | Block; feedback drives a bounded regenerate-retry loop |
57
57
 
58
- `PASS` is the only verdict that unblocks publish. The shell guard treats `FAIL` and
59
- `NEEDS_REVISION` identically (exit 1); the distinction is for the feedback loop, not the gate.
58
+ `PASS` is the only verdict that unblocks publish outright. The shell guard treats `FAIL` and
59
+ `NEEDS_REVISION` identically (exit 1); the distinction is for the feedback loop, not the gate — a
60
+ workflow may spend a bounded revision budget on either and then classify the residue (20261005:
61
+ `kk-daily-ai-voice` routes non-PASS through `safety-revise` and publishes only the targets whose
62
+ platform did not BLOCK). A `fail` means a rule was *violated*, not merely suspected: see
63
+ "Evidence discipline".
60
64
 
61
65
  ### Confidence level
62
66
 
@@ -81,8 +85,10 @@ outside `HIGH|MEDIUM|LOW` is not a usable verdict anywhere:
81
85
  a valid confidence; an absent verdict file keeps the dry-run convention.
82
86
 
83
87
  Add that filter to any new judge gate; `references/workflow-integration.md` carries the pattern and
84
- `plugins/kk/tests/verdict-confidence.test.ts` fails when a workflow accepts a judge `PASS` without
85
- checking the enum.
88
+ `plugins/kk/tests/verdict-confidence.test.ts` fails on either half of the contract: a workflow that
89
+ accepts a judge `PASS` without checking the enum, and this skill drifting out of the producer side
90
+ (enum line, per-level definition, or a reference snippet whose filter no longer matches the guards
91
+ the consumer scan collects).
86
92
 
87
93
  The level itself does not change routing — escalation stays owned by the review's `RISK` decisions
88
94
  and `escalate` flag; treat `LOW` as a signal to demand human review before publishing.
@@ -111,6 +117,12 @@ and `escalate` flag; treat `LOW` as a signal to demand human review before publi
111
117
  contradicted by source Y" is.
112
118
  - Never fabricate sources. If a claim cannot be verified against the provided docs/brief, mark it
113
119
  `warn` (unverified), not `fail`, unless the rubric classifies unverified claims as must-pass.
120
+ - **A rule you cannot verify is not a rule you may `fail` on.** When a finding rests on rule text
121
+ that is unverified, out of date, or an editorial stance rather than platform/governing text — the
122
+ upstream report marks these `evidenceGap` — grade the criterion `warn` and escalate. `fail` is
123
+ reserved for a violation of rule text you actually verified. Downgrading here is what keeps a
124
+ gate a gate: an unverifiable must-pass `fail` is unanswerable, so it can neither be remediated nor
125
+ honestly cleared.
114
126
 
115
127
  ## Rubrics
116
128
 
@@ -89,8 +89,14 @@ Thresholds: `pass_threshold = 0.85`
89
89
  - `no-prohibited-claims` is strict: a single prohibited claim is an immediate `FAIL`, regardless
90
90
  of overall score. Examples: unverified health/financial advice, guarantees of outcome,
91
91
  - `jurisdictional-rules`: the wrapper supplies the jurisdiction (e.g., `US-SEC`, `EU-GDPR`,
92
- `CN-CAC`). If none is supplied, mark `warn` and note "no jurisdiction specified" — do not
93
- guess a jurisdiction and fail against a guessed rule.
92
+ `CN-CAC`, or the platform reference files a distribution gate names). If none is supplied, mark
93
+ `warn` and note "no jurisdiction specified" — do not guess a jurisdiction and fail against a
94
+ guessed rule.
95
+ Grade `fail` only against rule text verified for that jurisdiction. A rule the wrapper or its
96
+ process marks `evidenceGap` (unverified text, a stale `Last verified` date, or an editorial
97
+ stance rather than platform/governing text) is `warn` + escalate: unverifiable rule text cannot
98
+ be a must-pass failure, because no revision can clear it (20261005 — the daily gate blocked
99
+ every run on the evidence-gapped CN 资质口径 stance until this was fixed).
94
100
  - `data-handling`: scan for PII that may have been carried through from input Docs (emails,
95
101
  phone numbers, national IDs). Leakage is a `fail`.
96
102
 
@@ -35,7 +35,8 @@ if [ ! -f "$verdict_path" ]; then
35
35
  fi
36
36
  # Confidence-aware read: a PASS without a HIGH|MEDIUM|LOW confidence becomes `PASS!`, which no
37
37
  # accept guard matches and the catch-all below rejects. Keep this filter byte-identical wherever a
38
- # guard reads a judge verdict (`plugins/kk/tests/verdict-confidence.test.ts` enforces that).
38
+ # guard reads a judge verdict — and to this snippet: `plugins/kk/tests/verdict-confidence.test.ts`
39
+ # collects every guard's filter plus this one and fails unless they are byte-identical.
39
40
  verdict=$(jq -r 'if (.verdict // "") == "PASS" then (.verdict + (if ((.confidence // "") | test("^(HIGH|MEDIUM|LOW)$")) then "" else "!" end)) else (.verdict // "") end' "$verdict_path")
40
41
  case "$verdict" in
41
42
  PASS)
@@ -45,7 +45,9 @@ prefer exclusion or abort over any rewording.
45
45
  - **detect-only** (default): emit the report; touch nothing.
46
46
  - **detect+revise**: additionally emit a proposed revision for findings in categories 3–5 where
47
47
  the fix is deletion/desensitization/disclosure. Category-2 findings get exclusion (drop the
48
- item) or abort — prefer exclusion over any rewrite.
48
+ item) or abort — prefer exclusion over any rewrite. The `kk-daily-ai-voice` `safety-revise`
49
+ state calls this mode: the revision proposals are what the agent applies to the drafts before the
50
+ re-review (the platform level is what gets excluded when a category-2 finding survives).
49
51
 
50
52
  A revision proposal is a **proposal**. The operator/agent that owns the artifact applies it —
51
53
  keep originals intact and write the revision to a new artifact — and the revised artifact must
@@ -130,8 +132,9 @@ rule. Never emit `OK` with `evidenceGap` findings; that combination is a contrac
130
132
  - After a detect+revise revision is applied, the revised Content is re-evaluated by content-judge
131
133
  (rubric `compliance`) — a revision **never auto-PASSes**; the previous verdict is stale and the
132
134
  gate re-runs.
133
- - A content-judge `FAIL` on safety is terminal for that artifact in the pipeline: FAIL /
134
- NEEDS_REVISION / missing-verdict states must not auto-publish.
135
+ - A content-judge verdict never publishes on its own: the workflow's gate decides. `kk-daily-ai-voice`
136
+ spends a bounded revision budget on `FAIL`/`NEEDS_REVISION`, blocks only findings that travel with
137
+ the draft, and excludes the admission-blocked platforms instead of halting the run (20261005).
135
138
 
136
139
  ## Inputs
137
140
 
@@ -149,15 +152,23 @@ credentials, and no platform word lists are embedded in any caller or in TypeScr
149
152
 
150
153
  ## Workflow integration
151
154
 
152
- The `kk-daily-ai-voice` workflow runs this skill in `detect-only` mode on every publishing run
153
- (`safety-review` state, task 0189 R10): it reads the review request for the target `platforms`
154
- and writes the report JSON; `kk:content-judge` (`compliance` rubric) then re-checks the same
155
- drafts independently, and the `daily-safety-gate` stage consumes both. The program reads only
156
- `platforms[].decision`, `escalate`, and the judge's `verdict` — a `BLOCK`, a non-`PASS` verdict,
157
- or a missing/unreadable file stops the run; a `RISK` or `escalate: true` forces the human review
158
- even when the run is otherwise unattended. The report is bound to the exact draft bytes: editing a
159
- draft after the verdict invalidates it. The earlier deterministic gate is the topic-boundary QC
160
- stage (`plugins/kk/scripts/topic-boundary.json` via `daily-qc-content`); this skill covers the
161
- judgment-shaped residue that gate cannot express. Step shape:
155
+ The `kk-daily-ai-voice` workflow runs this skill on every publishing run (task 0189 R10; revise
156
+ loop 20261005): `safety-review` reads the request for the target `platforms` and writes the report
157
+ (`detect-only`); `kk:content-judge` (`compliance` rubric) re-checks the same drafts independently;
158
+ then `daily-safety-gate` consumes both. The gate **alerts and adjusts rather than stopping the run**:
159
+ a non-PASS verdict spends a bounded budget in the `safety-revise` state, where this skill runs in
160
+ `detect+revise` mode and the agent applies the proposals to the article and the VoiceScript before
161
+ `safety-review` re-opens on the edited bytes. What survives the budget is classified — findings that
162
+ travel with the draft still block (a PII finding on a blocked platform, or a judge must-pass `fail`
163
+ outside the admission criteria: prohibited claims / disclosure / data-handling), while
164
+ **admission-only findings** (the CN 资质口径 / 播报形态 rules) drop the platform
165
+ they name from that run and let every other target publish; the drop is recorded in the verdict
166
+ stamp's `excludedPlatforms` and honoured by the per-target publish stages. A `RISK` decision or
167
+ `escalate: true` still forces the human review even when the run is otherwise unattended.
168
+
169
+ The report is bound to the exact draft bytes: editing a draft after the verdict invalidates it, and
170
+ the remediation loop re-requests a fresh review for that reason. The earlier deterministic gate is
171
+ the topic-boundary QC stage (`plugins/kk/scripts/topic-boundary.json` via `daily-qc-content`); this
172
+ skill covers the judgment-shaped residue that gate cannot express. Step shape:
162
173
  `plugins/kk/skills/content-judge/references/workflow-integration.md`. Outside the workflow the
163
174
  skill stays usable ad hoc on any draft.