@gobing-ai/knowledge-kit 0.1.11 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +2359 -1721
  3. package/package.json +1 -1
  4. package/plugins/generations/image-gen/README.md +108 -0
  5. package/plugins/generations/image-gen/assets/sample/cover__clean-webapp-ui.jpg +0 -0
  6. package/plugins/generations/image-gen/assets/sample/cover__cute.jpg +0 -0
  7. package/plugins/generations/image-gen/assets/sample/cover__daily-news.jpg +0 -0
  8. package/plugins/generations/image-gen/assets/sample/cover__editorial.jpg +0 -0
  9. package/plugins/generations/image-gen/assets/sample/cover__fresh.jpg +0 -0
  10. package/plugins/generations/image-gen/assets/sample/cover__minimalist.jpg +0 -0
  11. package/plugins/generations/image-gen/assets/sample/cover__photorealistic.jpg +0 -0
  12. package/plugins/generations/image-gen/assets/sample/cover__sketch.jpg +0 -0
  13. package/plugins/generations/image-gen/assets/sample/cover__technical-diagram.jpg +0 -0
  14. package/plugins/generations/image-gen/assets/sample/cover__vibrant.jpg +0 -0
  15. package/plugins/generations/image-gen/assets/sample/cover__warm.jpg +0 -0
  16. package/plugins/generations/image-gen/assets/sample/cover__xhs-knowledge.jpg +0 -0
  17. package/plugins/generations/image-gen/assets/sample/daily-news-cover__clean-webapp-ui.jpg +0 -0
  18. package/plugins/generations/image-gen/assets/sample/daily-news-cover__cute.jpg +0 -0
  19. package/plugins/generations/image-gen/assets/sample/daily-news-cover__daily-news.jpg +0 -0
  20. package/plugins/generations/image-gen/assets/sample/daily-news-cover__editorial.jpg +0 -0
  21. package/plugins/generations/image-gen/assets/sample/daily-news-cover__fresh.jpg +0 -0
  22. package/plugins/generations/image-gen/assets/sample/daily-news-cover__minimalist.jpg +0 -0
  23. package/plugins/generations/image-gen/assets/sample/daily-news-cover__photorealistic.jpg +0 -0
  24. package/plugins/generations/image-gen/assets/sample/daily-news-cover__sketch.jpg +0 -0
  25. package/plugins/generations/image-gen/assets/sample/daily-news-cover__technical-diagram.jpg +0 -0
  26. package/plugins/generations/image-gen/assets/sample/daily-news-cover__vibrant.jpg +0 -0
  27. package/plugins/generations/image-gen/assets/sample/daily-news-cover__warm.jpg +0 -0
  28. package/plugins/generations/image-gen/assets/sample/daily-news-cover__xhs-knowledge.jpg +0 -0
  29. package/plugins/generations/image-gen/assets/sample/free__clean-webapp-ui.jpg +0 -0
  30. package/plugins/generations/image-gen/assets/sample/free__cute.jpg +0 -0
  31. package/plugins/generations/image-gen/assets/sample/free__daily-news.jpg +0 -0
  32. package/plugins/generations/image-gen/assets/sample/free__editorial.jpg +0 -0
  33. package/plugins/generations/image-gen/assets/sample/free__fresh.jpg +0 -0
  34. package/plugins/generations/image-gen/assets/sample/free__minimalist.jpg +0 -0
  35. package/plugins/generations/image-gen/assets/sample/free__photorealistic.jpg +0 -0
  36. package/plugins/generations/image-gen/assets/sample/free__sketch.jpg +0 -0
  37. package/plugins/generations/image-gen/assets/sample/free__technical-diagram.jpg +0 -0
  38. package/plugins/generations/image-gen/assets/sample/free__vibrant.jpg +0 -0
  39. package/plugins/generations/image-gen/assets/sample/free__warm.jpg +0 -0
  40. package/plugins/generations/image-gen/assets/sample/free__xhs-knowledge.jpg +0 -0
  41. package/plugins/generations/image-gen/assets/sample/illustration__clean-webapp-ui.jpg +0 -0
  42. package/plugins/generations/image-gen/assets/sample/illustration__cute.jpg +0 -0
  43. package/plugins/generations/image-gen/assets/sample/illustration__daily-news.jpg +0 -0
  44. package/plugins/generations/image-gen/assets/sample/illustration__editorial.jpg +0 -0
  45. package/plugins/generations/image-gen/assets/sample/illustration__fresh.jpg +0 -0
  46. package/plugins/generations/image-gen/assets/sample/illustration__minimalist.jpg +0 -0
  47. package/plugins/generations/image-gen/assets/sample/illustration__photorealistic.jpg +0 -0
  48. package/plugins/generations/image-gen/assets/sample/illustration__sketch.jpg +0 -0
  49. package/plugins/generations/image-gen/assets/sample/illustration__technical-diagram.jpg +0 -0
  50. package/plugins/generations/image-gen/assets/sample/illustration__vibrant.jpg +0 -0
  51. package/plugins/generations/image-gen/assets/sample/illustration__warm.jpg +0 -0
  52. package/plugins/generations/image-gen/assets/sample/illustration__xhs-knowledge.jpg +0 -0
  53. package/plugins/generations/image-gen/assets/sample/xhs-card__clean-webapp-ui.jpg +0 -0
  54. package/plugins/generations/image-gen/assets/sample/xhs-card__cute.jpg +0 -0
  55. package/plugins/generations/image-gen/assets/sample/xhs-card__daily-news.jpg +0 -0
  56. package/plugins/generations/image-gen/assets/sample/xhs-card__editorial.jpg +0 -0
  57. package/plugins/generations/image-gen/assets/sample/xhs-card__fresh.jpg +0 -0
  58. package/plugins/generations/image-gen/assets/sample/xhs-card__minimalist.jpg +0 -0
  59. package/plugins/generations/image-gen/assets/sample/xhs-card__photorealistic.jpg +0 -0
  60. package/plugins/generations/image-gen/assets/sample/xhs-card__sketch.jpg +0 -0
  61. package/plugins/generations/image-gen/assets/sample/xhs-card__technical-diagram.jpg +0 -0
  62. package/plugins/generations/image-gen/assets/sample/xhs-card__vibrant.jpg +0 -0
  63. package/plugins/generations/image-gen/assets/sample/xhs-card__warm.jpg +0 -0
  64. package/plugins/generations/image-gen/assets/sample/xhs-card__xhs-knowledge.jpg +0 -0
  65. package/plugins/generations/image-gen/dist/index.js +102 -58
  66. package/plugins/generations/image-gen/scripts/compose-e2e-artifact.ts +5 -21
  67. package/plugins/generations/image-gen/scripts/gen-samples.ts +208 -0
  68. package/plugins/generations/image-gen/src/compose/chromium.ts +2 -3
  69. package/plugins/generations/image-gen/src/presets.ts +6 -3
  70. package/plugins/generations/image-gen/src/providers/errors.ts +16 -1
  71. package/plugins/generations/image-gen/src/providers/minimax.ts +46 -17
  72. package/plugins/ingestions/web-search/src/index.ts +1 -1
  73. package/plugins/kk/README.md +18 -0
  74. package/plugins/kk/commands/image-extract.md +1 -0
  75. package/plugins/kk/commands/image-generate.md +1 -0
  76. package/plugins/kk/commands/tell-me.md +1 -0
  77. package/plugins/kk/commands/workflow-run.md +7 -1
  78. package/plugins/kk/decisions/extract/it-topics.yaml +24 -0
  79. package/plugins/kk/decisions/kk-itc.yaml +6 -4
  80. package/plugins/kk/plugin.json +1 -1
  81. package/plugins/kk/publish-presets/daily-ai-voice-migration.json +146 -0
  82. package/plugins/kk/publish-presets/daily-ai-voice.json +11 -3
  83. package/plugins/kk/schedules/kk-collect.grok-bot.md.tmpl +69 -0
  84. package/plugins/kk/schedules/kk-daily-ai-voice3.cron.tmpl +18 -0
  85. package/plugins/kk/schedules/kk-daily-publish3-cn-peak.cron.tmpl +15 -5
  86. package/plugins/kk/schedules/kk-daily-publish3-overseas.cron.tmpl +9 -4
  87. package/plugins/kk/scripts/daily-stages.ts +346 -48
  88. package/plugins/kk/scripts/kk-workflow-stages.ts +190 -0
  89. package/plugins/kk/scripts/safety-artifact-check.ts +70 -28
  90. package/plugins/kk/scripts/voice3-publish-window.ts +202 -0
  91. package/plugins/kk/skills/article-adapt/SKILL.md +1 -0
  92. package/plugins/kk/skills/audio-authoring/SKILL.md +1 -0
  93. package/plugins/kk/skills/content-judge/SKILL.md +1 -0
  94. package/plugins/kk/skills/content-safety/SKILL.md +1 -0
  95. package/plugins/kk/skills/explain-things/SKILL.md +1 -0
  96. package/plugins/kk/skills/image-authoring/SKILL.md +1 -0
  97. package/plugins/kk/skills/itc-generating/SKILL.md +1 -0
  98. package/plugins/kk/skills/publish/SKILL.md +1 -0
  99. package/plugins/kk/skills/smart-seo/SKILL.md +1 -0
  100. package/plugins/kk/skills/storm-research/SKILL.md +1 -0
  101. package/plugins/kk/skills/taste-unslop/SKILL.md +1 -0
  102. package/plugins/kk/skills/topic/SKILL.md +1 -0
  103. package/plugins/kk/skills/web-automation/SKILL.md +1 -0
  104. package/plugins/kk/skills/xhs-images/SKILL.md +1 -0
  105. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +9 -7
  106. package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +161 -61
  107. package/plugins/kk/workflows/kk-daily-publish3.yaml +82 -26
  108. package/plugins/kk/workflows/kk-daily-safety.yaml +39 -31
  109. package/plugins/kk/workflows/kk-itc-author.yaml +75 -0
  110. package/plugins/kk/workflows/kk-itc-publish-tail.yaml +25 -3
  111. package/plugins/kk/workflows/kk-itc.yaml +36 -4
  112. package/plugins/publishings/linkedin-pub/dist/index.js +21333 -0
  113. package/plugins/publishings/linkedin-pub/dist/profile-check.js +21005 -0
  114. package/plugins/publishings/linkedin-pub/package.json +19 -0
  115. package/plugins/publishings/linkedin-pub/plugin.json +12 -0
  116. package/plugins/publishings/linkedin-pub/src/index.ts +705 -0
  117. package/plugins/publishings/linkedin-pub/src/profile-check.ts +17 -0
  118. package/plugins/publishings/linkedin-pub/tsconfig.json +4 -0
  119. package/plugins/publishings/surfdash-pub/dist/index.js +1 -1
  120. package/plugins/publishings/surfdash-pub/src/index.ts +3 -1
  121. package/plugins/publishings/threads-pub/dist/index.js +7 -0
  122. package/plugins/publishings/threads-pub/src/index.ts +12 -0
  123. package/plugins/publishings/wechatmp-pub/dist/index.js +5 -2
  124. package/plugins/publishings/wechatmp-pub/src/flow.ts +11 -3
  125. package/plugins/publishings/x-pub/dist/index.js +7 -0
  126. package/plugins/publishings/x-pub/src/index.ts +11 -0
@@ -1442,6 +1442,180 @@ function dailyPublishPartial(args: string[]): void {
1442
1442
  * `KK_SURFDASH_PATCH_OFFLINE=1` keeps fixture tests hermetic — the plugin would otherwise write to,
1443
1443
  * commit and push a real checkout.
1444
1444
  */
1445
+ /**
1446
+ * `publish-matrix-partial` (0333): a partial/none window leaves an explicit recovery marker.
1447
+ *
1448
+ * Reads the window's receipt-derived outcome (written by `publish-matrix-classify`) — the legacy
1449
+ * `_17_publish_result.json` aggregate is never an input — names the exact failed target ids, and
1450
+ * points at the matrix rerun contract (`kk stage run publish-matrix-send <work_dir> --window <w>
1451
+ * --retry`). The rerun is receipt-scoped: `--retry` resends only effect-none failures, never a
1452
+ * succeeded or live-uncertain row, so there is no `--retry-from`/`_17` style file replay. A full
1453
+ * window deletes any stale marker from a previous attempt, so the report never re-flags recovered
1454
+ * targets.
1455
+ */
1456
+ function publishMatrixPartial(args: string[]): void {
1457
+ const flag = (name: string): string | undefined => {
1458
+ const i = args.indexOf(name);
1459
+ return i >= 0 ? args[i + 1] : undefined;
1460
+ };
1461
+ const workDir = args[0] ?? '';
1462
+ const window = flag('--window');
1463
+ if (workDir === '' || window === undefined || window === '') {
1464
+ failWith(1, 'usage: publish-matrix-partial <work_dir> --window <w>');
1465
+ }
1466
+ const outcomePath = join(workDir, '4-publish', `outcome.${window}.json`);
1467
+ const outcome = requireJson(outcomePath, 'publish-matrix-partial') as {
1468
+ status?: string;
1469
+ rows?: Array<{ id?: string; status?: string; dummy?: boolean }>;
1470
+ };
1471
+ const marker = join(workDir, '4-publish', `partial-marker.${window}.txt`);
1472
+ timed(workDir, 'publish-matrix-partial', () => {
1473
+ if (outcome.status === 'full') {
1474
+ if (isFile(marker)) sted('rm', marker, () => unlinkSync(marker));
1475
+ return;
1476
+ }
1477
+ const failed = (outcome.rows ?? [])
1478
+ .filter((row) => row.dummy !== true && row.status !== 'succeeded')
1479
+ .map((row) => row.id ?? '(unknown)')
1480
+ .join(',');
1481
+ sted('mkdir', dirname(marker), () => mkdirSync(dirname(marker), { recursive: true }));
1482
+ sted('write', marker, () =>
1483
+ writeRaw(
1484
+ marker,
1485
+ `window ${window} ended ${outcome.status ?? 'unknown'}: failed targets: ${failed === '' ? '(unparsed)' : failed}\n` +
1486
+ `recovery: kk stage run publish-matrix-send ${workDir} --window ${window} --retry (resends effect-none failures only; receipts scope the retry)\n`,
1487
+ ),
1488
+ );
1489
+ console.error(`WARNING: partial publish — failed targets: ${failed === '' ? '(unparsed)' : failed}`);
1490
+ });
1491
+ }
1492
+
1493
+ /**
1494
+ * `publish-matrix-patch-surfdash` (0333): patch podcast links into a real surfdash post.
1495
+ *
1496
+ * The patch payload is derived from the window's own receipts, not from legacy aggregate files and
1497
+ * not from a `KK_SURFDASH_ROOT` argument — the plugin resolves its checkout itself. Evidence rules:
1498
+ * the surfdash row must have a real (non-dummy) succeeded receipt whose aggregate metadata carries
1499
+ * an absolute `postPath`; without that there is nothing to patch and the stage records an explicit
1500
+ * skip result instead of fabricating one. Podcast links ride along only when the podcast row is
1501
+ * real too — a dummy or missing episode yields explicit empty `coverUrl`/`showNotesUrl`, never a
1502
+ * fabricated link. `KK_SURFDASH_PATCH_OFFLINE=1` keeps rehearsal runs hermetic the same way the
1503
+ * legacy voice2 stage does.
1504
+ */
1505
+ function publishMatrixPatchSurfdash(args: string[]): void {
1506
+ const flag = (name: string): string | undefined => {
1507
+ const i = args.indexOf(name);
1508
+ return i >= 0 ? args[i + 1] : undefined;
1509
+ };
1510
+ const workDir = args[0] ?? '';
1511
+ const window = flag('--window');
1512
+ if (workDir === '' || window === undefined || window === '') {
1513
+ failWith(1, 'usage: publish-matrix-patch-surfdash <work_dir> --window <w>');
1514
+ }
1515
+ const publishDir = join(workDir, '4-publish');
1516
+ const resultPath = join(publishDir, 'patch-surfdash.result.json');
1517
+ const writeResult = (value: Record<string, unknown>): void => {
1518
+ sted('write', resultPath, () => writeJqText(resultPath, value));
1519
+ };
1520
+ const receipt = (id: string): Record<string, unknown> | null =>
1521
+ readJson(join(publishDir, 'receipts', `${id}.json`));
1522
+ const aggregateTarget = (receiptPath: string | undefined): Record<string, unknown> => {
1523
+ const aggregate = receiptPath === undefined ? null : readJson(receiptPath);
1524
+ const targets = aggregate?.targets;
1525
+ if (!Array.isArray(targets) || targets.length === 0 || targets[0] === null || typeof targets[0] !== 'object') {
1526
+ return {};
1527
+ }
1528
+ return targets[0] as Record<string, unknown>;
1529
+ };
1530
+ timed(workDir, 'publish-matrix-patch-surfdash', () => {
1531
+ const outcome = readJson(join(publishDir, `outcome.${window}.json`)) as {
1532
+ rows?: Array<{ id?: string }>;
1533
+ } | null;
1534
+ const rowId = (prefix: string): string | undefined =>
1535
+ (outcome?.rows ?? []).map((row) => row.id ?? '').find((id) => id.startsWith(prefix));
1536
+ const sdId = rowId('surfdash-pub');
1537
+ const podId = rowId('podcast-pub');
1538
+ if (sdId === undefined) {
1539
+ const error = 'skipped: no surfdash target in this window — nothing to patch';
1540
+ writeResult({ ok: false, target: 'surfdash-pub', error });
1541
+ console.error(`patch-surfdash: ${error}`);
1542
+ return;
1543
+ }
1544
+ const sdReceipt = receipt(sdId);
1545
+ const sdReal = sdReceipt !== null && sdReceipt?.dummy !== true && sdReceipt?.status === 'succeeded';
1546
+ const sdTarget = aggregateTarget(
1547
+ typeof sdReceipt?.aggregatePath === 'string' ? sdReceipt.aggregatePath : undefined,
1548
+ );
1549
+ const postPath = (sdTarget.metadata as Record<string, unknown> | undefined)?.postPath;
1550
+ if (!sdReal || typeof postPath !== 'string' || postPath === '') {
1551
+ const error =
1552
+ sdReceipt === null
1553
+ ? 'skipped: no surfdash publish receipt — nothing to patch'
1554
+ : typeof postPath !== 'string' || postPath === ''
1555
+ ? 'skipped: surfdash receipt carries no real postPath — nothing to patch'
1556
+ : 'skipped: surfdash receipt is dummy — nothing to patch';
1557
+ writeResult({ ok: false, target: 'surfdash-pub', error });
1558
+ console.error(`patch-surfdash: ${error}`);
1559
+ return;
1560
+ }
1561
+ // Links ride along only from real podcast evidence; dummy/missing episodes patch as
1562
+ // explicit empty strings — the patch must never fabricate a link.
1563
+ const podReceipt = podId === undefined ? null : receipt(podId);
1564
+ const podReal = podReceipt !== null && podReceipt?.dummy !== true && podReceipt?.status === 'succeeded';
1565
+ const podTarget = aggregateTarget(
1566
+ typeof podReceipt?.aggregatePath === 'string' ? podReceipt.aggregatePath : undefined,
1567
+ );
1568
+ const podMeta = (podTarget.metadata ?? {}) as Record<string, unknown>;
1569
+ const runDate = String(readJson(join(publishDir, `outcome.${window}.json`))?.runDate ?? '');
1570
+ const payload = join(publishDir, 'patch-surfdash.payload.json');
1571
+ sted('write', payload, () =>
1572
+ writeJqText(payload, {
1573
+ body: '',
1574
+ options: {
1575
+ operation: 'patch',
1576
+ postPath,
1577
+ coverUrl: podReal && typeof podMeta.imageUrl === 'string' ? podMeta.imageUrl : '',
1578
+ showNotesUrl: podReal && typeof podTarget.url === 'string' ? podTarget.url : '',
1579
+ ...(runDate.length === 8
1580
+ ? { dateIso: `${runDate.slice(0, 4)}-${runDate.slice(4, 6)}-${runDate.slice(6, 8)}` }
1581
+ : runDate.length === 10
1582
+ ? { dateIso: runDate }
1583
+ : {}),
1584
+ },
1585
+ }),
1586
+ );
1587
+ if (process.env.KK_SURFDASH_PATCH_OFFLINE === '1') {
1588
+ // Offline rehearsals still derive the payload (evidence), but never invoke the plugin.
1589
+ writeResult({ ok: false, target: 'surfdash-pub', error: 'skipped: offline (KK_SURFDASH_PATCH_OFFLINE=1)' });
1590
+ console.error(
1591
+ 'patch-surfdash: offline — payload derived, plugin patch skipped (KK_SURFDASH_PATCH_OFFLINE=1)',
1592
+ );
1593
+ return;
1594
+ }
1595
+ const status = kk(['executor', 'run', 'surfdash-pub', '--in', payload, '--out', resultPath], {
1596
+ unset: NESTED_SPUR_UNSET,
1597
+ soft: true,
1598
+ });
1599
+ const result = readJson(resultPath);
1600
+ if (result?.ok === true) {
1601
+ const meta = (result.metadata ?? {}) as Record<string, unknown>;
1602
+ console.error(
1603
+ `patch-surfdash: ${String(result.message ?? 'patched')} — cms-resync ${String(meta.cmsResync ?? 'ok')}`,
1604
+ );
1605
+ return;
1606
+ }
1607
+ const meta = (result?.metadata ?? {}) as Record<string, unknown>;
1608
+ if (meta.cmsResync === 'failed') {
1609
+ // The posts are patched and committed; only the CMS re-sync (a soft channel) failed.
1610
+ console.error(
1611
+ `patch-surfdash: WARNING: cms re-sync failed — ${String(result?.error ?? `exit ${status}`)}; fix: kk executor run surfdash-pub --in ${payload} --out ${resultPath}`,
1612
+ );
1613
+ return;
1614
+ }
1615
+ failWith(1, `patch-surfdash: ${String(result?.error ?? `surfdash-pub patch exited ${status}`)}`);
1616
+ });
1617
+ }
1618
+
1445
1619
  function dailyPatchSurfdash(args: string[]): void {
1446
1620
  const [workDir = '', runDate = '', surfdashRoot = '', pluginsPath = ''] = args;
1447
1621
  if (!surfdashRoot || !existsSync(surfdashRoot)) {
@@ -1718,6 +1892,11 @@ if (import.meta.main)
1718
1892
  dailyPreflight(rest);
1719
1893
  break;
1720
1894
  }
1895
+ case 'daily-safety-manifest': {
1896
+ const { dailySafetyManifest } = await loadDailyStages();
1897
+ dailySafetyManifest(rest);
1898
+ break;
1899
+ }
1721
1900
  case 'daily-safety-prep': {
1722
1901
  const { dailySafetyPrep } = await loadDailyStages();
1723
1902
  dailySafetyPrep(rest);
@@ -1768,6 +1947,11 @@ if (import.meta.main)
1768
1947
  dailyQualityReport(rest);
1769
1948
  break;
1770
1949
  }
1950
+ case 'daily-quality-verdict': {
1951
+ const { dailyQualityVerdict } = await loadDailyStages();
1952
+ dailyQualityVerdict(rest);
1953
+ break;
1954
+ }
1771
1955
  case 'daily-publish-prep': {
1772
1956
  const { dailyPublishPrep } = await loadDailyStages();
1773
1957
  dailyPublishPrep(rest);
@@ -1820,6 +2004,12 @@ if (import.meta.main)
1820
2004
  case 'daily-publish-partial':
1821
2005
  dailyPublishPartial(rest);
1822
2006
  break;
2007
+ case 'publish-matrix-partial':
2008
+ publishMatrixPartial(rest);
2009
+ break;
2010
+ case 'publish-matrix-patch-surfdash':
2011
+ publishMatrixPatchSurfdash(rest);
2012
+ break;
1823
2013
  case 'daily-run-report':
1824
2014
  dailyRunReport(rest);
1825
2015
  break;
@@ -41,8 +41,10 @@ function readJson(path: string): JsonObject | null {
41
41
  }
42
42
  }
43
43
 
44
- function safetyPath(workDir: string, runDate: string, name: string): string {
45
- return join(workDir, '24-script', `${runDate}_09_${name}`);
44
+ /** 0331: scope-aware artifact root — `24-script` (or empty) keeps the drafts paths byte-identical. */
45
+ function safetyPath(workDir: string, scopeDir: string, runDate: string, name: string): string {
46
+ const dir = scopeDir.trim() === '' || scopeDir.trim() === '24-script' ? '24-script' : scopeDir.trim();
47
+ return join(workDir, dir, `${runDate}_09_${name}`);
46
48
  }
47
49
 
48
50
  function nonEmptyString(value: unknown): value is string {
@@ -59,9 +61,9 @@ function sortedUnique(values: string[]): string[] | null {
59
61
  return unique.length === values.length ? unique.sort() : null;
60
62
  }
61
63
 
62
- function reportValid(workDir: string, runDate: string): string | null {
63
- const request = readJson(safetyPath(workDir, runDate, 'safety-review_request.json'));
64
- const report = readJson(safetyPath(workDir, runDate, 'safety-review_report.json'));
64
+ function reportValid(workDir: string, scopeDir: string, runDate: string): string | null {
65
+ const request = readJson(safetyPath(workDir, scopeDir, runDate, 'safety-review_request.json'));
66
+ const report = readJson(safetyPath(workDir, scopeDir, runDate, 'safety-review_report.json'));
65
67
  if (request === null || report === null) return 'review request or report is missing/invalid JSON';
66
68
 
67
69
  const requested = stringList(request.platforms);
@@ -100,8 +102,8 @@ function reportValid(workDir: string, runDate: string): string | null {
100
102
  return null;
101
103
  }
102
104
 
103
- function judgeValid(workDir: string, runDate: string): string | null {
104
- const verdict = readJson(safetyPath(workDir, runDate, 'safety-judge_verdict.json'));
105
+ function judgeValid(workDir: string, scopeDir: string, runDate: string): string | null {
106
+ const verdict = readJson(safetyPath(workDir, scopeDir, runDate, 'safety-judge_verdict.json'));
105
107
  if (verdict === null) return 'judge verdict is missing/invalid JSON';
106
108
  if (!VERDICTS.has(String(verdict.verdict))) return 'judge verdict value is invalid';
107
109
  if (!CONFIDENCE.has(String(verdict.confidence))) return 'judge confidence is missing/invalid';
@@ -149,10 +151,10 @@ function judgeValid(workDir: string, runDate: string): string | null {
149
151
  * Only the gate's persisted status represents a real content-safety escalation, so unattended mode
150
152
  * rejects that status while attended mode leaves it for the caller's pause node.
151
153
  */
152
- function publishValid(workDir: string, runDate: string, unattendedArg: string): string | null {
154
+ function publishValid(workDir: string, scopeDir: string, runDate: string, unattendedArg: string): string | null {
153
155
  if (unattendedArg !== 'true' && unattendedArg !== 'false') return 'unattended must be true or false';
154
- const verdictToken = safetyPath(workDir, runDate, 'safety-verdict.status');
155
- const gateStatusPath = safetyPath(workDir, runDate, 'safety-judge_status.txt');
156
+ const verdictToken = safetyPath(workDir, scopeDir, runDate, 'safety-verdict.status');
157
+ const gateStatusPath = safetyPath(workDir, scopeDir, runDate, 'safety-judge_status.txt');
156
158
  let token = '';
157
159
  let gateStatus = '';
158
160
  try {
@@ -171,27 +173,62 @@ function publishValid(workDir: string, runDate: string, unattendedArg: string):
171
173
  return null;
172
174
  }
173
175
 
174
- function draftDigest(workDir: string, runDate: string): string | null {
175
- const article = join(workDir, '22-article', `${runDate}_06_article_content.json`);
176
- const script = join(workDir, '24-script', `${runDate}_08_script_voicescript.yaml`);
176
+ function draftDigest(workDir: string, scopeDir: string, runDate: string): string | null {
177
+ const hash = createHash('sha256');
178
+ if (scopeDir.trim() === '' || scopeDir.trim() === '24-script') {
179
+ const article = join(workDir, '22-article', `${runDate}_06_article_content.json`);
180
+ const script = join(workDir, '24-script', `${runDate}_08_script_voicescript.yaml`);
181
+ try {
182
+ hash.update(readFileSync(article)).update('\0');
183
+ hash.update(readFileSync(script)).update('\0');
184
+ return hash.digest('hex');
185
+ } catch {
186
+ return null;
187
+ }
188
+ }
189
+ // Variants scope: digest over the manifest's sorted keys + current bytes (mirrors the
190
+ // daily-stages scopeDigest algorithm, so request/guard/verdict all bind the same bytes).
177
191
  try {
178
- const hash = createHash('sha256');
179
- hash.update(readFileSync(article)).update('\0');
180
- hash.update(readFileSync(script)).update('\0');
192
+ const manifest = readJson(join(workDir, '4-safety-variants', 'manifest.json')) as {
193
+ variants?: Record<string, { path?: unknown }>;
194
+ } | null;
195
+ if (manifest === null || !isObject(manifest.variants)) return null;
196
+ for (const key of Object.keys(manifest.variants).sort()) {
197
+ const path = join(workDir, String(manifest.variants[key].path));
198
+ hash.update(key).update('\0').update(readFileSync(path)).update('\0');
199
+ }
181
200
  return hash.digest('hex');
182
201
  } catch {
183
202
  return null;
184
203
  }
185
204
  }
186
205
 
187
- function revisionValid(workDir: string, runDate: string): string | null {
188
- const request = readJson(safetyPath(workDir, runDate, 'safety-review_request.json'));
206
+ function revisionValid(workDir: string, scopeDir: string, runDate: string): string | null {
207
+ const request = readJson(safetyPath(workDir, scopeDir, runDate, 'safety-review_request.json'));
208
+ if (request === null || !nonEmptyString(request.digest)) return 'review request digest is missing';
209
+ const isVariants = scopeDir.trim() !== '' && scopeDir.trim() !== '24-script';
210
+ const digest = draftDigest(workDir, scopeDir, runDate);
211
+ if (digest === null) {
212
+ return isVariants
213
+ ? 'a manifest-listed variant is missing/unreadable'
214
+ : 'revised article or VoiceScript is missing/unreadable';
215
+ }
216
+ if (digest === request.digest) return isVariants ? 'variant digest is unchanged' : 'draft digest is unchanged';
217
+ if (isVariants) {
218
+ const manifest = readJson(join(workDir, '4-safety-variants', 'manifest.json')) as {
219
+ variants?: Record<string, { path?: unknown }>;
220
+ } | null;
221
+ if (manifest === null || !isObject(manifest.variants)) return 'variants manifest is missing/malformed';
222
+ for (const entry of Object.values(manifest.variants)) {
223
+ const value = readJson(join(workDir, String(entry.path)));
224
+ if (value === null || !nonEmptyString(value.title) || !nonEmptyString(value.body)) {
225
+ return 'revised variant title/body is missing';
226
+ }
227
+ }
228
+ return null;
229
+ }
189
230
  const articlePath = join(workDir, '22-article', `${runDate}_06_article_content.json`);
190
231
  const scriptPath = join(workDir, '24-script', `${runDate}_08_script_voicescript.yaml`);
191
- if (request === null || !nonEmptyString(request.digest)) return 'review request digest is missing';
192
- if (draftDigest(workDir, runDate) === null) return 'revised article or VoiceScript is missing/unreadable';
193
- if (draftDigest(workDir, runDate) === request.digest) return 'draft digest is unchanged';
194
-
195
232
  const article = readJson(articlePath);
196
233
  if (article === null || !nonEmptyString(article.title) || !nonEmptyString(article.body)) {
197
234
  return 'revised article title/body is missing';
@@ -207,22 +244,27 @@ function revisionValid(workDir: string, runDate: string): string | null {
207
244
  }
208
245
 
209
246
  function main(): number {
210
- const [kind = '', workDir = '', runDate = '', unattended = 'false'] = process.argv.slice(2);
247
+ // 0331: an optional trailing scope dir re-roots every artifact path (variants review reads
248
+ // 4-safety-variants); publish-valid keeps `unattended` in the 4th slot it has always had.
249
+ const [kind = '', workDir = '', runDate = '', fourth = '', fifth = ''] = process.argv.slice(2);
250
+ const isPublish = kind === 'publish-valid';
251
+ const unattended = isPublish ? fourth : 'false';
252
+ const scopeDir = isPublish ? fifth : fourth;
211
253
  if (!kind || !workDir || !runDate) {
212
254
  console.error(
213
- 'usage: safety-artifact-check.ts <report-valid|judge-valid|revision-valid|publish-valid> <work_dir> <run_date> [unattended]',
255
+ 'usage: safety-artifact-check.ts <report-valid|judge-valid|revision-valid|publish-valid> <work_dir> <run_date> [unattended] [scope_dir]',
214
256
  );
215
257
  return 2;
216
258
  }
217
259
  const reason =
218
260
  kind === 'report-valid'
219
- ? reportValid(workDir, runDate)
261
+ ? reportValid(workDir, scopeDir, runDate)
220
262
  : kind === 'judge-valid'
221
- ? judgeValid(workDir, runDate)
263
+ ? judgeValid(workDir, scopeDir, runDate)
222
264
  : kind === 'revision-valid'
223
- ? revisionValid(workDir, runDate)
265
+ ? revisionValid(workDir, scopeDir, runDate)
224
266
  : kind === 'publish-valid'
225
- ? publishValid(workDir, runDate, unattended)
267
+ ? publishValid(workDir, scopeDir, runDate, unattended)
226
268
  : 'unknown predicate';
227
269
  if (reason !== null) {
228
270
  console.error(`safety-artifact-check: ${kind}: ${reason}`);
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * voice3-publish-window — cron-safe launcher for the voice3 shadow publish window (task 0334).
4
+ *
5
+ * WHY a launcher (audit F08/F09/F10, docs/design/kk-daily-ai-voice3.md):
6
+ * - cron ends a command at the first UNESCAPED % and feeds the rest to stdin, so any inline
7
+ * `$(date +%Y%m%d)` shell in a crontab entry silently truncates the command. Date math and
8
+ * flag assembly live here, in plain TypeScript, behind zero percent signs.
9
+ * - the publish window must read the GENERATION bundle (`<works>/kk-daily-ai-voice2/<date>/
10
+ * bundle.json` — voice3 prepares under the voice2 namespace until the operator's cutover
11
+ * changes it), never the publish3 output directory that earlier entries pointed at.
12
+ * - the CN consent token list is validated against the pinned preset's WINDOW rows before any
13
+ * spur process starts; the old baked `wechatmp-pub,xhs-pub,…` list (tokens that have no
14
+ * cn-peak row) is refused up front.
15
+ *
16
+ * Contract: everything before `spur workflow run plugins/kk/workflows/kk-daily-publish3.yaml`
17
+ * is preparation and refusal; the only side effects are refusal messages and, on success, the
18
+ * single spur exec. `KK_SPUR_BIN` pins the spur binary (same convention as board/capture.ts and
19
+ * decision-hub.ts); `--dry-run` prints the forwarded vars and stops (rehearsal aid).
20
+ */
21
+ import { spawnSync } from 'node:child_process';
22
+ import { existsSync, readFileSync } from 'node:fs';
23
+ import { homedir } from 'node:os';
24
+ import { dirname, join, resolve } from 'node:path';
25
+ import { fileURLToPath } from 'node:url';
26
+
27
+ const USAGE =
28
+ 'usage: voice3-publish-window.ts --window <after-generate|cn-peak|overseas-rss> [--run-date <YYYY-MM-DD|YYYYMMDD>] ' +
29
+ '[--preset <slug>] [--live-channels a,b] [--work-dir <dir>] [--dummy] [--dry-run]';
30
+
31
+ const WINDOWS = new Set(['after-generate', 'cn-peak', 'overseas-rss']);
32
+
33
+ function failWith(code: number, message: string): never {
34
+ console.error(message);
35
+ process.exit(code);
36
+ }
37
+
38
+ function expandHome(raw: string): string {
39
+ return raw === '~' || raw.startsWith('~/') ? join(homedir(), raw.slice(1)) : raw;
40
+ }
41
+
42
+ /** KK_WORKS_DIR env > config works_dir > <cwd>/.works — the kk-workflow-stages precedence. */
43
+ function resolveWorksDir(): string {
44
+ const fromEnv = process.env.KK_WORKS_DIR;
45
+ if (fromEnv) return expandHome(fromEnv);
46
+ const configPath = process.env.KK_CONFIG || join(homedir(), '.config/kk/config.yaml');
47
+ if (existsSync(configPath)) {
48
+ try {
49
+ const parsed = Bun.YAML.parse(readFileSync(configPath, 'utf-8')) as Record<string, unknown> | null;
50
+ const worksDir = parsed !== null && typeof parsed === 'object' ? parsed.works_dir : undefined;
51
+ if (typeof worksDir === 'string' && worksDir !== '') return expandHome(worksDir);
52
+ } catch {
53
+ // fall through to the compiled default
54
+ }
55
+ }
56
+ return resolve(process.cwd(), '.works');
57
+ }
58
+
59
+ /** A real calendar day as `YYYY-MM-DD`, or undefined (mirrors publish-matrix normalizeRunDate). */
60
+ function normalizeRunDate(raw: string): string | undefined {
61
+ const match = /^(\d{4})-?(\d{2})-?(\d{2})$/.exec(raw.trim());
62
+ if (match === null) return undefined;
63
+ const iso = `${match[1]}-${match[2]}-${match[3]}`;
64
+ const parsed = new Date(`${iso}T00:00:00Z`);
65
+ if (
66
+ Number.isNaN(parsed.getTime()) ||
67
+ parsed.getUTCFullYear() !== Number(match[1]) ||
68
+ parsed.getUTCMonth() + 1 !== Number(match[2]) ||
69
+ parsed.getUTCDate() !== Number(match[3])
70
+ ) {
71
+ return undefined;
72
+ }
73
+ return iso;
74
+ }
75
+
76
+ /** Today (offset 0) or yesterday (offset -1) in America/Los_Angeles — the episodes' own day. */
77
+ function losAngelesDate(offsetDays: number): string {
78
+ const instant = new Date(Date.now() + offsetDays * 24 * 60 * 60 * 1000);
79
+ // en-CA renders YYYY-MM-DD; Intl does the zone math cron cannot do inline.
80
+ return new Intl.DateTimeFormat('en-CA', { timeZone: 'America/Los_Angeles' }).format(instant);
81
+ }
82
+
83
+ /** `--flag value`, or undefined. */
84
+ function flagValue(args: string[], flag: string): string | undefined {
85
+ const index = args.indexOf(flag);
86
+ return index >= 0 ? args[index + 1] : undefined;
87
+ }
88
+
89
+ function main(): void {
90
+ const args = process.argv.slice(2);
91
+ if (args.includes('--help') || args.includes('-h')) {
92
+ console.log(USAGE);
93
+ process.exit(0);
94
+ }
95
+ const window = flagValue(args, '--window');
96
+ if (window === undefined || !WINDOWS.has(window))
97
+ failWith(1, `${USAGE}\nvoice3-publish-window: --window must be one of ${[...WINDOWS].join('|')}`);
98
+ const dummy = args.includes('--dummy');
99
+ const dryRun = args.includes('--dry-run');
100
+ const preset = flagValue(args, '--preset');
101
+ const liveChannelsRaw = flagValue(args, '--live-channels');
102
+ const workDirOverride = flagValue(args, '--work-dir');
103
+
104
+ // R1: run date = explicit flag, else window-derived LA day (overseas reads the episode that
105
+ // landed after the 07:00 tick, i.e. yesterday).
106
+ const runDateRaw = flagValue(args, '--run-date');
107
+ let runDate: string | undefined;
108
+ if (runDateRaw !== undefined && runDateRaw.trim() !== '') {
109
+ runDate = normalizeRunDate(runDateRaw);
110
+ if (runDate === undefined)
111
+ failWith(1, `voice3-publish-window: '${runDateRaw}' is not a valid calendar day (YYYY-MM-DD or YYYYMMDD)`);
112
+ } else {
113
+ runDate = losAngelesDate(window === 'overseas-rss' ? -1 : 0);
114
+ }
115
+ const digits = runDate.replaceAll('-', '');
116
+
117
+ if (dummy && liveChannelsRaw !== undefined) {
118
+ failWith(1, 'voice3-publish-window: --dummy and --live-channels are mutually exclusive');
119
+ }
120
+
121
+ // R3/F10: live tokens validate against the pinned preset's WINDOW rows before any spawn.
122
+ const liveChannels =
123
+ liveChannelsRaw !== undefined && liveChannelsRaw.trim() !== ''
124
+ ? liveChannelsRaw
125
+ .split(',')
126
+ .map((token) => token.trim())
127
+ .filter((token) => token !== '')
128
+ : [];
129
+ if (liveChannels.length > 0 && preset === undefined) {
130
+ failWith(1, 'voice3-publish-window: --live-channels requires an explicit --preset to validate the window');
131
+ }
132
+ if (liveChannels.length > 0) {
133
+ const scriptDir = dirname(fileURLToPath(import.meta.url));
134
+ const presetPath = join(dirname(scriptDir), 'publish-presets', `${preset}.json`);
135
+ if (!existsSync(presetPath))
136
+ failWith(1, `voice3-publish-window: preset '${preset}' not found at ${presetPath}`);
137
+ const parsed = JSON.parse(readFileSync(presetPath, 'utf-8')) as {
138
+ targets?: Array<{ plugin: string; id?: string; mode?: string; window?: string }>;
139
+ };
140
+ const targets = parsed.targets ?? [];
141
+ const inWindow = targets.filter((target) => (target.window ?? 'after-generate') === window);
142
+ const refused = liveChannels.filter((token) => {
143
+ const row = inWindow.find((target) => target.plugin === token || target.id === token);
144
+ return row === undefined || (row.mode ?? 'draft') === 'off';
145
+ });
146
+ if (refused.length > 0) {
147
+ failWith(
148
+ 1,
149
+ `voice3-publish-window: live token(s) ${refused.join(', ')} have no active row in window '${window}' of preset '${preset}'` +
150
+ ` (send-confirm resumes are consent-scoped per window; see docs/design/kk-daily-ai-voice3.md)`,
151
+ );
152
+ }
153
+ }
154
+
155
+ // R1/F08: the bundle is the GENERATION artifact under the voice2 namespace, never a publish3
156
+ // output dir. Mismatched/missing bundles refuse before spur starts.
157
+ const genDir =
158
+ workDirOverride !== undefined && workDirOverride.trim() !== ''
159
+ ? resolve(workDirOverride)
160
+ : join(resolveWorksDir(), 'kk-daily-ai-voice2', digits);
161
+ const bundlePath = join(genDir, 'bundle.json');
162
+ if (!existsSync(bundlePath)) failWith(1, `voice3-publish-window: no generation bundle at ${bundlePath}`);
163
+ let bundleRunDate: string | undefined;
164
+ try {
165
+ const bundle = JSON.parse(readFileSync(bundlePath, 'utf-8')) as { runDate?: string };
166
+ bundleRunDate = typeof bundle.runDate === 'string' ? normalizeRunDate(bundle.runDate) : undefined;
167
+ } catch {
168
+ failWith(1, `voice3-publish-window: unreadable generation bundle at ${bundlePath}`);
169
+ }
170
+ if (bundleRunDate !== runDate) {
171
+ failWith(
172
+ 1,
173
+ `voice3-publish-window: bundle at ${bundlePath} is dated ${bundleRunDate ?? 'unknown'}, not the requested ${runDate}`,
174
+ );
175
+ }
176
+
177
+ const vars: Record<string, string> = {
178
+ work_dir: genDir,
179
+ run_date: digits,
180
+ window,
181
+ };
182
+ if (preset !== undefined && preset.trim() !== '') vars.preset = preset;
183
+ if (liveChannels.length > 0) vars.live_channels = liveChannels.join(',');
184
+ if (dummy) vars.dummy = 'true';
185
+
186
+ if (dryRun) {
187
+ console.log(JSON.stringify(vars, null, 2));
188
+ process.exit(0);
189
+ }
190
+
191
+ const proc = spawnSync(
192
+ process.env.KK_SPUR_BIN || 'spur',
193
+ ['workflow', 'run', 'plugins/kk/workflows/kk-daily-publish3.yaml', '--vars', JSON.stringify(vars)],
194
+ {
195
+ stdio: 'inherit',
196
+ },
197
+ );
198
+ // node spawnSync surfaces the child's code as `status` (exitCode is the async-API field).
199
+ process.exit(proc.status ?? 1);
200
+ }
201
+
202
+ main();
@@ -3,6 +3,7 @@ name: article-adapt
3
3
  # Description rules: front-load the leading identity phrase; one trigger per genuine
4
4
  # branch (collapse synonym triggers into one); never restate the body's identity line.
5
5
  description: This skill should be used when a kk-itc publish plan contains variants with adapt=true and per-variant Content files must be written — "adapt the article", "translate for the target profile", "write content.zh-article.json", or when a spur workflow agent.run step invokes kk:article-adapt. Translates title and body to the variant locale, applies the plan limits, and preserves code blocks, URLs and references; it holds no platform table.
6
+ requires: [cli:kk, cli:spur, local-fs]
6
7
  ---
7
8
 
8
9
  # article-adapt
@@ -6,6 +6,7 @@ description: >-
6
6
  "outline options for audio", "author a VoiceScript", "revise the script".
7
7
  Runs briefing → outline → script with explicit operator gates; solo mode
8
8
  ships first, dialogue/interview/narration are named extension points.
9
+ requires: [local-fs]
9
10
  ---
10
11
 
11
12
  # audio-authoring — spoken-audio authoring (create | briefing | outline | script)
@@ -2,6 +2,7 @@
2
2
  name: content-judge
3
3
  description: This skill should be used when an LLM evaluation gate is needed in a knowledge-kit pipeline — "judge this content", "evaluate against rubric", "quality gate before publish", "check technical accuracy", "review brand tone", "compliance check", or when a spur workflow step of type `agent.run` targets the content-judge skill to emit a PASS/FAIL/NEEDS_REVISION verdict. Centralizes evaluation rubrics, prompt templates, and structured verdict emission for generated Content.
4
4
  version: 0.1.0
5
+ requires: [cli:kk, cli:spur, local-fs]
5
6
  ---
6
7
 
7
8
  # content-judge — LLM-as-judge evaluation skill
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: content-safety
3
3
  description: This skill should be used when a knowledge-kit pipeline or operator needs a content-safety / platform-admission review before publishing — "check this before publishing", "小宇宙/公众号/网易云/苹果播客 审核风险", "desensitize personal info", "verify disclosure and sourcing before publish". Detects admission risk per distribution platform, proposes honest revisions or exclusion, and escalates what it cannot verify. Quality scoring is content-judge's job.
4
+ requires: [local-fs]
4
5
  ---
5
6
 
6
7
  # content-safety — pre-publication content-safety review
@@ -7,6 +7,7 @@ description: >-
7
7
  "how does X work", "ELI5", "like I'm 5", or "show me". Normal explanations are
8
8
  concise inline views; the ELI5 register is opt-in. This is not for content authoring,
9
9
  research, judging, publishing, or package/code review workflows.
10
+ requires: [local-fs]
10
11
  ---
11
12
 
12
13
  # explain-things
@@ -7,6 +7,7 @@ description: >-
7
7
  "list the image formats and styles". Three operations — generate,
8
8
  extract-style, list-presets — over named format and style presets, with a
9
9
  confirmation gate before any paid generation.
10
+ requires: [cli:kk, cli:spur, local-fs]
10
11
  ---
11
12
 
12
13
  # image-authoring — image generation agent surface (generate | extract-style | list-presets)
@@ -7,6 +7,7 @@ description: >-
7
7
  writing method", "generate an IT technical draft on X", "create a technical
8
8
  article for WeChat/English platforms". Orchestrates topic (outline + draft),
9
9
  optional storm-research, and optional content-judge without enlarging those skills.
10
+ requires: [cli:kk, local-fs]
10
11
  ---
11
12
 
12
13
  # itc-generating — IT technical draft generation (method + orchestration)
@@ -6,6 +6,7 @@ description: >-
6
6
  `kk executor run` / `kk fanout` for file-cli and script channels; sequences
7
7
  agent-default CMS channels as workflow `agent.run` steps. Does not own
8
8
  transport.
9
+ requires: [cli:kk, local-fs]
9
10
  ---
10
11
 
11
12
  # publish — leftover channel operator surface