@gobing-ai/knowledge-kit 0.0.35 → 0.0.37

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 (124) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +1 -1
  3. package/dist/board/collect/index.css +42 -0
  4. package/dist/board/collect/index.js +787 -0
  5. package/dist/board/daily-voice/index.css +13 -0
  6. package/dist/board/daily-voice/index.js +902 -0
  7. package/dist/board/itc/index.css +14 -0
  8. package/dist/board/itc/index.js +1026 -0
  9. package/dist/index.js +12996 -10476
  10. package/package.json +10 -9
  11. package/plugins/generations/content-gen/dist/index.js +1015 -10814
  12. package/plugins/generations/content-gen/package.json +1 -1
  13. package/plugins/generations/core-facts-gen/dist/index.js +1002 -10801
  14. package/plugins/generations/core-facts-gen/package.json +1 -1
  15. package/plugins/generations/daily-article-gen/dist/index.js +1004 -10803
  16. package/plugins/generations/daily-article-gen/package.json +1 -1
  17. package/plugins/generations/dailynews-gen/dist/index.js +1111 -10888
  18. package/plugins/generations/dailynews-gen/package.json +1 -1
  19. package/plugins/generations/dailynews-gen/src/script-plan.ts +32 -0
  20. package/plugins/generations/episode-plan-gen/dist/index.js +1124 -10923
  21. package/plugins/generations/episode-plan-gen/package.json +1 -1
  22. package/plugins/generations/gemini-tts/dist/index.js +1104 -10903
  23. package/plugins/generations/gemini-tts/package.json +1 -1
  24. package/plugins/generations/image-gen/dist/index.js +1239 -11035
  25. package/plugins/generations/image-gen/package.json +1 -1
  26. package/plugins/generations/news-report-gen/dist/index.js +1048 -10847
  27. package/plugins/generations/news-report-gen/package.json +1 -1
  28. package/plugins/generations/voice-gen/dist/index.js +1062 -10861
  29. package/plugins/generations/voice-gen/package.json +1 -1
  30. package/plugins/generations/voxcpm-tts/README.md +72 -0
  31. package/plugins/generations/voxcpm-tts/dist/index.js +14006 -0
  32. package/plugins/generations/voxcpm-tts/package.json +16 -0
  33. package/plugins/generations/voxcpm-tts/plugin.json +6 -0
  34. package/plugins/generations/voxcpm-tts/profiles.json +10 -0
  35. package/plugins/generations/voxcpm-tts/pyproject.toml +9 -0
  36. package/plugins/generations/voxcpm-tts/python/worker.py +353 -0
  37. package/plugins/generations/voxcpm-tts/src/index.ts +42 -0
  38. package/plugins/generations/voxcpm-tts/src/voxcpm-client.ts +765 -0
  39. package/plugins/generations/voxcpm-tts/tsconfig.json +8 -0
  40. package/plugins/generations/voxcpm-tts/uv.lock +2571 -0
  41. package/plugins/ingestions/aihot-ingest/dist/index.js +1081 -10628
  42. package/plugins/ingestions/aihot-ingest/package.json +1 -1
  43. package/plugins/ingestions/horizon-ingest/dist/index.js +1013 -10812
  44. package/plugins/ingestions/horizon-ingest/package.json +1 -1
  45. package/plugins/ingestions/karakeep-local/dist/index.js +689 -10774
  46. package/plugins/ingestions/karakeep-local/package.json +1 -1
  47. package/plugins/ingestions/last30days-ingest/dist/index.js +1011 -10810
  48. package/plugins/ingestions/last30days-ingest/package.json +1 -1
  49. package/plugins/ingestions/web-search/dist/index.js +1250 -11033
  50. package/plugins/ingestions/web-search/package.json +1 -1
  51. package/plugins/kk/README.md +29 -2
  52. package/plugins/kk/plugin.json +1 -1
  53. package/plugins/kk/schedules/cron.tmpl +9 -1
  54. package/plugins/kk/schedules/kk-serve.launchd.plist.tmpl +79 -0
  55. package/plugins/kk/schedules/launchd.plist.tmpl +11 -1
  56. package/plugins/kk/scripts/daily-stages.ts +428 -46
  57. package/plugins/kk/scripts/daily-voice-style.zh.md +1 -1
  58. package/plugins/kk/scripts/kk-workflow-stages.ts +76 -146
  59. package/plugins/kk/skills/publish/SKILL.md +8 -4
  60. package/plugins/kk/workflows/feature-verification.yaml +93 -0
  61. package/plugins/kk/workflows/judge-gated-publish-example.yaml +1 -0
  62. package/plugins/kk/workflows/kk-collect.yaml +43 -11
  63. package/plugins/kk/workflows/kk-daily-ai-voice.yaml +4 -2
  64. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +87 -16
  65. package/plugins/kk/workflows/kk-daily-safety.yaml +15 -2
  66. package/plugins/kk/workflows/kk-itc.yaml +8 -1
  67. package/plugins/kk/workflows/kk-solo-podcast.yaml +2 -0
  68. package/plugins/publishings/emdash-pub/dist/index.js +987 -10814
  69. package/plugins/publishings/emdash-pub/package.json +1 -1
  70. package/plugins/publishings/infoq-pub/dist/index.js +1928 -11444
  71. package/plugins/publishings/infoq-pub/dist/profile-check.js +1677 -11199
  72. package/plugins/publishings/infoq-pub/package.json +1 -1
  73. package/plugins/publishings/infoq-pub/plugin.json +5 -1
  74. package/plugins/publishings/juejin-pub/dist/index.js +1940 -11456
  75. package/plugins/publishings/juejin-pub/dist/profile-check.js +1677 -11199
  76. package/plugins/publishings/juejin-pub/package.json +1 -1
  77. package/plugins/publishings/juejin-pub/plugin.json +5 -1
  78. package/plugins/publishings/medium-pub/dist/index.js +978 -10805
  79. package/plugins/publishings/medium-pub/package.json +1 -1
  80. package/plugins/publishings/podcast-pub/dist/index.js +1016 -10843
  81. package/plugins/publishings/podcast-pub/package.json +1 -1
  82. package/plugins/publishings/qiita-pub/dist/index.js +976 -10803
  83. package/plugins/publishings/qiita-pub/package.json +1 -1
  84. package/plugins/publishings/substack-pub/dist/index.js +1932 -11448
  85. package/plugins/publishings/substack-pub/dist/profile-check.js +1698 -11200
  86. package/plugins/publishings/substack-pub/package.json +1 -1
  87. package/plugins/publishings/substack-pub/plugin.json +4 -1
  88. package/plugins/publishings/substack-pub/src/profile-check.ts +36 -3
  89. package/plugins/publishings/surfdash-pub/dist/index.js +1151 -10834
  90. package/plugins/publishings/surfdash-pub/package.json +1 -1
  91. package/plugins/publishings/surfdash-pub/src/index.ts +137 -15
  92. package/plugins/publishings/surfdash-pub/src/patch.ts +144 -0
  93. package/plugins/publishings/threads-pub/dist/index.js +12394 -0
  94. package/plugins/publishings/threads-pub/package.json +17 -0
  95. package/plugins/publishings/threads-pub/plugin.json +7 -0
  96. package/plugins/publishings/threads-pub/src/index.ts +340 -0
  97. package/plugins/publishings/threads-pub/tsconfig.json +4 -0
  98. package/plugins/publishings/wechatmp-pub/dist/index.js +2508 -11695
  99. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +1656 -11188
  100. package/plugins/publishings/wechatmp-pub/package.json +1 -1
  101. package/plugins/publishings/wechatmp-pub/plugin.json +5 -1
  102. package/plugins/publishings/wechatmp-pub/src/enrich.ts +244 -71
  103. package/plugins/publishings/wechatmp-pub/src/flow.ts +412 -96
  104. package/plugins/publishings/wechatmp-pub/src/index.ts +24 -3
  105. package/plugins/publishings/wechatmp-pub/src/page.ts +16 -0
  106. package/plugins/publishings/wechatmp-pub/src/profile-check.ts +40 -4
  107. package/plugins/publishings/weibo-pub/dist/index.js +21345 -0
  108. package/plugins/publishings/weibo-pub/dist/profile-check.js +20817 -0
  109. package/plugins/publishings/weibo-pub/package.json +19 -0
  110. package/plugins/publishings/weibo-pub/plugin.json +12 -0
  111. package/plugins/publishings/weibo-pub/src/index.ts +868 -0
  112. package/plugins/publishings/weibo-pub/src/profile-check.ts +20 -0
  113. package/plugins/publishings/weibo-pub/tsconfig.json +4 -0
  114. package/plugins/publishings/x-pub/dist/index.js +1948 -11440
  115. package/plugins/publishings/x-pub/dist/profile-check.js +1677 -11199
  116. package/plugins/publishings/x-pub/package.json +1 -1
  117. package/plugins/publishings/x-pub/plugin.json +5 -1
  118. package/plugins/publishings/xhs-pub/dist/index.js +1993 -11509
  119. package/plugins/publishings/xhs-pub/dist/profile-check.js +1697 -11212
  120. package/plugins/publishings/xhs-pub/package.json +1 -1
  121. package/plugins/publishings/xhs-pub/plugin.json +5 -1
  122. package/plugins/publishings/xhs-pub/src/profile-check.ts +72 -2
  123. package/plugins/publishings/zenn-pub/dist/index.js +988 -10815
  124. package/plugins/publishings/zenn-pub/package.json +1 -1
