@gobing-ai/knowledge-kit 0.0.18 → 0.0.19

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.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "plugins": [
8
8
  {
9
- "version": "0.0.18",
9
+ "version": "0.0.19",
10
10
  "name": "kk",
11
11
  "source": "./plugins/kk",
12
12
  "description": "knowledge-kit — a set of toolkit for knowledge management"
package/dist/index.js CHANGED
@@ -26420,24 +26420,38 @@ function spawnPluginEntry(opts) {
26420
26420
  stderr: proc.stderr.toString().trim()
26421
26421
  };
26422
26422
  }
26423
- function killProcessTree(pid) {
26423
+ var KILL_TREE_SETTLE_MS = 100;
26424
+ function listChildren(pid) {
26424
26425
  let stdout;
26425
26426
  try {
26426
26427
  stdout = Bun.spawnSync(["pgrep", "-P", String(pid)]).stdout.toString();
26427
26428
  } catch {
26428
26429
  stdout = "";
26429
26430
  }
26430
- for (const line of stdout.trim().split(`
26431
- `)) {
26432
- const child = Number(line);
26433
- if (Number.isInteger(child) && child > 0) {
26434
- killProcessTree(child);
26435
- }
26431
+ return stdout.trim().split(`
26432
+ `).map(Number).filter((child) => Number.isInteger(child) && child > 0);
26433
+ }
26434
+ function killSubtree(pid) {
26435
+ for (const child of listChildren(pid)) {
26436
+ killSubtree(child);
26436
26437
  }
26437
26438
  try {
26438
26439
  process.kill(pid, "SIGKILL");
26439
26440
  } catch {}
26440
26441
  }
26442
+ async function killProcessTree(pid) {
26443
+ const sweep = () => {
26444
+ for (const child of listChildren(pid)) {
26445
+ killSubtree(child);
26446
+ }
26447
+ };
26448
+ sweep();
26449
+ await Bun.sleep(KILL_TREE_SETTLE_MS);
26450
+ sweep();
26451
+ try {
26452
+ process.kill(pid, "SIGKILL");
26453
+ } catch {}
26454
+ }
26441
26455
  async function spawnPluginEntryAsync(opts) {
26442
26456
  let timedOut = false;
26443
26457
  let timer;
@@ -27445,6 +27459,46 @@ function registerPluginCommand(program2) {
27445
27459
  registerStatusVerb(plugins, "doctor", "Alias for `plugin status` (deprecated)", true);
27446
27460
  }
27447
27461
 
27462
+ // src/stage.ts
27463
+ import { spawnSync } from "child_process";
27464
+ import { existsSync as existsSync5, realpathSync as realpathSync3 } from "fs";
27465
+ import { dirname as dirname3, join as join5, resolve as resolve6 } from "path";
27466
+ var STAGES_REL = "plugins/kk/scripts/kk-workflow-stages.ts";
27467
+ function resolveStagesScript(argv1 = process.argv[1]) {
27468
+ let fromPkg = "";
27469
+ if (argv1) {
27470
+ try {
27471
+ fromPkg = join5(dirname3(dirname3(realpathSync3(argv1))), STAGES_REL);
27472
+ } catch {
27473
+ fromPkg = "";
27474
+ }
27475
+ }
27476
+ if (fromPkg && existsSync5(fromPkg))
27477
+ return fromPkg;
27478
+ const fromCwd = resolve6(STAGES_REL);
27479
+ if (existsSync5(fromCwd))
27480
+ return fromCwd;
27481
+ throw new Error(`kk-workflow-stages.ts not found at ${fromPkg || STAGES_REL} \u2014 install or refresh the kk package (bun link in a checkout)`);
27482
+ }
27483
+ function runStage(args, execPath = process.execPath, argv1) {
27484
+ const script = resolveStagesScript(argv1);
27485
+ const res = spawnSync(execPath, [script, ...args], { stdio: "inherit" });
27486
+ if (res.error) {
27487
+ throw new Error(`failed to spawn stage runner at ${script}: ${res.error.message}`);
27488
+ }
27489
+ return res.status ?? 1;
27490
+ }
27491
+
27492
+ // src/commands/stage.ts
27493
+ function registerStageCommand(program2) {
27494
+ program2.command("stage").description("Run a kk-workflow-stages.ts stage (workflow orchestration steps)").argument("<args...>", "Stage name followed by its positional arguments").allowUnknownOption(true).action((args) => {
27495
+ const code = runStage(args);
27496
+ if (code !== 0) {
27497
+ throw new CommanderError(code, "kk.stage.failed", `stage failed with exit code ${code}`);
27498
+ }
27499
+ });
27500
+ }
27501
+
27448
27502
  // src/config.ts
27449
27503
  import { readFileSync as readFileSync4 } from "fs";
27450
27504
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -27460,6 +27514,7 @@ function createProgram() {
27460
27514
  const program2 = new Command().name(CLI_CONFIG.binaryName).description("knowledge-kit CLI").version(CLI_CONFIG.binaryVersion).option("-v, --verbose", "enable verbose diagnostics");
27461
27515
  registerExecutorCommand(program2);
27462
27516
  registerPluginCommand(program2);
27517
+ registerStageCommand(program2);
27463
27518
  return program2;
27464
27519
  }
27465
27520
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/knowledge-kit",
3
- "version": "0.0.18",
3
+ "version": "0.0.19",
4
4
  "description": "An ingest → create → publish content pipeline CLI (Bun).",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21744,7 +21744,7 @@ function findProjectRoot(startDir) {
21744
21744
  var init_file_system_node = () => {};
21745
21745
 
21746
21746
  // ../../plugins/generations/voice-gen/src/index.ts
21747
- import { basename, dirname as dirname2, extname, join, resolve } from "path";
21747
+ import { basename, dirname as dirname2, extname, join as join2, resolve } from "path";
21748
21748
  import { parseArgs } from "util";
21749
21749
 
21750
21750
  // ../../packages/kk-core/src/kinds.ts
@@ -22413,6 +22413,60 @@ function createVoiceboxClient(options = {}) {
22413
22413
  };
22414
22414
  }
22415
22415
 
22416
+ // ../../plugins/generations/voice-gen/src/voicebox-server.ts
22417
+ import { spawn as nodeSpawn } from "child_process";
22418
+ import { existsSync as existsSync2, openSync } from "fs";
22419
+ import { homedir } from "os";
22420
+ import { join } from "path";
22421
+ var BOOT_WAIT_MS = 30000;
22422
+ var POLL_MS = 500;
22423
+ var LOG_PATH = "/tmp/voicebox-server.log";
22424
+ async function probeVoiceboxHealth(url2, customFetch) {
22425
+ try {
22426
+ return (await customFetch(`${url2}/health`)).ok;
22427
+ } catch {
22428
+ return false;
22429
+ }
22430
+ }
22431
+ async function ensureVoiceboxRunning(options = {}) {
22432
+ const rawUrl = options.url ?? process.env.VOICEBOX_URL ?? "http://127.0.0.1:17493";
22433
+ const customFetch = options.fetch ?? fetch;
22434
+ const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
22435
+ const customSpawn = options.spawn ?? nodeSpawn;
22436
+ if (await probeVoiceboxHealth(rawUrl, customFetch)) {
22437
+ return;
22438
+ }
22439
+ const url2 = new URL(rawUrl);
22440
+ const local = ["127.0.0.1", "localhost", "::1"].includes(url2.hostname);
22441
+ if (!local) {
22442
+ throw new Error(`voice-gen: Voicebox not reachable at ${rawUrl} \u2014 auto-start only applies to local URLs; is the remote server up? (fix VOICEBOX_URL)`);
22443
+ }
22444
+ const serverBin = options.serverBin ?? process.env.VOICEBOX_SERVER_BIN ?? "/Applications/Voicebox.app/Contents/MacOS/voicebox-server";
22445
+ if (!existsSync2(serverBin)) {
22446
+ throw new Error(`voice-gen: Voicebox not reachable at ${rawUrl} and VOICEBOX_SERVER_BIN is missing (${serverBin}) \u2014 start Voicebox.app or set VOICEBOX_SERVER_BIN`);
22447
+ }
22448
+ const dataDir = options.dataDir ?? process.env.VOICEBOX_DATA_DIR ?? join(homedir(), "Library/Application Support/sh.voicebox.app");
22449
+ const logPath = options.logPath ?? LOG_PATH;
22450
+ const port = url2.port === "" ? 17493 : Number(url2.port);
22451
+ echoError(`voice-gen: Voicebox not reachable at ${rawUrl} \u2014 auto-starting ${serverBin} (log: ${logPath})`);
22452
+ const child = customSpawn(serverBin, ["--port", String(port), "--data-dir", dataDir], {
22453
+ detached: true,
22454
+ stdio: ["ignore", "ignore", openSync(logPath, "a")]
22455
+ });
22456
+ child.unref?.();
22457
+ const bootWaitMs = options.bootWaitMs ?? BOOT_WAIT_MS;
22458
+ const pollMs = options.pollMs ?? POLL_MS;
22459
+ const deadline = Date.now() + bootWaitMs;
22460
+ while (Date.now() < deadline) {
22461
+ await sleep(pollMs);
22462
+ if (await probeVoiceboxHealth(rawUrl, customFetch)) {
22463
+ echoError(`voice-gen: Voicebox ready at ${rawUrl}`);
22464
+ return;
22465
+ }
22466
+ }
22467
+ throw new Error(`voice-gen: Voicebox did not become healthy at ${rawUrl} within ${bootWaitMs}ms of auto-start \u2014 check ${logPath}`);
22468
+ }
22469
+
22416
22470
  // ../../plugins/generations/voice-gen/src/voicescript.ts
22417
22471
  var VOICEBOX_TEXT_MAX = 50000;
22418
22472
  var VOICEBOX_INSTRUCT_MAX = 500;
@@ -22821,8 +22875,8 @@ async function processGeneratorIO(inputPath, outputPath, clientOverride, transco
22821
22875
  const fs = createNodeFileSystem();
22822
22876
  const outDir = dirname2(outputPath);
22823
22877
  const outStem = basename(outputPath, extname(outputPath));
22824
- const audioPath = resolve(join(outDir, `${outStem}.wav`));
22825
- const mp3Path = resolve(join(outDir, `${outStem}.mp3`));
22878
+ const audioPath = resolve(join2(outDir, `${outStem}.wav`));
22879
+ const mp3Path = resolve(join2(outDir, `${outStem}.mp3`));
22826
22880
  const wantMp3 = isMp3Requested();
22827
22881
  await fs.deleteFile(outputPath);
22828
22882
  await fs.deleteFile(audioPath);
@@ -22840,6 +22894,9 @@ async function processGeneratorIO(inputPath, outputPath, clientOverride, transco
22840
22894
  return;
22841
22895
  }
22842
22896
  const client = clientOverride ?? createVoiceboxClient();
22897
+ if (!clientOverride) {
22898
+ await ensureVoiceboxRunning();
22899
+ }
22843
22900
  try {
22844
22901
  await client.health();
22845
22902
  const envDefaultProfile = process.env.VOICEBOX_DEFAULT_PROFILE;
@@ -7,6 +7,7 @@ import { applySpeed, concatWavs } from './concat';
7
7
  import { isMp3Requested, type Mp3Transcoder, transcodeWavToMp3 } from './mp3';
8
8
  import { auditVoiceSegments, detectLoudnessDip, TRANSCRIPTION_FIDELITY_FLOOR, transcriptionFidelity } from './qc';
9
9
  import { createVoiceboxClient, type VoiceboxClient, type VoiceboxGenerateBody } from './voicebox-client';
10
+ import { ensureVoiceboxRunning } from './voicebox-server';
10
11
  import {
11
12
  mergeVoiceScripts,
12
13
  parseDocToVoiceScript,
@@ -71,6 +72,13 @@ export async function processGeneratorIO(
71
72
 
72
73
  const client = clientOverride ?? createVoiceboxClient();
73
74
 
75
+ // Auto-start the local server when it is down — only on the default client path;
76
+ // tests inject clientOverride and skip the spawn entirely. With vdriver=ominivoice
77
+ // (the default) this plugin never runs, so no server is needed at all.
78
+ if (!clientOverride) {
79
+ await ensureVoiceboxRunning();
80
+ }
81
+
74
82
  try {
75
83
  // 1. Health check
76
84
  await client.health();
@@ -0,0 +1,91 @@
1
+ import { type ChildProcess, spawn as nodeSpawn } from 'node:child_process';
2
+ import { existsSync, openSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { echoError } from '@gobing-ai/ts-utils';
6
+
7
+ /** Boot wait / poll cadence for a freshly spawned local server (Voicebox cold start ≈ 2-5s). */
8
+ const BOOT_WAIT_MS = 30_000;
9
+ const POLL_MS = 500;
10
+ const LOG_PATH = '/tmp/voicebox-server.log';
11
+
12
+ export interface VoiceboxServerOptions {
13
+ url?: string;
14
+ serverBin?: string;
15
+ dataDir?: string;
16
+ logPath?: string;
17
+ bootWaitMs?: number;
18
+ pollMs?: number;
19
+ fetch?: typeof fetch;
20
+ sleep?: (ms: number) => Promise<void>;
21
+ spawn?: typeof nodeSpawn;
22
+ }
23
+
24
+ async function probeVoiceboxHealth(url: string, customFetch: typeof fetch): Promise<boolean> {
25
+ try {
26
+ return (await customFetch(`${url}/health`)).ok;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Ensure a local Voicebox server is reachable: probe `/health`, and when it is down and the
34
+ * URL points at this machine, spawn the app's server binary detached (stderr appended to the
35
+ * log file, survives plugin exit) and poll until healthy — fail loud otherwise. Remote URLs
36
+ * and a missing binary are configuration errors, not auto-start candidates.
37
+ */
38
+ export async function ensureVoiceboxRunning(options: VoiceboxServerOptions = {}): Promise<void> {
39
+ const rawUrl = options.url ?? process.env.VOICEBOX_URL ?? 'http://127.0.0.1:17493';
40
+ const customFetch = options.fetch ?? fetch;
41
+ const sleep = options.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
42
+ const customSpawn = options.spawn ?? nodeSpawn;
43
+
44
+ if (await probeVoiceboxHealth(rawUrl, customFetch)) {
45
+ return;
46
+ }
47
+
48
+ const url = new URL(rawUrl);
49
+ const local = ['127.0.0.1', 'localhost', '::1'].includes(url.hostname);
50
+ if (!local) {
51
+ throw new Error(
52
+ `voice-gen: Voicebox not reachable at ${rawUrl} — auto-start only applies to local URLs; is the remote server up? (fix VOICEBOX_URL)`,
53
+ );
54
+ }
55
+ const serverBin =
56
+ options.serverBin ??
57
+ process.env.VOICEBOX_SERVER_BIN ??
58
+ '/Applications/Voicebox.app/Contents/MacOS/voicebox-server';
59
+ if (!existsSync(serverBin)) {
60
+ throw new Error(
61
+ `voice-gen: Voicebox not reachable at ${rawUrl} and VOICEBOX_SERVER_BIN is missing (${serverBin}) — start Voicebox.app or set VOICEBOX_SERVER_BIN`,
62
+ );
63
+ }
64
+ const dataDir =
65
+ options.dataDir ??
66
+ process.env.VOICEBOX_DATA_DIR ??
67
+ join(homedir(), 'Library/Application Support/sh.voicebox.app');
68
+ const logPath = options.logPath ?? LOG_PATH;
69
+ const port = url.port === '' ? 17493 : Number(url.port);
70
+
71
+ echoError(`voice-gen: Voicebox not reachable at ${rawUrl} — auto-starting ${serverBin} (log: ${logPath})`);
72
+ const child: ChildProcess = customSpawn(serverBin, ['--port', String(port), '--data-dir', dataDir], {
73
+ detached: true,
74
+ stdio: ['ignore', 'ignore', openSync(logPath, 'a')],
75
+ });
76
+ child.unref?.();
77
+
78
+ const bootWaitMs = options.bootWaitMs ?? BOOT_WAIT_MS;
79
+ const pollMs = options.pollMs ?? POLL_MS;
80
+ const deadline = Date.now() + bootWaitMs;
81
+ while (Date.now() < deadline) {
82
+ await sleep(pollMs);
83
+ if (await probeVoiceboxHealth(rawUrl, customFetch)) {
84
+ echoError(`voice-gen: Voicebox ready at ${rawUrl}`);
85
+ return;
86
+ }
87
+ }
88
+ throw new Error(
89
+ `voice-gen: Voicebox did not become healthy at ${rawUrl} within ${bootWaitMs}ms of auto-start — check ${logPath}`,
90
+ );
91
+ }
@@ -246,15 +246,19 @@ spur workflow run "$dest" --vars \
246
246
 
247
247
  ### Profile `kk-daily-ai-voice`
248
248
 
249
- Bind vars: `work_dir`, `publish_enabled`, `last30days_enabled`, `skip_review`, `plugins_path`
250
- (empty → ADR-012 default discovery), `agent`. `$dest` resolves like solo-podcast. The run is
251
- topicless — no `topic`/`input_file`/`fixture` vars. `run_date` is computed by the `prepare`
252
- state and written to `$work_dir/run-date.txt`. The `review` state pauses the run for operator
253
- approval of the VoiceScript; resume with `spur workflow continue [run-id]`.
249
+ Bind vars: `work_dir`, `run_date`, `publish_enabled`, `last30days_enabled`, `skip_review`,
250
+ `plugins_path` (empty → ADR-012 default discovery), `agent`. `$dest` resolves like solo-podcast.
251
+ The run is topicless — no `topic`/`input_file`/`fixture` vars. `run_date` is optional: empty →
252
+ today (manual path), or a dashed/digits date for backfills (headless cron path). `daily-prepare`
253
+ derives the digits-only stamp from it and composes `work_dir` when empty:
254
+ `$works_dir/kk-daily-ai-voice/<yyyymmdd>`; an explicit `work_dir` wins. Either way the effective
255
+ workspace lands in the anchor `.spur/runs/kk-daily-ai-voice/work-dir.txt` and the stamp in
256
+ `$work_dir/run-date.txt`. The `review` state pauses the run for operator approval of the
257
+ VoiceScript; resume with `spur workflow continue [run-id]`.
254
258
 
255
259
  ```bash
256
260
  spur workflow run "$dest" --vars \
257
- "{\"work_dir\":\"$work_dir\",\"publish_enabled\":\"$PUBLISH\",\"last30days_enabled\":\"$L30D\",\"skip_review\":\"$SKIP_REVIEW\",\"plugins_path\":\"\",\"agent\":\"$AGENT\"}"
261
+ "{\"work_dir\":\"$work_dir\",\"run_date\":\"\",\"publish_enabled\":\"$PUBLISH\",\"last30days_enabled\":\"$L30D\",\"skip_review\":\"$SKIP_REVIEW\",\"plugins_path\":\"\",\"agent\":\"$AGENT\"}"
258
262
  ```
259
263
 
260
264
  ## 6. Report
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "kk",
3
- "version": "0.0.18",
3
+ "version": "0.0.19",
4
4
  "description": "knowledge-kit Claude Code plugin: skills, commands, subagents, hooks, and rules"
5
5
  }
@@ -23,11 +23,13 @@
23
23
  * The YAML prelude resolves this file as `$pkg/plugins/kk/scripts/kk-workflow-stages.ts` where `pkg`
24
24
  * comes from `realpath "$(command -v kk)"` (the installed package root), and falls back to the
25
25
  * cwd-relative `plugins/kk/scripts/kk-workflow-stages.ts` when that copy is absent — the checkout
26
- * tier kk-storm-research's render has always relied on, preserved on purpose.
26
+ * tier kk-storm-research's render has always relied on, preserved on purpose. The daily-voice
27
+ * workflow reaches the same script through `kk stage` (apps/cli/src/stage.ts), which applies the
28
+ * same two resolution tiers in TypeScript.
27
29
  *
28
- * SYNC: plugins/kk/workflows/{kk-solo-podcast,kk-itc,kk-storm-research,kk-daily-ai-voice}.yaml shell
29
- * this script. Keep message substrings stable — apps/cli/tests/kk-workflow-stages.test.ts and the
30
- * four workflow static tests match them.
30
+ * SYNC: plugins/kk/workflows/{kk-solo-podcast,kk-itc,kk-storm-research}.yaml shell this script
31
+ * (kk-daily-ai-voice goes through `kk stage`). Keep message substrings stable —
32
+ * apps/cli/tests/kk-workflow-stages.test.ts and the four workflow static tests match them.
31
33
  */
32
34
  import { spawnSync } from 'node:child_process';
33
35
  import { createHash } from 'node:crypto';
@@ -43,6 +45,7 @@ import {
43
45
  unlinkSync,
44
46
  writeFileSync,
45
47
  } from 'node:fs';
48
+ import { homedir } from 'node:os';
46
49
  import { basename, dirname, join, resolve } from 'node:path';
47
50
 
48
51
  const SCRIPTS_DIR = import.meta.dir;
@@ -534,15 +537,29 @@ function dailyPrepare(args: string[]): void {
534
537
  last30daysEnabled = '',
535
538
  topic = '',
536
539
  horizonHours = '',
540
+ runDateArg = '',
537
541
  ] = args;
538
- const workDir = resolve(workDirArg);
539
- timed(workDirArg, 'prepare', () => {
540
- if (!workDirArg) failWith(1, 'missing required variable: work_dir');
542
+ // Empty work_dir composes the real workspace from the operator's works_dir
543
+ // (workflow-run.md §2 precedence: KK_WORKS_DIR > config works_dir > ~/.config/kk/works):
544
+ // $works_dir/kk-daily-ai-voice/<stamp>
545
+ // The stamp is digits-only (dashes stripped from the caller's run_date, else today) so a
546
+ // resumed/retried run whose persisted run_date var already carries the digits-only
547
+ // run-date.txt value re-derives the same directory.
548
+ const dirDate = runDateArg ? runDateArg.replaceAll('-', '') : formatRunDate();
549
+ const workDir = resolve(workDirArg || join(resolveWorksDir(), 'kk-daily-ai-voice', dirDate));
550
+ timed(workDir, 'prepare', () => {
541
551
  for (const dir of DAILY_DIRS)
542
552
  sted('mkdir', join(workDir, dir), () => mkdirSync(join(workDir, dir), { recursive: true }));
543
553
  sted('write', join(workDir, 'run-date.txt'), () =>
544
- writeFileSync(join(workDir, 'run-date.txt'), `${formatRunDate()}\n`),
554
+ writeFileSync(join(workDir, 'run-date.txt'), `${dirDate}\n`),
545
555
  );
556
+ // Anchor for the workflow: shell actions cannot set vars, so the YAML reads the
557
+ // effective work_dir back into vars.work_dir from this cwd-relative file.
558
+ const anchorDir = join('.spur', 'runs', 'kk-daily-ai-voice');
559
+ sted('write', join(anchorDir, 'work-dir.txt'), () => {
560
+ mkdirSync(anchorDir, { recursive: true });
561
+ writeFileSync(join(anchorDir, 'work-dir.txt'), `${workDir}\n`);
562
+ });
546
563
  sted('write', join(workDir, '1-ingest/source.json'), () =>
547
564
  writeJsonText(join(workDir, '1-ingest/source.json'), {
548
565
  limit: Number(limit),
@@ -572,6 +589,35 @@ function formatRunDate(): string {
572
589
  return `${now.getFullYear()}${month}${day}`;
573
590
  }
574
591
 
592
+ /** Expand the `~/` and `$HOME/` prefixes the config examples use. */
593
+ function expandHome(p: string): string {
594
+ if (p.startsWith('~/')) return join(homedir(), p.slice(2));
595
+ if (p.startsWith('$HOME/')) return join(homedir(), p.slice('$HOME/'.length));
596
+ return p;
597
+ }
598
+
599
+ /**
600
+ * works_dir for composed daily workspaces — workflow-run.md §2 precedence:
601
+ * KK_WORKS_DIR env > config works_dir > compiled default ~/.config/kk/works.
602
+ * Unreadable/unparseable config falls through to the default (pluginEnvFromConfig
603
+ * tolerates the same), never fails the run.
604
+ */
605
+ function resolveWorksDir(): string {
606
+ const fromEnv = process.env.KK_WORKS_DIR;
607
+ if (fromEnv) return fromEnv;
608
+ const configPath = process.env.KK_CONFIG || join(homedir(), '.config/kk/config.yaml');
609
+ if (existsSync(configPath)) {
610
+ try {
611
+ const parsed = Bun.YAML.parse(readFileSync(configPath, 'utf-8')) as Record<string, unknown> | null;
612
+ const worksDir = parsed !== null && typeof parsed === 'object' ? parsed.works_dir : undefined;
613
+ if (typeof worksDir === 'string' && worksDir !== '') return expandHome(worksDir);
614
+ } catch {
615
+ // fall through to the compiled default
616
+ }
617
+ }
618
+ return join(homedir(), '.config/kk/works');
619
+ }
620
+
575
621
  function dailyCollectFacts(args: string[]): void {
576
622
  const [workDir = '', runDate = '', pluginsPath = ''] = args;
577
623
  const content = join(workDir, '2-facts', `${runDate}_02_collect_content.json`);
@@ -780,7 +826,9 @@ function dailyArticle(args: string[]): void {
780
826
  {
781
827
  env: {
782
828
  ARTICLE_DATE: runDate,
783
- ...(isFile(proseBody) ? { ARTICLE_BODY_FILE: proseBody } : {}),
829
+ // Plugin entries run with cwd = plugin dir (invoke.ts), so relative
830
+ // paths in env must be resolved or the plugin sees ENOENT.
831
+ ...(isFile(proseBody) ? { ARTICLE_BODY_FILE: resolve(proseBody) } : {}),
784
832
  },
785
833
  },
786
834
  );
@@ -35,6 +35,9 @@ vars:
35
35
  plan_translate_enabled: "true"
36
36
  article_write_enabled: "true"
37
37
  plugins_path: ""
38
+ # Projected by the publish state from 3-publish/<run_date>_17_publish_publish-status.txt;
39
+ # guards test this var instead of cat-ing the file (state at decision time).
40
+ publish_status: ""
38
41
  last30days_topic: "AI news"
39
42
  horizon_hours: "24"
40
43
  publish_enabled: "false"
@@ -62,13 +65,18 @@ states:
62
65
  onEnter:
63
66
  - kind: shell
64
67
  options:
68
+ # run_date (dashed or digits; the job worker expands $(date +%F) before spur sees the
69
+ # JSON — var values reach non-shell steps verbatim) only names the workspace:
70
+ # daily-prepare composes the real dir as works_dir/kk-daily-ai-voice/<stamp> when
71
+ # work_dir is empty and always writes the effective work_dir to the cwd-relative
72
+ # anchor below. Shell actions cannot set vars, so the reads project it (then the
73
+ # digits-only stamp) into vars before any later state interpolates them.
65
74
  command: >-
66
- kbin="$(command -v kk 2>/dev/null)";
67
- pkg="$(dirname "$(dirname "$(realpath "$kbin" 2>/dev/null)")")";
68
- st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
69
- [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
70
- [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
71
- bun "$st" daily-prepare "${vars.work_dir}" "${vars.limit}" "${vars.cursor}" "${vars.state_file}" "${vars.last30days_enabled}" "${vars.last30days_topic}" "${vars.horizon_hours}"
75
+ kk stage daily-prepare "${vars.work_dir}" "${vars.limit}" "${vars.cursor}" "${vars.state_file}" "${vars.last30days_enabled}" "${vars.last30days_topic}" "${vars.horizon_hours}" "${vars.run_date}"
76
+ - kind: file.read.into-var
77
+ options:
78
+ path: .spur/runs/kk-daily-ai-voice/work-dir.txt
79
+ var: work_dir
72
80
  - kind: file.read.into-var
73
81
  options:
74
82
  path: ${vars.work_dir}/run-date.txt
@@ -89,12 +97,7 @@ states:
89
97
  - kind: shell
90
98
  options:
91
99
  command: >-
92
- kbin="$(command -v kk 2>/dev/null)";
93
- pkg="$(dirname "$(dirname "$(realpath "$kbin" 2>/dev/null)")")";
94
- st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
95
- [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
96
- [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
97
- bun "$st" daily-collect-facts "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
100
+ kk stage daily-collect-facts "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
98
101
 
99
102
  - id: plan
100
103
  description: "Score/dedup/select + annotate the blended Doc[] into the episode plan (episode-plan-gen; annotate rides inside planEpisode)"
@@ -102,12 +105,7 @@ states:
102
105
  - kind: shell
103
106
  options:
104
107
  command: >-
105
- kbin="$(command -v kk 2>/dev/null)";
106
- pkg="$(dirname "$(dirname "$(realpath "$kbin" 2>/dev/null)")")";
107
- st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
108
- [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
109
- [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
110
- bun "$st" daily-plan "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.plan_max_items}"
108
+ kk stage daily-plan "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.plan_max_items}"
111
109
 
112
110
  - id: quality-control-content
113
111
  description: "Content QC: filter the plan's annotated candidates by category allowlist + score thresholds (episode-plan-gen --mode filter); zero candidates route to done-no-content"
@@ -115,12 +113,7 @@ states:
115
113
  - kind: shell
116
114
  options:
117
115
  command: >-
118
- kbin="$(command -v kk 2>/dev/null)";
119
- pkg="$(dirname "$(dirname "$(realpath "$kbin" 2>/dev/null)")")";
120
- st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
121
- [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
122
- [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
123
- bun "$st" daily-qc-content "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.qc_categories}" "${vars.qc_min_quality}" "${vars.qc_min_importance}" "${vars.qc_min_urgency}" "${vars.qc_min_impact}"
116
+ kk stage daily-qc-content "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.qc_categories}" "${vars.qc_min_quality}" "${vars.qc_min_importance}" "${vars.qc_min_urgency}" "${vars.qc_min_impact}"
124
117
 
125
118
  - id: plan-translate
126
119
  description: "Translate needsTranslation plan items into the output plan.json via agent.run"
@@ -162,12 +155,7 @@ states:
162
155
  - kind: shell
163
156
  options:
164
157
  command: >-
165
- kbin="$(command -v kk 2>/dev/null)";
166
- pkg="$(dirname "$(dirname "$(realpath "$kbin" 2>/dev/null)")")";
167
- st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
168
- [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
169
- [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
170
- bun "$st" daily-article "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
158
+ kk stage daily-article "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
171
159
 
172
160
  - id: cover
173
161
  description: "Generate the article cover image via the kk:image-authoring skill (agent.run)"
@@ -186,12 +174,7 @@ states:
186
174
  - kind: shell
187
175
  options:
188
176
  command: >-
189
- kbin="$(command -v kk 2>/dev/null)";
190
- pkg="$(dirname "$(dirname "$(realpath "$kbin" 2>/dev/null)")")";
191
- st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
192
- [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
193
- [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
194
- bun "$st" daily-cover-normalize "${vars.work_dir}" "${vars.run_date}"
177
+ kk stage daily-cover-normalize "${vars.work_dir}" "${vars.run_date}"
195
178
 
196
179
  - id: script
197
180
  description: "Compile the episode plan Doc[] into structured broadcast VoiceScript YAML Content"
@@ -199,12 +182,7 @@ states:
199
182
  - kind: shell
200
183
  options:
201
184
  command: >-
202
- kbin="$(command -v kk 2>/dev/null)";
203
- pkg="$(dirname "$(dirname "$(realpath "$kbin" 2>/dev/null)")")";
204
- st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
205
- [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
206
- [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
207
- bun "$st" daily-script "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.language}" "${vars.title}" "${vars.voice_profile}"
185
+ kk stage daily-script "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.language}" "${vars.title}" "${vars.voice_profile}"
208
186
 
209
187
  - id: review
210
188
  description: "Operator reviews the generated news script before speech synthesis"
@@ -221,12 +199,7 @@ states:
221
199
  - kind: shell
222
200
  options:
223
201
  command: >-
224
- kbin="$(command -v kk 2>/dev/null)";
225
- pkg="$(dirname "$(dirname "$(realpath "$kbin" 2>/dev/null)")")";
226
- st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
227
- [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
228
- [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
229
- bun "$st" daily-wrap-docs "${vars.work_dir}" "${vars.run_date}" "${vars.voice_profile}"
202
+ kk stage daily-wrap-docs "${vars.work_dir}" "${vars.run_date}" "${vars.voice_profile}"
230
203
 
231
204
  - id: generate
232
205
  description: "Invoke voice driver plugin (vdriver var: ominivoice -> omni-voice-gen, voicebox -> voice-gen) to generate audio"
@@ -234,12 +207,7 @@ states:
234
207
  - kind: shell
235
208
  options:
236
209
  command: >-
237
- kbin="$(command -v kk 2>/dev/null)";
238
- pkg="$(dirname "$(dirname "$(realpath "$kbin" 2>/dev/null)")")";
239
- st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
240
- [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
241
- [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
242
- OMNIVOICE_SPEED=1.06 bun "$st" daily-generate "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.transcode_mp3}" "${vars.vdriver}"
210
+ OMNIVOICE_SPEED=1.06 kk stage daily-generate "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.transcode_mp3}" "${vars.vdriver}"
243
211
 
244
212
  - id: intro-music
245
213
  description: "Prepend the 8s intro sting (intro_music var; empty = skip) to the generated wav, update duration/audioPath"
@@ -247,12 +215,7 @@ states:
247
215
  - kind: shell
248
216
  options:
249
217
  command: >-
250
- kbin="$(command -v kk 2>/dev/null)";
251
- pkg="$(dirname "$(dirname "$(realpath "$kbin" 2>/dev/null)")")";
252
- st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
253
- [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
254
- [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
255
- bun "$st" daily-intro "${vars.work_dir}" "${vars.run_date}" "${vars.intro_music}"
218
+ kk stage daily-intro "${vars.work_dir}" "${vars.run_date}" "${vars.intro_music}"
256
219
 
257
220
  - id: quality-control
258
221
  description: "Verify audio quality, speech rate, and silence/repetition QC metrics"
@@ -260,12 +223,7 @@ states:
260
223
  - kind: shell
261
224
  options:
262
225
  command: >-
263
- kbin="$(command -v kk 2>/dev/null)";
264
- pkg="$(dirname "$(dirname "$(realpath "$kbin" 2>/dev/null)")")";
265
- st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
266
- [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
267
- [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
268
- bun "$st" daily-quality-report "${vars.work_dir}" "${vars.run_date}"
226
+ kk stage daily-quality-report "${vars.work_dir}" "${vars.run_date}"
269
227
 
270
228
  - id: publish-prep
271
229
  description: "Merge article Content with audio (podcast assembly facts ride the content options channel for podcast-pub's absorbed assembly; runDate kept for surfdash slug)"
@@ -273,12 +231,7 @@ states:
273
231
  - kind: shell
274
232
  options:
275
233
  command: >-
276
- kbin="$(command -v kk 2>/dev/null)";
277
- pkg="$(dirname "$(dirname "$(realpath "$kbin" 2>/dev/null)")")";
278
- st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
279
- [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
280
- [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
281
- bun "$st" daily-publish-prep "${vars.work_dir}" "${vars.run_date}"
234
+ kk stage daily-publish-prep "${vars.work_dir}" "${vars.run_date}"
282
235
 
283
236
  - id: publish-surfdash
284
237
  description: "Publish the merged Content to surfdash (single target); result lands in result.json — surfdash publishes first so show-notes can link the article (task 0118 seam reorder)"
@@ -286,12 +239,7 @@ states:
286
239
  - kind: shell
287
240
  options:
288
241
  command: >-
289
- kbin="$(command -v kk 2>/dev/null)";
290
- pkg="$(dirname "$(dirname "$(realpath "$kbin" 2>/dev/null)")")";
291
- st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
292
- [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
293
- [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
294
- bun "$st" daily-publish-surfdash "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
242
+ kk stage daily-publish-surfdash "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
295
243
 
296
244
  - id: show-notes
297
245
  description: "Render timeline show notes from the episode plan (podcast-pub show-notes op, briefMode; article URL from the surfdash result postPath — surfdash failure degrades to legacy full-body layout)"
@@ -299,12 +247,7 @@ states:
299
247
  - kind: shell
300
248
  options:
301
249
  command: >-
302
- kbin="$(command -v kk 2>/dev/null)";
303
- pkg="$(dirname "$(dirname "$(realpath "$kbin" 2>/dev/null)")")";
304
- st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
305
- [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
306
- [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
307
- bun "$st" daily-show-notes "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.title}"
250
+ kk stage daily-show-notes "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.title}"
308
251
 
309
252
  - id: publish-podcast
310
253
  description: "Publish the episode to podcast-pub with the rendered show notes (metadata.showNotes carries the brief/timeline markdown; map.ts falls back to content.body when absent — task 0118 R3)"
@@ -312,12 +255,7 @@ states:
312
255
  - kind: shell
313
256
  options:
314
257
  command: >-
315
- kbin="$(command -v kk 2>/dev/null)";
316
- pkg="$(dirname "$(dirname "$(realpath "$kbin" 2>/dev/null)")")";
317
- st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
318
- [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
319
- [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
320
- bun "$st" daily-publish-podcast "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
258
+ kk stage daily-publish-podcast "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
321
259
 
322
260
  - id: publish
323
261
  description: "Classify the sequenced surfdash → podcast outcomes full|partial|none into publish-status.txt (exit 0 in all three — routing is transition-owned; task 0118: per-target classification over the merged result.json)"
@@ -325,12 +263,11 @@ states:
325
263
  - kind: shell
326
264
  options:
327
265
  command: >-
328
- kbin="$(command -v kk 2>/dev/null)";
329
- pkg="$(dirname "$(dirname "$(realpath "$kbin" 2>/dev/null)")")";
330
- st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
331
- [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
332
- [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
333
- bun "$st" daily-publish-classify "${vars.work_dir}" "${vars.run_date}"
266
+ kk stage daily-publish-classify "${vars.work_dir}" "${vars.run_date}"
267
+ - kind: file.read.into-var
268
+ options:
269
+ path: ${vars.work_dir}/3-publish/${vars.run_date}_17_publish_publish-status.txt
270
+ var: publish_status
334
271
 
335
272
  - id: patch-surfdash
336
273
  description: "20260917 #4/#6 — patch published surfdash posts: podcast cover URL into image/og_image frontmatter + podcast cross-link (zh/en/ja), commit+push"
@@ -338,12 +275,7 @@ states:
338
275
  - kind: shell
339
276
  options:
340
277
  command: >-
341
- kbin="$(command -v kk 2>/dev/null)";
342
- pkg="$(dirname "$(dirname "$(realpath "$kbin" 2>/dev/null)")")";
343
- st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
344
- [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
345
- [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
346
- bun "$st" daily-patch-surfdash "${vars.work_dir}" "${vars.run_date}" "${vars.surfdash_root}"
278
+ kk stage daily-patch-surfdash "${vars.work_dir}" "${vars.run_date}" "${vars.surfdash_root}"
347
279
 
348
280
  - id: publish-partial
349
281
  description: "Partial publish — at least one target published, at least one failed; echo warning, write a .spur/run marker, preserve result.json for --retry-from, continue to done (task 0118: surfdash failure = no-URL fallback notes; podcast failure = its own partial)"
@@ -351,12 +283,7 @@ states:
351
283
  - kind: shell
352
284
  options:
353
285
  command: >-
354
- kbin="$(command -v kk 2>/dev/null)";
355
- pkg="$(dirname "$(dirname "$(realpath "$kbin" 2>/dev/null)")")";
356
- st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
357
- [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
358
- [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
359
- bun "$st" daily-publish-partial "${vars.work_dir}" "${vars.run_date}"
286
+ kk stage daily-publish-partial "${vars.work_dir}" "${vars.run_date}"
360
287
 
361
288
  - id: run-report
362
289
  description: "Assemble the per-run report (scores+aggregates+rejected counts+step timing) from existing artifacts — news-report-gen; output-only, never throws; 0117"
@@ -364,12 +291,7 @@ states:
364
291
  - kind: shell
365
292
  options:
366
293
  command: >-
367
- kbin="$(command -v kk 2>/dev/null)";
368
- pkg="$(dirname "$(dirname "$(realpath "$kbin" 2>/dev/null)")")";
369
- st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
370
- [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
371
- [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
372
- bun "$st" daily-run-report "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.qc_min_quality}" "${vars.qc_min_importance}" "${vars.qc_min_urgency}" "${vars.qc_min_impact}"
294
+ kk stage daily-run-report "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.qc_min_quality}" "${vars.qc_min_importance}" "${vars.qc_min_urgency}" "${vars.qc_min_impact}"
373
295
 
374
296
  - id: translate
375
297
  description: "Locales — surfdash-pub scaffolds en/ja drafts (deterministic, plugin-owned), agent translates both, sync into the surfdash CMS"
@@ -377,12 +299,7 @@ states:
377
299
  - kind: shell
378
300
  options:
379
301
  command: >-
380
- kbin="$(command -v kk 2>/dev/null)";
381
- pkg="$(dirname "$(dirname "$(realpath "$kbin" 2>/dev/null)")")";
382
- st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
383
- [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
384
- [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
385
- bun "$st" daily-translate-localize "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
302
+ kk stage daily-translate-localize "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
386
303
  - kind: file.read.into-var
387
304
  options:
388
305
  path: ${vars.work_dir}/3-publish/${vars.run_date}_20_translate-localize_en-post-path.txt
@@ -405,12 +322,7 @@ states:
405
322
  - kind: shell
406
323
  options:
407
324
  command: >-
408
- kbin="$(command -v kk 2>/dev/null)";
409
- pkg="$(dirname "$(dirname "$(realpath "$kbin" 2>/dev/null)")")";
410
- st="$pkg/plugins/kk/scripts/kk-workflow-stages.ts";
411
- [ -f "$st" ] || st="plugins/kk/scripts/kk-workflow-stages.ts";
412
- [ -f "$st" ] || echo "kk-workflow-stages.ts not found at $st — install or refresh the kk package (bun link in a checkout)" >&2;
413
- bun "$st" daily-translate-sync "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.work_dir}/3-publish/${vars.run_date}_20_translate-localize_zh-post-path.txt"
325
+ kk stage daily-translate-sync "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.work_dir}/3-publish/${vars.run_date}_20_translate-localize_zh-post-path.txt"
414
326
 
415
327
  - id: done
416
328
  description: "Terminal — Daily AI news pipeline completed (collect → QC'd plan → article/cover/script/voice → publish/locales)"
@@ -623,7 +535,7 @@ transitions:
623
535
  guard:
624
536
  kind: shell
625
537
  options:
626
- command: 'test "$(cat "${vars.work_dir}/3-publish/${vars.run_date}_17_publish_publish-status.txt")" = "partial"'
538
+ command: 'test "${vars.publish_status}" = "partial"'
627
539
 
628
540
  - from: publish
629
541
  to: failed
@@ -631,7 +543,7 @@ transitions:
631
543
  guard:
632
544
  kind: shell
633
545
  options:
634
- command: 'test "$(cat "${vars.work_dir}/3-publish/${vars.run_date}_17_publish_publish-status.txt")" = "none"'
546
+ command: 'test "${vars.publish_status}" = "none"'
635
547
 
636
548
  - from: publish-partial
637
549
  to: run-report
@@ -645,7 +557,7 @@ transitions:
645
557
  guard:
646
558
  kind: shell
647
559
  options:
648
- command: 'test "$(cat "${vars.work_dir}/3-publish/${vars.run_date}_17_publish_publish-status.txt")" = "full"'
560
+ command: 'test "${vars.publish_status}" = "full"'
649
561
 
650
562
  - from: run-report
651
563
  to: done
@@ -653,7 +565,7 @@ transitions:
653
565
  guard:
654
566
  kind: shell
655
567
  options:
656
- command: 'test "$(cat "${vars.work_dir}/3-publish/${vars.run_date}_17_publish_publish-status.txt")" = "partial"'
568
+ command: 'test "${vars.publish_status}" = "partial"'
657
569
 
658
570
  - from: translate
659
571
  to: done