@gobing-ai/knowledge-kit 0.0.19 → 0.0.21

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.
@@ -12,13 +12,21 @@
12
12
  * wrap [<override>] --in <voicescript.yaml> --out <docs.json> [--profile <name>]
13
13
  * duration <work_dir> <target_duration_min>
14
14
  * render-storm [<override>] --in <content.json> --out <content.md>
15
+ * itc-outline-pick <work_dir> (0149, body in the sibling itc-stages.ts)
16
+ * itc-report <work_dir> [max_revisions] (0149, body in the sibling itc-stages.ts)
17
+ * itc-materialize <work_dir> <publish_live> (0150, sibling)
18
+ * itc-publish-plan <work_dir> <targets> <source_locale> <publish_live> (0150, sibling)
19
+ * itc-publish-fanout <work_dir> [--retry] (0151, sibling)
20
+ * itc-publish-next <work_dir> (0151, sibling)
15
21
  *
16
22
  * `validate` / `wrap` / `render-storm` resolve the shipped sibling sidecars and delegate to them;
17
23
  * nothing about those sidecars changed. The `prepare-*` verbs and `duration` keep the logic that
18
24
  * used to live inline in `plugins/kk/workflows/*.yaml`, message-for-message.
19
25
  *
20
- * Node/Bun builtins only — no @gobing-ai/* and no relative imports (the file is resolved from the
21
- * installed package or from a checkout).
26
+ * Node/Bun builtins only — no @gobing-ai/* and no cross-directory relative imports (the file is
27
+ * resolved from the installed package or from a checkout). The itc-* stages delegate to their
28
+ * same-directory sibling `itc-stages.ts`, imported lazily so a runner copy without the sibling
29
+ * still loads and fails loud with the remediation path.
22
30
  *
23
31
  * The YAML prelude resolves this file as `$pkg/plugins/kk/scripts/kk-workflow-stages.ts` where `pkg`
24
32
  * comes from `realpath "$(command -v kk)"` (the installed package root), and falls back to the
@@ -63,6 +71,21 @@ function fail(message: string): never {
63
71
  process.exit(1);
64
72
  }
65
73
 
74
+ /**
75
+ * Load the itc-* helper sibling lazily: this runner is resolved (and copied alone, e.g. by
76
+ * apps/cli/tests/kk-workflow-stages.test.ts) from installs where the sibling may be absent, so
77
+ * the itc-* stages fail loud with a remediation path instead of breaking every stage at load.
78
+ */
79
+ async function loadItcStages(): Promise<typeof import('./itc-stages.ts')> {
80
+ try {
81
+ return await import('./itc-stages.ts');
82
+ } catch {
83
+ return fail(
84
+ 'itc-stages.ts not found next to kk-workflow-stages.ts — install or refresh the kk package (bun link in a checkout)',
85
+ );
86
+ }
87
+ }
88
+
66
89
  /** coreutils-style reason for the errno codes the staged file operations can hit. */
67
90
  const ERRNO_REASONS: Record<string, string> = {
68
91
  EACCES: 'Permission denied',
@@ -904,7 +927,14 @@ function dailyScript(args: string[]): void {
904
927
  sted('write', yamlOut, () => writeRaw(yamlOut, body));
905
928
  // R3 (task 0139): fresh writes are validated here; the script cache path is covered by
906
929
  // wrap-docs, which runs on every path that reaches audio.
907
- assertScriptContent(yamlOut);
930
+ try {
931
+ assertScriptContent(yamlOut);
932
+ } catch (err) {
933
+ // A rejected fresh write would otherwise poison the isFile(yamlOut) cache: every
934
+ // resume skips regeneration and re-fails at wrap-docs. Drop it so retry regenerates.
935
+ unlinkSync(yamlOut);
936
+ throw err;
937
+ }
908
938
  });
909
939
  }
910
940
 
@@ -1318,9 +1348,12 @@ function dailyPatchSurfdash(args: string[]): void {
1318
1348
  };
1319
1349
  let changed = 0;
1320
1350
  for (const [locale, dir] of [
1351
+ // zh posts live under articles/zh/<slug>; en/ja under articles/<locale>/<slug> —
1352
+ // two dirnames up, not one (20260919: `articles/zh/en/...` never existed, so en/ja
1353
+ // were silently skipped by the not-published branch).
1321
1354
  ['zh', slugDir],
1322
- ['en', join(dirname(slugDir), 'en', basename(slugDir))],
1323
- ['ja', join(dirname(slugDir), 'ja', basename(slugDir))],
1355
+ ['en', join(dirname(dirname(slugDir)), 'en', basename(slugDir))],
1356
+ ['ja', join(dirname(dirname(slugDir)), 'ja', basename(slugDir))],
1324
1357
  ] as const) {
1325
1358
  const indexMd = join(dir, 'index.md');
1326
1359
  if (!existsSync(indexMd)) {
@@ -1330,17 +1363,28 @@ function dailyPatchSurfdash(args: string[]): void {
1330
1363
  let md = readFileSync(indexMd, 'utf-8');
1331
1364
  const fmEnd = md.indexOf('---', 3); // frontmatter is `---\n ... \n---\n`
1332
1365
  if (fmEnd < 0) failWith(1, `patch-surfdash: no frontmatter in ${indexMd}`);
1333
- if (!/^image:/m.test(md.slice(0, fmEnd))) {
1334
- let image = coverUrl;
1335
- if (image === '' && existsSync(localCover)) {
1336
- // Fallback for pre-#6 runs: copy the local cover instead of a hosted URL.
1366
+ // Upsert a single-line `key: 'value'`; the regex consumes folded-scalar continuation
1367
+ // lines (`og_image: >-\n https://...`) so multi-line publish output is replaced whole.
1368
+ const upsertKey = (src: string, key: string, value: string): string => {
1369
+ const line = `${key}: '${value}'`;
1370
+ const re = new RegExp(`^${key}:.*(?:\\n[ \\t]+.*)*`, 'm');
1371
+ if (re.test(src)) return src.replace(re, line);
1372
+ return src.replace(/^publishDate: .*$/m, (m0) => `${m0}\n${line}`);
1373
+ };
1374
+ if (coverUrl !== '') {
1375
+ // 20260919 prod feedback: the podcast-hosted cover URL wins even when surfdash-pub
1376
+ // already wrote `image:` — its resolved og_image points at /content-posts/<slug>/
1377
+ // assets/cover.png, which the deployed worker does not serve (404).
1378
+ md = upsertKey(md, 'image', coverUrl);
1379
+ md = upsertKey(md, 'og_image', coverUrl);
1380
+ } else if (!/^image:/m.test(md.slice(0, fmEnd))) {
1381
+ // Fallback for runs without a podcast cover: copy the local cover instead.
1382
+ if (existsSync(localCover)) {
1337
1383
  const assets = join(dir, 'assets');
1338
1384
  mkdirSync(assets, { recursive: true });
1339
1385
  copyFileSync(localCover, join(assets, 'cover.png'));
1340
- image = './assets/cover.png';
1341
- }
1342
- if (image !== '') {
1343
- md = md.replace(/^publishDate: .*$/m, (m0) => `${m0}\nimage: '${image}'\nog_image: '${image}'`);
1386
+ md = upsertKey(md, 'image', './assets/cover.png');
1387
+ md = upsertKey(md, 'og_image', './assets/cover.png');
1344
1388
  }
1345
1389
  }
1346
1390
  if (showNotesUrl !== '' && !md.includes(showNotesUrl)) {
@@ -1626,8 +1670,43 @@ if (import.meta.main)
1626
1670
  case 'daily-translate-sync':
1627
1671
  dailyTranslateSync(rest);
1628
1672
  break;
1673
+ case 'itc-outline-pick': {
1674
+ const { itcOutlinePick } = await loadItcStages();
1675
+ itcOutlinePick(rest);
1676
+ break;
1677
+ }
1678
+ case 'itc-report': {
1679
+ const { itcReport } = await loadItcStages();
1680
+ itcReport(rest);
1681
+ break;
1682
+ }
1683
+ case 'itc-materialize': {
1684
+ const { itcMaterialize } = await loadItcStages();
1685
+ itcMaterialize(rest);
1686
+ break;
1687
+ }
1688
+ case 'itc-publish-plan': {
1689
+ const { itcPublishPlan } = await loadItcStages();
1690
+ itcPublishPlan(rest);
1691
+ break;
1692
+ }
1693
+ case 'itc-publish-fanout': {
1694
+ const { itcPublishFanout } = await loadItcStages();
1695
+ itcPublishFanout(rest);
1696
+ break;
1697
+ }
1698
+ case 'itc-publish-next': {
1699
+ const { itcPublishNext } = await loadItcStages();
1700
+ itcPublishNext(rest);
1701
+ break;
1702
+ }
1703
+ case 'itc-adapt-verify': {
1704
+ const { itcAdaptVerify } = await loadItcStages();
1705
+ itcAdaptVerify(rest);
1706
+ break;
1707
+ }
1629
1708
  default:
1630
1709
  fail(
1631
- `unknown stage: ${stage ?? '(none)'} (expected prepare-itc|prepare-solo|prepare-storm|validate|wrap|duration|render-storm|daily-*)`,
1710
+ `unknown stage: ${stage ?? '(none)'} (expected prepare-itc|prepare-solo|prepare-storm|validate|wrap|duration|render-storm|itc-*|daily-*)`,
1632
1711
  );
1633
1712
  }
@@ -0,0 +1,85 @@
1
+ ---
2
+ name: article-adapt
3
+ # Description rules: front-load the leading identity phrase; one trigger per genuine
4
+ # branch (collapse synonym triggers into one); never restate the body's identity line.
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
+ ---
7
+
8
+ # article-adapt
9
+
10
+
11
+ This skill adapts one published article into one Content file per `publish-plan.json` variant with
12
+ `adapt: true`. It is deliberately **thin**: the plan (built from `TARGET_PROFILES` in
13
+ `plugins/kk/scripts/itc-stages.ts`) carries the per-variant locale, format and limits — this skill
14
+ holds **no platform table** and never decides which variants exist.
15
+
16
+ ## Inputs
17
+
18
+ - `<work_dir>/4-publish/publish-plan.json` — `{live, sourceLocale, variants, fanout, browser}` where
19
+ each `variants[<locale>-<format>]` entry is
20
+ `{file, adapt, limits?, switches}`.
21
+ - `<work_dir>/4-publish/content.json` — the source article (the `sourceLocale` materialized draft).
22
+
23
+ ## Output
24
+
25
+ For every `variants[*]` entry with `adapt: true`, write
26
+ `<work_dir>/4-publish/<entry.file>` (e.g. `content.zh-article.json`) as a Content JSON object:
27
+
28
+ ```json
29
+ {
30
+ "title": "<translated title>",
31
+ "format": "<variant format suffix, e.g. article | wechat-article | short-note | short-post>",
32
+ "body": "<translated body>",
33
+ "metadata": { "coverImage": "...", "tags": [], "description": "..." },
34
+ "references": [{ "url": "...", "title": "...", "cite": "id" }]
35
+ }
36
+ ```
37
+
38
+ ## Procedure
39
+
40
+ 1. Read the plan. Collect the variants whose `adapt` is `true`. If there are none, report that and
41
+ stop — do not write any file.
42
+ 2. For each such variant, read `content.json` and produce the variant file:
43
+ - **Translate** `title` and `body` into the variant locale (the locale is the part of the variant
44
+ key before the first `-`). Keep every code block, URL and inline link verbatim — never
45
+ translate code, identifiers, commands or URLs.
46
+ - **Carry over** `references[]`, `metadata.coverImage`, `metadata.tags`, `metadata.keywords` and
47
+ `metadata.description` unchanged.
48
+ - **Apply the limits** from the entry's `limits` (e.g. a max character count): trim the body
49
+ within the limit without dropping code blocks or breaking markdown; state in one closing line
50
+ that the full article is available at the source URL when content had to be cut.
51
+ - **Set `format`** to the variant key's format suffix (the part after the first `-`).
52
+ 3. Write the file with `metadata` present (at minimum `{}`). A valid Content object always has a
53
+ non-empty `title` and `body`.
54
+ 4. Report each written path as the last line of stdout.
55
+
56
+ ## Invariants
57
+
58
+ - **Never set draft or live switch keys.** Do not write `metadata.published`,
59
+ `metadata.publishStatus` or `metadata.operation`: `itc-publish-fanout` stamps the plan's
60
+ `switches` into each variant's metadata at send time (V3 — the switch is deterministic code, not
61
+ model output).
62
+ - **No platform knowledge.** If the plan names a variant, adapt it; if a limit is missing, adapt
63
+ without a limit. Never hard-code target names, locales or limits in this skill.
64
+ - **Faithful translation.** Do not add, remove or reorder sections relative to the source body; the
65
+ approved outline's structure is preserved.
66
+
67
+ ## Gotchas
68
+
69
+ 1. **Read the plan, not the target list** — the plan, not `publish_targets`, is the adaptation
70
+ contract.
71
+ 2. **A missing source `content.json` is a blocking error** — the materialize state always writes it
72
+ before publish-plan; if it is absent, fail loudly instead of inventing content.
73
+ 3. **One file per adapt variant, exact path** — write `<entry.file>` under `4-publish/`; a
74
+ follow-up `kk stage itc-adapt-verify` step fails the workflow when a variant file is missing or
75
+ lacks `title`/`body`.
76
+
77
+ ## Platform Notes
78
+
79
+ ### Claude Code
80
+
81
+ Invoked from a spur workflow `agent.run` step; the prompt supplies the work_dir path.
82
+
83
+ ### Codex / OpenClaw / OpenCode / Antigravity
84
+
85
+ Run file reads/writes via the available file tools; arguments arrive in the step prompt.
@@ -19,8 +19,10 @@ vars:
19
19
  limit: "10"
20
20
  language: "zh"
21
21
  voice_profile: "robin-news"
22
- # 20260917: voice driver switch — ominivoice (default, omni-voice-gen) | voicebox (voice-gen).
23
- vdriver: "ominivoice"
22
+ # 20260917: voice driver switch — voicebox (default, voice-gen) | ominivoice (omni-voice-gen).
23
+ # 20260920: default flipped to voicebox — operator A/B at matched speech rate (~5.2 chars/s)
24
+ # judged voicebox clearly better (ominivoice carries ~1min dead air over 16min + ASR-garble class).
25
+ vdriver: "voicebox"
24
26
  # 20260917 review #7: 8s intro sting prepended to every daily output; empty = skip.
25
27
  intro_music: ".works/kk-daily-ai-voice/AI每日资讯_8秒播客片头.mp3"
26
28
  # 20260917 review #4/#6: surfdash checkout the patch stage commits to.
@@ -463,8 +465,14 @@ transitions:
463
465
  kind: always
464
466
 
465
467
  - from: generate
468
+ to: intro-music
469
+ description: "Audio generated -> prepend intro sting"
470
+ guard:
471
+ kind: always
472
+
473
+ - from: intro-music
466
474
  to: quality-control
467
- description: "Audio generated -> run quality control validation"
475
+ description: "Intro merged -> run quality control validation"
468
476
  guard:
469
477
  kind: always
470
478
 
@@ -518,10 +526,12 @@ transitions:
518
526
  kind: always
519
527
 
520
528
  - from: publish
521
- to: patch-surfdash
522
- description: "Publish classified -> patch surfdash posts with cover + podcast cross-link"
529
+ to: translate
530
+ description: "Publish classified -> full publish continues to en/ja translation; guard is full-only (partial exits via publish-partial); patch-surfdash runs after translate so all three locales get cover + podcast cross-link patching"
523
531
  guard:
524
- kind: always
532
+ kind: shell
533
+ options:
534
+ command: 'test "${vars.publish_status}" = "full"'
525
535
 
526
536
  - from: patch-surfdash
527
537
  to: run-report
@@ -551,24 +561,24 @@ transitions:
551
561
  guard:
552
562
  kind: always
553
563
 
554
- - from: run-report
555
- to: translate
556
- description: "Run report assembled after full publish -> scaffold + translate en/ja locales + sync"
564
+ - from: translate
565
+ to: patch-surfdash
566
+ description: "zh post + en/ja locales scaffolded, translated, and synced -> patch all published posts with cover + podcast cross-link"
557
567
  guard:
558
- kind: shell
559
- options:
560
- command: 'test "${vars.publish_status}" = "full"'
568
+ kind: always
561
569
 
562
570
  - from: run-report
563
571
  to: done
564
- description: "Run report assembled after partial publish -> done; retry failed targets later via kk executor fan-out --retry-from"
572
+ description: "Run report assembled after non-full publish (partial) -> done; retry failed targets later via kk executor fan-out --retry-from"
565
573
  guard:
566
574
  kind: shell
567
575
  options:
568
576
  command: 'test "${vars.publish_status}" = "partial"'
569
577
 
570
- - from: translate
578
+ - from: run-report
571
579
  to: done
572
- description: "zh post + en/ja locales scaffolded, translated, and synced into the surfdash CMS -> episode complete"
580
+ description: "Run report assembled after full publish -> episode complete (en/ja already translated and patched)"
573
581
  guard:
574
- kind: always
582
+ kind: shell
583
+ options:
584
+ command: 'test "${vars.publish_status}" = "full"'