@@ -55,7 +55,7 @@ import {
55
55
  writeFileSync,
56
56
  } from 'node:fs';
57
57
  import { homedir } from 'node:os';
58
- import { basename, dirname, join, relative, resolve } from 'node:path';
58
+ import { basename, dirname, join, resolve } from 'node:path';
59
59
 
60
60
  export const SCRIPTS_DIR = import.meta.dir;
61
61
  export const VALIDATE_SIDECAR = 'validate-voicescript.ts';
@@ -1280,10 +1280,24 @@ function publishCompleteness(
1280
1280
  detail: 'en/ja scaffolded, never synced',
1281
1281
  });
1282
1282
  } else if (syncResult.ok === true) {
1283
+ // A filled marker only proves the locale was SCAFFOLDED. The workflow's `translate` agent is
1284
+ // what publishes it (its prompt flips `draft: true` → `draft: false`), so a run whose
1285
+ // translation failed used to report `locales: done` while the site kept an unpublished
1286
+ // scaffold — the one channel whose loss nothing in the run's artifacts named. Read the draft
1287
+ // the marker points at: still `draft: true` means it was never translated.
1288
+ const untranslated = ['en', 'ja'].filter((locale) => {
1289
+ const marker = readFileSync(at(`20_translate-localize_${locale}-post-path.txt`), 'utf-8').trim();
1290
+ if (!isFile(marker)) return true;
1291
+ const head = readFileSync(marker, 'utf-8').split('---')[1] ?? '';
1292
+ return /^draft:\s*true\s*$/m.test(head);
1293
+ });
1283
1294
  rows.push({
1284
1295
  channel: 'locales',
1285
- state: 'done',
1286
- detail: 'en/ja drafts scaffolded',
1296
+ state: untranslated.length === 0 ? 'done' : 'failed',
1297
+ detail:
1298
+ untranslated.length === 0
1299
+ ? 'en/ja drafts scaffolded and translated'
1300
+ : `${untranslated.join(', ')} draft is still a scaffold (draft: true) — not translated, so it stays unpublished`,
1287
1301
  });
1288
1302
  } else {
1289
1303
  rows.push({
@@ -1327,6 +1341,12 @@ function publishCompleteness(
1327
1341
  function dailyPublishPartial(args: string[]): void {
1328
1342
  const [workDir = '', runDate = ''] = args;
1329
1343
  const result = join(workDir, '31-publish', `${runDate}_17_publish_result.json`);
1344
+ // The marker describes this run, so it belongs under that run's workDir — a cwd-relative path
1345
+ // litters whatever tree the caller stood in and hides the note from the episode it describes.
1346
+ const marker =
1347
+ workDir === ''
1348
+ ? join('.spur', 'run', `publish-partial-${runDate}.txt`)
1349
+ : join(workDir, '.spur', 'run', `publish-partial-${runDate}.txt`);
1330
1350
  timed(workDir, 'publish-partial', () => {
1331
1351
  const failed = targetsOf(result)
1332
1352
  .filter((entry) => entry.ok !== true || entry.status !== 'published')
@@ -1334,10 +1354,10 @@ function dailyPublishPartial(args: string[]): void {
1334
1354
  .join(',');
1335
1355
  const list = failed === '' ? '(unparsed)' : failed;
1336
1356
  console.error(`WARNING: partial publish — failed targets: ${list}`);
1337
- sted('mkdir', '.spur/run', () => mkdirSync('.spur/run', { recursive: true }));
1338
- sted('write', `.spur/run/publish-partial-${runDate}.txt`, () =>
1357
+ sted('mkdir', dirname(marker), () => mkdirSync(dirname(marker), { recursive: true }));
1358
+ sted('write', marker, () =>
1339
1359
  writeFileSync(
1340
- `.spur/run/publish-partial-${runDate}.txt`,
1360
+ marker,
1341
1361
  `${runDate} partial publish: failed targets: ${list} (result.json preserved for kk executor fan-out --retry-from)\n`,
1342
1362
  ),
1343
1363
  );
@@ -1345,10 +1365,16 @@ function dailyPublishPartial(args: string[]): void {
1345
1365
  }
1346
1366
 
1347
1367
  /**
1348
- * 20260917 review #4/#6: after both publishes land, patch the surfdash disk posts —
1349
- * reuse the podcast-hosted cover URL in frontmatter (image/og_image; no re-upload; fallback:
1350
- * copy the local cover into assets/) and append a podcast cross-link at the end of zh/en/ja.
1351
- * Idempotent per post; commits + pushes the surfdash checkout (SURFDASH_AUTO_PUSH=0 keeps commit-only).
1368
+ * Post-publish surfdash healing — a thin delegation to the publisher that owns it (2026-10-12).
1369
+ *
1370
+ * The work itself (podcast cover URL into `image`/`og_image`, the per-locale podcast cross-link, the
1371
+ * local cover's `assets/` fallback, the EmDash re-sync that makes CMS cards carry a servable image,
1372
+ * and the pathspec commit + push) lives in the `surfdash-pub` `patch` operation now: it is the
1373
+ * publisher's job, and every path that publishes a post needs it. This stage only reads the run's
1374
+ * publish results into the operation's payload and reports its outcome.
1375
+ *
1376
+ * `KK_SURFDASH_PATCH_OFFLINE=1` keeps fixture tests hermetic — the plugin would otherwise write to,
1377
+ * commit and push a real checkout.
1352
1378
  */
1353
1379
  function dailyPatchSurfdash(args: string[]): void {
1354
1380
  const [workDir = '', runDate = '', surfdashRoot = '', pluginsPath = ''] = args;
@@ -1369,149 +1395,53 @@ function dailyPatchSurfdash(args: string[]): void {
1369
1395
  const sd = targetOf(sdResult);
1370
1396
  const pod = targetOf(podcastResult);
1371
1397
  const postPath = (sd.metadata as Record<string, unknown> | undefined)?.postPath;
1372
- if (typeof postPath !== 'string' || postPath === '')
1398
+ if (typeof postPath !== 'string' || postPath === '') {
1373
1399
  failWith(1, 'patch-surfdash: surfdash result carries no postPath');
1374
- // Refuse before anything is written: surfdash's deploy.yml publishes from main, so a commit
1375
- // on any other branch is invisible to production. Placed here (after the input checks) so a
1376
- // refusal leaves the working tree untouched.
1377
- const head = spawnSync('git', ['-C', surfdashRoot, 'rev-parse', '--abbrev-ref', 'HEAD'])
1378
- .stdout?.toString()
1379
- .trim();
1380
- if (head !== 'main')
1381
- failWith(1, `patch-surfdash: refusing to commit on '${head}' — surfdash deploys from main`);
1382
- // Paths this run actually wrote, relative to surfdashRoot. `git add`/`commit` take these as a
1383
- // pathspec so the operator's unrelated staged work is never swept into the commit.
1384
- const written: string[] = [];
1385
- const slugDir = dirname(postPath);
1386
- const showNotesUrl = typeof pod.url === 'string' ? pod.url : '';
1387
- const coverUrl = ((pod.metadata as Record<string, unknown> | undefined)?.imageUrl as string | undefined) ?? '';
1388
- const localCover = join(workDir, '23-cover', `${runDate}_07_cover_cover.png`);
1389
- const dateIso = `${runDate.slice(0, 4)}-${runDate.slice(4, 6)}-${runDate.slice(6, 8)}`;
1390
- const crossLink: Record<string, string> = {
1391
- // Task 0189 R4: cross-links carry the column's episode-title pattern, not the retired
1392
- // news-bulletin name (the VoiceScript episode title is episodeTitle(lead, date)).
1393
- zh: `🎧 本期已同步制作为播客节目,[点击收听《每日AI乱炖 · ${dateIso}》](${showNotesUrl})。`,
1394
- en: `🎧 This episode is also available as a podcast: [listen to 每日AI乱炖 · ${dateIso}](${showNotesUrl}).`,
1395
- ja: `🎧 本エピソードはポッドキャストでも配信中:[每日AI乱炖 · ${dateIso} を聴く](${showNotesUrl})。`,
1396
- };
1397
- let changed = 0;
1398
- for (const [locale, dir] of [
1399
- // zh posts live under articles/zh/<slug>; en/ja under articles/<locale>/<slug> —
1400
- // two dirnames up, not one (20260919: `articles/zh/en/...` never existed, so en/ja
1401
- // were silently skipped by the not-published branch).
1402
- ['zh', slugDir],
1403
- ['en', join(dirname(dirname(slugDir)), 'en', basename(slugDir))],
1404
- ['ja', join(dirname(dirname(slugDir)), 'ja', basename(slugDir))],
1405
- ] as const) {
1406
- const indexMd = join(dir, 'index.md');
1407
- if (!existsSync(indexMd)) {
1408
- console.error(`patch-surfdash: ${locale} post not published (${indexMd}) — skip`);
1409
- continue;
1410
- }
1411
- let md = readFileSync(indexMd, 'utf-8');
1412
- const fmEnd = md.indexOf('---', 3); // frontmatter is `---\n ... \n---\n`
1413
- if (fmEnd < 0) failWith(1, `patch-surfdash: no frontmatter in ${indexMd}`);
1414
- // Upsert a single-line `key: 'value'`; the regex consumes folded-scalar continuation
1415
- // lines (`og_image: >-\n https://...`) so multi-line publish output is replaced whole.
1416
- const upsertKey = (src: string, key: string, value: string): string => {
1417
- // YAML single-quoted scalars escape `'` as `''`; the replacement is a function so
1418
- // `$&`/`$1`/`$$` in the value are never expanded. Scope the match to the frontmatter
1419
- // (recomputed per call, since each upsert shifts offsets) so a body line starting with
1420
- // `image:` is not rewritten.
1421
- const end = src.indexOf('\n---', 3);
1422
- const fm = src.slice(0, end);
1423
- const body = src.slice(end);
1424
- const line = `${key}: '${value.replaceAll("'", "''")}'`;
1425
- const re = new RegExp(`^${key}:.*(?:\\n[ \\t]+.*)*`, 'm');
1426
- const next = re.test(fm)
1427
- ? fm.replace(re, () => line)
1428
- : fm.replace(/^publishDate: .*$/m, (m0) => `${m0}\n${line}`);
1429
- return next + body;
1430
- };
1431
- if (coverUrl !== '') {
1432
- // 20260919 prod feedback: the podcast-hosted cover URL wins even when surfdash-pub
1433
- // already wrote `image:` — its resolved og_image points at /content-posts/<slug>/
1434
- // assets/cover.png, which the deployed worker does not serve (404).
1435
- md = upsertKey(md, 'image', coverUrl);
1436
- md = upsertKey(md, 'og_image', coverUrl);
1437
- } else if (!/^image:/m.test(md.slice(0, fmEnd))) {
1438
- // Fallback for runs without a podcast cover: copy the local cover instead.
1439
- if (existsSync(localCover)) {
1440
- const assets = join(dir, 'assets');
1441
- mkdirSync(assets, { recursive: true });
1442
- copyFileSync(localCover, join(assets, 'cover.png'));
1443
- written.push(relative(surfdashRoot, join(assets, 'cover.png')));
1444
- md = upsertKey(md, 'image', './assets/cover.png');
1445
- md = upsertKey(md, 'og_image', './assets/cover.png');
1446
- }
1447
- }
1448
- if (showNotesUrl !== '' && !md.includes(showNotesUrl)) {
1449
- md = `${md.replace(/\s*$/, '')}\n\n${crossLink[locale]}\n`;
1450
- }
1451
- if (md !== readFileSync(indexMd, 'utf-8')) {
1452
- sted('write', indexMd, () => writeFileSync(indexMd, md));
1453
- written.push(relative(surfdashRoot, indexMd));
1454
- changed++;
1455
- }
1456
1400
  }
1457
- // 20260929 prod feedback: CMS cards (homepage hero/tag/search) read EmDash rows whose
1458
- // featured_image is snapshotted at translate-sync time — i.e. the pre-patch
1459
- // `./assets/cover.png`, which the deployed worker does not serve (404). The patch only
1460
- // rewrites files, so re-sync the EmDash rows from the patched files through the same
1461
- // `kk executor` path as translate-sync (its plugin child gets EMDASH_API_TOKEN from kk
1462
- // config; a direct orchestrator spawn gets 403 from prod dev-bypass). Runs before the
1463
- // changed===0 early-return so re-runs heal rows an earlier commit left stale
1464
- // (20260925/20260929). Fixture tests pass no plugins-path and must not shell out.
1465
- if (pluginsPath === '') {
1466
- // A missing plugins-path silently drops the CMS re-sync, and the CMS cards then keep the
1467
- // pre-patch `./assets/cover.png` the deployed worker cannot serve (20261002 lost its
1468
- // cover this way). Leave a marker for the classify step's completeness report.
1401
+ const resultPath = join(workDir, '31-publish', `${runDate}_23_patch-surfdash_sync-result.json`);
1402
+ if (process.env.KK_SURFDASH_PATCH_OFFLINE === '1') {
1403
+ // The marker keeps the completeness report's cms-resync row honest in offline runs.
1469
1404
  const skipMarker = join(workDir, '31-publish', `${runDate}_23_patch-surfdash_emdash-resync.txt`);
1470
- sted('write', skipMarker, () => writeRaw(skipMarker, 'skipped: no plugins-path\n'));
1471
- console.error('patch-surfdash: no plugins-path — skip emdash re-sync');
1472
- } else {
1473
- const syncIn = join(workDir, '31-publish', `${runDate}_23_patch-surfdash_sync.json`);
1474
- const syncResult = join(workDir, '31-publish', `${runDate}_23_patch-surfdash_sync-result.json`);
1475
- sted('write', syncIn, () =>
1476
- writeJqText(syncIn, { body: '', options: { operation: 'sync', postPath: join(slugDir, 'index.md') } }),
1477
- );
1478
- const status = kk(
1479
- ['executor', 'run', 'surfdash-pub', '--in', syncIn, '--out', syncResult, ...pluginsArgs(pluginsPath)],
1480
- { unset: NESTED_SPUR_UNSET, soft: true },
1481
- );
1482
- const result = isFile(syncResult) ? readJson(syncResult) : null;
1483
- const ok =
1484
- status === 0 &&
1485
- result !== null &&
1486
- (result.ok === true || (result.targets as Array<{ ok?: boolean }> | undefined)?.[0]?.ok === true);
1487
- if (!ok) {
1488
- console.error(
1489
- `patch-surfdash: WARNING: emdash re-sync failed (exit ${status}) — CMS cards keep the stale cover; fix: kk executor run surfdash-pub --in ${syncIn} --out ${syncResult} --plugins-path ${pluginsPath}`,
1490
- );
1491
- } else {
1492
- console.error('patch-surfdash: emdash rows re-synced from patched files');
1493
- }
1494
- }
1495
- if (changed === 0) {
1496
- console.error('patch-surfdash: nothing to change');
1405
+ sted('write', skipMarker, () => writeRaw(skipMarker, 'skipped: offline (KK_SURFDASH_PATCH_OFFLINE=1)\n'));
1406
+ console.error('patch-surfdash: offline — skip the plugin patch (KK_SURFDASH_PATCH_OFFLINE=1)');
1497
1407
  return;
1498
1408
  }
1499
- const git = (...a: string[]): void => {
1500
- const r = spawnSync('git', ['-C', surfdashRoot, ...a]);
1501
- if (r.status !== 0) failWith(1, `patch-surfdash: git ${a[0]} failed: ${r.stderr?.toString().trim()}`);
1502
- };
1503
- git('add', '--', ...written);
1504
- git(
1505
- 'commit',
1506
- '-m',
1507
- `feat: cover + podcast cross-link for ${basename(slugDir)} (kk daily patch)`,
1508
- '--',
1509
- ...written,
1409
+ const payload = join(workDir, '31-publish', `${runDate}_23_patch-surfdash_patch.json`);
1410
+ const podcastMeta = (pod.metadata ?? {}) as Record<string, unknown>;
1411
+ sted('write', payload, () =>
1412
+ writeJqText(payload, {
1413
+ body: '',
1414
+ options: {
1415
+ operation: 'patch',
1416
+ postPath,
1417
+ coverUrl: typeof podcastMeta.imageUrl === 'string' ? podcastMeta.imageUrl : '',
1418
+ showNotesUrl: typeof pod.url === 'string' ? pod.url : '',
1419
+ dateIso: `${runDate.slice(0, 4)}-${runDate.slice(4, 6)}-${runDate.slice(6, 8)}`,
1420
+ localCover: join(workDir, '23-cover', `${runDate}_07_cover_cover.png`),
1421
+ },
1422
+ }),
1510
1423
  );
1511
- if (process.env.SURFDASH_AUTO_PUSH !== '0') git('push', 'origin', 'main');
1512
- console.error(
1513
- `patch-surfdash: patched ${changed} post(s), committed${process.env.SURFDASH_AUTO_PUSH !== '0' ? ' + pushed' : ' (push skipped: SURFDASH_AUTO_PUSH=0)'}`,
1424
+ const status = kk(
1425
+ ['executor', 'run', 'surfdash-pub', '--in', payload, '--out', resultPath, ...pluginsArgs(pluginsPath)],
1426
+ { unset: NESTED_SPUR_UNSET, env: { SURFDASH_ROOT: surfdashRoot }, soft: true },
1514
1427
  );
1428
+ const result = isFile(resultPath) ? readJson(resultPath) : null;
1429
+ const meta = (result?.metadata ?? {}) as Record<string, unknown>;
1430
+ if (result?.ok === true) {
1431
+ console.error(
1432
+ `patch-surfdash: ${String(result.message ?? 'patched')} — cms-resync ${String(meta.cmsResync ?? 'ok')}, commit ${String(meta.gitCommit ?? 'unchanged')}${Array.isArray(meta.absentLocales) && meta.absentLocales.length > 0 ? `, no post for ${(meta.absentLocales as string[]).join('/')}` : ''}`,
1433
+ );
1434
+ return;
1435
+ }
1436
+ // The posts are patched and committed; only the EmDash rows keep the unservable cover. That is
1437
+ // the soft channel the completeness report carries, so warn instead of failing the run.
1438
+ if (meta.cmsResync === 'failed') {
1439
+ console.error(
1440
+ `patch-surfdash: WARNING: cms re-sync failed — ${String(result?.error ?? `exit ${status}`)}; fix: kk executor run surfdash-pub --in ${payload} --out ${resultPath}${pluginsArgs(pluginsPath).join(' ')}`,
1441
+ );
1442
+ return;
1443
+ }
1444
+ failWith(1, `patch-surfdash: ${String(result?.error ?? `surfdash-pub patch exited ${status}`)}`);
1515
1445
  });
1516
1446
  }
1517
1447
 
@@ -24,10 +24,12 @@ not add a `kk publish` noun.
24
24
  | medium | `medium-pub` | `file-cli` (token API) | `kk executor run medium-pub --in <content.json> --out <result.json>` |
25
25
  | wechatmp | `wechatmp-pub` | `file-cli` (`draft/add`) | `kk executor run wechatmp-pub` (default operation `draft`) |
26
26
  | x post | `x-pub` | `file-cli` (`POST /2/tweets`) | `kk executor run x-pub` (`options.operation` omitted / `post`) |
27
- | x article | `x-pub` | `browser` / `agent` | workflow `agent.run` — not `kk fanout` |
28
- | juejin | `juejin-pub` | `browser` / `agent` | workflow `agent.run` |
29
- | infoq | `infoq-pub` | `browser` / `agent` | workflow `agent.run` |
30
- | substack | `substack-pub` | `browser` / `agent` | workflow `agent.run` |
27
+ | x article | `x-pub` | `browser` / `script` | `kk executor run x-pub --in <content.json> --out <result.json>` (`options.operation: article`); a missing or expired profile refuses (`ok: false`, naming `profile-check.ts login`) before any window opens |
28
+ | juejin | `juejin-pub` | `browser` / `script` | `kk executor run juejin-pub --in <content.json> --out <result.json>` |
29
+ | infoq | `infoq-pub` | `browser` / `script` | `kk executor run infoq-pub --in <content.json> --out <result.json>` |
30
+ | substack | `substack-pub` | `browser` / `script` | `kk executor run substack-pub --in <content.json> --out <result.json>` (needs `SUBSTACK_PUBLICATION_URL`) |
31
+ | threads | `threads-pub` | `file-cli` (Threads Graph, container → `threads_publish`) | `kk executor run threads-pub --in <content.json> --out <result.json>` — text only; needs `THREADS_ACCESS_TOKEN` + `THREADS_USER_ID`, no browser path |
32
+ | weibo | `weibo-pub` | `browser` / `script` | `kk executor run weibo-pub --in <content.json> --out <result.json>`; `options.operation` `article` (头条文章, draft via 保存草稿 by default) or `post` (home composer, which has **no draft** — it requires `metadata.publishStatus: public`) |
31
33
  | xhs | `xhs-pub` | `browser` / `script` | `kk executor run xhs-pub --in <content.json> --out <result.json>` — its default `script` driver uploads the ordered card images with Playwright `setInputFiles`; the `agent` driver fails loud for this channel (ADR-024) |
32
34
 
33
35
  Already-harvested file-cli channels (`qiita-pub`, `zenn-pub`, `surfdash-pub`,
@@ -99,5 +101,7 @@ the confirmed live status; otherwise fail naming the switch.
99
101
  | medium-pub | `MEDIUM_INTEGRATION_TOKEN` |
100
102
  | wechatmp-pub | `WECHAT_APP_ID`, `WECHAT_APP_SECRET` |
101
103
  | x-pub `post` | `X_API_KEY`, `X_API_SECRET`, `X_ACCESS_TOKEN`, `X_ACCESS_TOKEN_SECRET` |
104
+ | threads-pub `post` | `THREADS_ACCESS_TOKEN`, `THREADS_USER_ID` |
105
+ | weibo-pub | none — its session lives in the browser profile (`WEIBO_PROFILE_DIR` overrides the dir; tier: `--profile` > `WEIBO_PROFILE_DIR` > `KK_BROWSER_PROFILE_DIR` > `~/.config/kk/browser-profiles/weibo`) |
102
106
 
103
107
  Missing env → plugin exit 1, `Result.ok false`, error names the variable.
@@ -0,0 +1,93 @@
1
+ # Feature-scoped verification pass (ADR-119, feature D62, task 0872; D63 task
2
+ # 0915 remediation folds the evidence contract into a workflow-owned script).
3
+ #
4
+ # Owns a lifecycle boundary no per-task graph owns: the repo-wide checks whose
5
+ # invariant spans the repository rather than a single task's diff. They run
6
+ # once per feature, deliberately the slow pass; `feature-lifecycle`'s verifying
7
+ # entry invokes this workflow (task 0880 caller wiring), and its verifying→done
8
+ # guard refuses to complete the feature until this pass records PASS (ADR-119
9
+ # consequence: a feature is not done until it passes).
10
+ #
11
+ # Run: spur workflow run feature-verification.yaml --vars '{"featureId":"D63"}'
12
+ #
13
+ # Shape: verify -> done | failed. The onEnter step runs the standard script
14
+ # `feature-verification-steps.ts` (no public CLI verb, D63 task 0915): it
15
+ # resolves this very definition for the verifier identity (name, source path,
16
+ # layer, definition digest), records the RUNNING receipt (run-scoped +
17
+ # feature-latest copies), registers the run-scoped receipt as a run artifact,
18
+ # runs the configured verification command via the safe launch splitter, and
19
+ # completes the receipt PASS/FAIL with the after-pass proof-input digest. A
20
+ # changed tree during the pass records FAIL.
21
+ #
22
+ # Reliability (aligned with task-pipeline / ADR-043): no agent.run nodes; the
23
+ # verification command comes from `vars.verificationCmd` below and is never
24
+ # interpolated through a shell. The always-exit-0 onEnter means missing,
25
+ # corrupt or FAIL evidence routes to `failed` via the guard (fail-closed),
26
+ # never a silent PASS; infrastructure errors exit nonzero and surface.
27
+ "$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
28
+ kind: state-machine
29
+ name: feature-verification
30
+ version: "1"
31
+ description: "Feature-scoped verification pass (ADR-119): runs the repo-wide check set once per feature and records a bound PASS/FAIL evidence receipt. Invoked by feature-lifecycle's verifying entry."
32
+ iterationBound: 2
33
+ initialState: verify
34
+ terminalStates:
35
+ - done
36
+ - failed
37
+ failureStates:
38
+ - failed
39
+ vars:
40
+ featureId: "X"
41
+ spurBin: "spur"
42
+ verificationCmd: "bun run spur-check"
43
+ __runId: ""
44
+
45
+ states:
46
+ - id: verify
47
+ description: >
48
+ Resolve this run's installed sp script root via Superskill, then run the
49
+ pinned installed verifier with Bun against the repo-wide check set. The
50
+ verifier records the bound evidence receipt (verifier identity,
51
+ checked-input digest, run identity) and the coarse status file. Both
52
+ actions fail closed — a locator/script failure, an unresolved or
53
+ non-installed root, an absent verifier, or a failing check yields a
54
+ non-PASS status — and always exit 0, so the transition guard routes
55
+ missing/corrupt/FAIL to `failed`.
56
+ onEnter:
57
+ - kind: shell
58
+ options:
59
+ command: >-
60
+ S="$(superskill script path sp script-root.mjs 2>/dev/null)" || S="";
61
+ if [ -n "$S" ] && [ -f "$S" ]; then node "$S" --run-id "$__runId" --spur-bin "$spurBin";
62
+ else echo "script-root failed closed — run 'superskill install sp --marketplace gobing-ai/spur'" >&2;
63
+ fi;
64
+ exit 0
65
+ - kind: shell
66
+ options:
67
+ command: >-
68
+ mkdir -p .spur/run;
69
+ DIR="$(jq -r 'select(.mode == "installed" and (.dir | type == "string") and .dir != "") | .dir' ".spur/run/$__runId-script-root.json" 2>/dev/null)" || DIR="";
70
+ [ -n "$DIR" ] && [ -f "$DIR/feature-verification-steps.mjs" ] && S="$DIR/feature-verification-steps.mjs";
71
+ [ -f "$S" ] && bun "$S" verify --feature-id "$featureId" --run-id "$__runId" --spur-bin "$spurBin" || printf 'FAIL feature-verification failed closed — unresolved root or verifier failure\n' > ".spur/run/$__runId-feature-verification.status";
72
+ exit 0
73
+
74
+ - id: done
75
+ description: Terminal — repo-wide checks passed and the receipt is bound.
76
+
77
+ - id: failed
78
+ description: Terminal — repo-wide checks failed or the evidence is missing/corrupt.
79
+
80
+ transitions:
81
+ - from: verify
82
+ to: done
83
+ description: Repo-wide checks passed — the script completed the receipt with PASS.
84
+ guard:
85
+ kind: shell
86
+ options:
87
+ command: 'test "$(cat .spur/run/$__runId-feature-verification.status 2>/dev/null)" = PASS'
88
+ - from: verify
89
+ to: failed
90
+ terminalReason: failed-check
91
+ description: Repo-wide checks failed (or the evidence is missing/corrupt) — stop at failed.
92
+ guard:
93
+ kind: always
@@ -91,6 +91,7 @@ transitions:
91
91
  # FAIL, out-of-contract verdict, or NEEDS_REVISION with no retries left -> failed.
92
92
  - from: judge
93
93
  to: failed
94
+ terminalReason: failed-guard
94
95
  description: "Judge verdict is FAIL, out-of-contract, or NEEDS_REVISION with retries exhausted -> block publish"
95
96
  guard:
96
97
  kind: shell
@@ -27,6 +27,10 @@ vars:
27
27
  # builtin — a shell command may only reference declared vars). It is the lock owner and the
28
28
  # `collect_runs` / ledger identity.
29
29
  __runId: ""
30
+ # Definition digest of the executed workflow, injected by spur at launch (G8/0252 §9). The
31
+ # Board capture compares it with the public `spur workflow list --json` catalog and reads the
32
+ # topology only on exact agreement; a missing/mismatched digest omits topology with a diagnostic.
33
+ __definitionDigest: ""
30
34
  # Per-source collection parameters, seeded into the plugin source configs by `lock`.
31
35
  limit: "10"
32
36
  last30days_topic: "AI news"
@@ -72,18 +76,34 @@ states:
72
76
  # IS the lock contract, and the guard below reads it back:
73
77
  # `{"acquired":true}` or `{"skipped":"locked","holder":<run>}` (R1).
74
78
  command: >-
75
- kk store lock acquire --name collect --run "${vars.__runId}" --ttl 21600000 > "${vars.work_dir}/lock.json"
79
+ kk lock acquire --name collect --run "${vars.__runId}" --ttl 21600000 > "${vars.work_dir}/lock.json"
76
80
 
77
81
  - id: collect
78
82
  description: >-
79
- Fan-in every ingestion source into the run's blended Doc[] (same sources and timeout overrides as
80
- kk-daily-ai-voice's collect state) and render the core-facts markdown with the existing stage (R3/R4).
83
+ Bind the run to its workspace for the Board (G8/0252 §9), then fan-in every ingestion source into
84
+ the run's blended Doc[] (same sources and timeout overrides as kk-daily-ai-voice's collect state)
85
+ and render the core-facts markdown with the existing stage (R3/R4).
81
86
  onEnter:
82
87
  - kind: shell
83
- # onError: continue on both actions, with the artifact guard below as the positive success
84
- # contract: a state-machine aborts the run on a failed action without taking a transition, so a
85
- # hard failure here would leave the lock behind. The run still ends `failed` — through
86
- # `release-failed`, which releases the lock first.
88
+ # Board capture (G8/0252 §9): revoke any previous workspace owner and bind THIS run to the
89
+ # workspace before any collection output exists. Revocation is mandatory — a refusal writes no
90
+ # board-capture.json, so the artifact guard below routes to `release-failed` and no result is
91
+ # ever stored over another owner's files. An optional capture failure after a successful
92
+ # revocation warns (captured:false) and only disables Board previews.
93
+ # onError: continue keeps the lock-releasing failure path intact instead of a hard abort.
94
+ options:
95
+ onError: continue
96
+ command: >-
97
+ kk board capture --workflow kk-collect --run "${vars.__runId}"
98
+ --work-dir "${vars.work_dir}" --stamp "${vars.stamp}"
99
+ --definition-digest "${vars.__definitionDigest}"
100
+ --setting "limit=${vars.limit}" --setting "horizon_hours=${vars.horizon_hours}"
101
+ > "${vars.work_dir}/board-capture.json"
102
+ - kind: shell
103
+ # onError: continue on both ingestion actions, with the artifact guard below as the positive
104
+ # success contract: a state-machine aborts the run on a failed action without taking a
105
+ # transition, so a hard failure here would leave the lock behind. The run still ends `failed`
106
+ # — through `release-failed`, which releases the lock first.
87
107
  onError: continue
88
108
  options:
89
109
  # horizon-ingest's latency straddles the fan-in 30m per-source default (0098/0099), so it gets
@@ -153,7 +173,7 @@ states:
153
173
  - kind: shell
154
174
  options:
155
175
  command: >-
156
- kk store lock release --name collect --run "${vars.__runId}"
176
+ kk lock release --name collect --run "${vars.__runId}"
157
177
 
158
178
  - id: release-failed
159
179
  description: >-
@@ -164,7 +184,7 @@ states:
164
184
  onError: continue
165
185
  options:
166
186
  command: >-
167
- kk store lock release --name collect --run "${vars.__runId}"
187
+ kk lock release --name collect --run "${vars.__runId}"
168
188
 
169
189
  - id: done
170
190
  description: "Terminal — collection stored; the daily workflow reads it back through `kk store extract`"
@@ -192,13 +212,14 @@ transitions:
192
212
 
193
213
  - from: collect
194
214
  to: store
195
- description: "Blended Doc[] written and core facts rendered -> store it"
215
+ description: "Blended Doc[], core facts and Board capture recorded -> store it"
196
216
  guard:
197
217
  kind: shell
198
218
  options:
199
219
  command: >-
200
220
  test -s "${vars.work_dir}/10-ingest/${vars.stamp}_02_collect_blended.json" &&
201
- test -s "${vars.work_dir}/20-facts/${vars.stamp}_02_collect_core-facts.md"
221
+ test -s "${vars.work_dir}/20-facts/${vars.stamp}_02_collect_core-facts.md" &&
222
+ test -s "${vars.work_dir}/board-capture.json"
202
223
 
203
224
  - from: collect
204
225
  to: release-failed
@@ -232,6 +253,17 @@ transitions:
232
253
  # refusal names the offending value on stderr before the transition falls through to
233
254
  # `release-failed` — fail-closed, and never a deadlock (that state is `always`).
234
255
  # Kept at 3 logical commands: spur's ADR-115 composition lint errors on a guard above 5.
256
+ # Task 0268 — degraded pass is a WARNING, not a gate. `kk store label` prints
257
+ # `degraded: true` and writes `kk.store.label.degraded: fallback: N/M consults (P%)` to
258
+ # stderr whenever the fallback share of its consults exceeds 0.5 — a majority of consults
259
+ # answered `source: default` (spur's `no-backend` degradation). Chosen at one-half because
260
+ # the 2026-10-12 incident measured `fallback: 22/26` (0.85) and `33/45` (0.73), well past
261
+ # it, while a healthy pass stays near zero; the threshold is the operator's signal in
262
+ # launchd.log/the ledger, not a transition. A degraded pass still prints `verdict: PASS`,
263
+ # so this guard releases the lock and the labels it DID write survive for the next tick —
264
+ # making the verdict non-`PASS` instead would route to `release-failed` and, with the
265
+ # label state's `onError: continue`, throw away a whole day's labels for a backend the
266
+ # next tick can reach. The fail-closed defaults themselves are unchanged (task 0268 R3).
235
267
  command: >-
236
268
  test "$(jq -r 'if (.verdict // "") == "PASS" then (.verdict + (if ((.confidence // "") | test("^(HIGH|MEDIUM|LOW)$")) then "" else "!" end)) else (.verdict // "") end' "${vars.work_dir}/label-result.txt" 2>/dev/null)" = PASS ||
237
269
  { echo "kk-collect.label.confidence-refused: verdict '$(jq -r '.verdict // "<none>"' "${vars.work_dir}/label-result.txt" 2>/dev/null)' carries confidence '$(jq -r '.confidence // "<absent>"' "${vars.work_dir}/label-result.txt" 2>/dev/null)', not HIGH|MEDIUM|LOW" >&2; false; }
@@ -21,7 +21,9 @@ vars:
21
21
  limit: "10"
22
22
  language: "zh"
23
23
  voice_profile: "robin-news"
24
- # Voice driver switch — voicebox (voice-gen) | ominivoice (omni-voice-gen) | gemini (gemini-tts).
24
+ # Voice driver switch — voicebox (voice-gen) | ominivoice (omni-voice-gen) | gemini (gemini-tts)
25
+ # | voxcpm (voxcpm-tts, opt-in; ADR-036). VoxCPM never probes Gemini and never falls back; its
26
+ # 5.6 字/秒 spoken-rate estimate is PROVISIONAL until 0258 measures a real render.
25
27
  # 20260917 introduced the switch; 20260920 flipped default to voicebox after an operator A/B;
26
28
  # 20260929 operator override: gemini-tts is the default again (verified live on this machine —
27
29
  # robin-news replicated profile, GEMINI_API_KEY + GEMINI_TTS_MODEL via ~/.config/kk/config.yaml
@@ -400,7 +402,7 @@ states:
400
402
  kk stage daily-wrap-docs "${vars.work_dir}" "${vars.run_date}" "${vars.voice_profile}"
401
403
 
402
404
  - id: generate
403
- description: "Invoke voice driver plugin (vdriver var: ominivoice -> omni-voice-gen, voicebox -> voice-gen, gemini -> gemini-tts) to generate audio"
405
+ description: "Invoke voice driver plugin (vdriver var: ominivoice -> omni-voice-gen, voicebox -> voice-gen, gemini -> gemini-tts, voxcpm -> voxcpm-tts) to generate audio"
404
406
  onEnter:
405
407
  - kind: shell
406
408
  options: