@gobing-ai/knowledge-kit 0.0.34 → 0.0.36
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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +1 -1
- package/dist/board/collect/index.css +42 -0
- package/dist/board/collect/index.js +787 -0
- package/dist/board/daily-voice/index.css +13 -0
- package/dist/board/daily-voice/index.js +902 -0
- package/dist/board/itc/index.css +14 -0
- package/dist/board/itc/index.js +1026 -0
- package/dist/index.js +11769 -9250
- package/package.json +10 -9
- package/plugins/generations/content-gen/dist/index.js +1015 -10814
- package/plugins/generations/content-gen/package.json +1 -1
- package/plugins/generations/core-facts-gen/dist/index.js +1002 -10801
- package/plugins/generations/core-facts-gen/package.json +1 -1
- package/plugins/generations/daily-article-gen/dist/index.js +1019 -10804
- package/plugins/generations/daily-article-gen/package.json +1 -1
- package/plugins/generations/daily-article-gen/src/index.ts +32 -3
- package/plugins/generations/dailynews-gen/dist/index.js +1094 -10892
- package/plugins/generations/dailynews-gen/package.json +1 -1
- package/plugins/generations/dailynews-gen/src/script-builder.ts +6 -1
- package/plugins/generations/episode-plan-gen/dist/index.js +1124 -10923
- package/plugins/generations/episode-plan-gen/package.json +1 -1
- package/plugins/generations/gemini-tts/dist/index.js +1108 -10906
- package/plugins/generations/gemini-tts/package.json +1 -1
- package/plugins/generations/image-gen/dist/index.js +1239 -11035
- package/plugins/generations/image-gen/package.json +1 -1
- package/plugins/generations/news-report-gen/dist/index.js +1048 -10847
- package/plugins/generations/news-report-gen/package.json +1 -1
- package/plugins/generations/omni-voice-gen/plugin.json +1 -1
- package/plugins/generations/voice-gen/dist/index.js +1066 -10864
- package/plugins/generations/voice-gen/package.json +1 -1
- package/plugins/generations/voice-gen/src/index.ts +9 -3
- package/plugins/generations/voxcpm-tts/README.md +72 -0
- package/plugins/generations/voxcpm-tts/dist/index.js +14006 -0
- package/plugins/generations/voxcpm-tts/package.json +16 -0
- package/plugins/generations/voxcpm-tts/plugin.json +6 -0
- package/plugins/generations/voxcpm-tts/profiles.json +10 -0
- package/plugins/generations/voxcpm-tts/pyproject.toml +9 -0
- package/plugins/generations/voxcpm-tts/python/worker.py +353 -0
- package/plugins/generations/voxcpm-tts/src/index.ts +42 -0
- package/plugins/generations/voxcpm-tts/src/voxcpm-client.ts +765 -0
- package/plugins/generations/voxcpm-tts/tsconfig.json +8 -0
- package/plugins/generations/voxcpm-tts/uv.lock +2571 -0
- package/plugins/ingestions/aihot-ingest/dist/index.js +1081 -10628
- package/plugins/ingestions/aihot-ingest/package.json +1 -1
- package/plugins/ingestions/horizon-ingest/dist/index.js +1013 -10812
- package/plugins/ingestions/horizon-ingest/package.json +1 -1
- package/plugins/ingestions/karakeep-local/dist/index.js +689 -10774
- package/plugins/ingestions/karakeep-local/package.json +1 -1
- package/plugins/ingestions/last30days-ingest/dist/index.js +1011 -10810
- package/plugins/ingestions/last30days-ingest/package.json +1 -1
- package/plugins/ingestions/web-search/dist/index.js +1250 -11033
- package/plugins/ingestions/web-search/package.json +1 -1
- package/plugins/kk/README.md +29 -2
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/schedules/cron.tmpl +9 -1
- package/plugins/kk/schedules/kk-serve.launchd.plist.tmpl +79 -0
- package/plugins/kk/schedules/launchd.plist.tmpl +11 -1
- package/plugins/kk/scripts/daily-stages.ts +702 -63
- package/plugins/kk/scripts/kk-workflow-stages.ts +116 -146
- package/plugins/kk/skills/content-safety/references/platforms/wechatmp.md +2 -2
- package/plugins/kk/skills/content-safety/references/platforms/xiaoyuzhou.md +4 -2
- package/plugins/kk/skills/publish/SKILL.md +8 -4
- package/plugins/kk/workflows/feature-verification.yaml +93 -0
- package/plugins/kk/workflows/judge-gated-publish-example.yaml +1 -0
- package/plugins/kk/workflows/kk-collect.yaml +43 -11
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +36 -9
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +92 -18
- package/plugins/kk/workflows/kk-daily-safety.yaml +16 -3
- package/plugins/kk/workflows/kk-itc.yaml +8 -1
- package/plugins/kk/workflows/kk-solo-podcast.yaml +2 -0
- package/plugins/publishings/emdash-pub/dist/index.js +987 -10814
- package/plugins/publishings/emdash-pub/package.json +1 -1
- package/plugins/publishings/infoq-pub/dist/index.js +1928 -11444
- package/plugins/publishings/infoq-pub/dist/profile-check.js +20814 -0
- package/plugins/publishings/infoq-pub/package.json +1 -1
- package/plugins/publishings/infoq-pub/plugin.json +6 -2
- package/plugins/publishings/juejin-pub/dist/index.js +1940 -11456
- package/plugins/publishings/juejin-pub/dist/profile-check.js +20800 -0
- package/plugins/publishings/juejin-pub/package.json +1 -1
- package/plugins/publishings/juejin-pub/plugin.json +6 -2
- package/plugins/publishings/medium-pub/dist/index.js +978 -10805
- package/plugins/publishings/medium-pub/package.json +1 -1
- package/plugins/publishings/podcast-pub/dist/index.js +1016 -10843
- package/plugins/publishings/podcast-pub/package.json +1 -1
- package/plugins/publishings/qiita-pub/dist/index.js +976 -10803
- package/plugins/publishings/qiita-pub/package.json +1 -1
- package/plugins/publishings/substack-pub/dist/index.js +1932 -11448
- package/plugins/publishings/substack-pub/dist/profile-check.js +20890 -0
- package/plugins/publishings/substack-pub/package.json +1 -1
- package/plugins/publishings/substack-pub/plugin.json +5 -2
- package/plugins/publishings/substack-pub/src/profile-check.ts +36 -3
- package/plugins/publishings/surfdash-pub/dist/index.js +1151 -10834
- package/plugins/publishings/surfdash-pub/package.json +1 -1
- package/plugins/publishings/surfdash-pub/src/index.ts +137 -15
- package/plugins/publishings/surfdash-pub/src/patch.ts +144 -0
- package/plugins/publishings/threads-pub/dist/index.js +12394 -0
- package/plugins/publishings/threads-pub/package.json +17 -0
- package/plugins/publishings/threads-pub/plugin.json +7 -0
- package/plugins/publishings/threads-pub/src/index.ts +340 -0
- package/plugins/publishings/threads-pub/tsconfig.json +4 -0
- package/plugins/publishings/wechatmp-pub/dist/index.js +2638 -11650
- package/plugins/publishings/wechatmp-pub/dist/profile-check.js +20739 -0
- package/plugins/publishings/wechatmp-pub/package.json +1 -1
- package/plugins/publishings/wechatmp-pub/plugin.json +6 -2
- package/plugins/publishings/wechatmp-pub/src/enrich.ts +297 -163
- package/plugins/publishings/wechatmp-pub/src/flow.ts +611 -54
- package/plugins/publishings/wechatmp-pub/src/index.ts +24 -3
- package/plugins/publishings/wechatmp-pub/src/page.ts +36 -0
- package/plugins/publishings/wechatmp-pub/src/profile-check.ts +40 -4
- package/plugins/publishings/weibo-pub/dist/index.js +21345 -0
- package/plugins/publishings/weibo-pub/dist/profile-check.js +20817 -0
- package/plugins/publishings/weibo-pub/package.json +19 -0
- package/plugins/publishings/weibo-pub/plugin.json +12 -0
- package/plugins/publishings/weibo-pub/src/index.ts +868 -0
- package/plugins/publishings/weibo-pub/src/profile-check.ts +20 -0
- package/plugins/publishings/weibo-pub/tsconfig.json +4 -0
- package/plugins/publishings/x-pub/dist/index.js +1948 -11440
- package/plugins/publishings/x-pub/dist/profile-check.js +20806 -0
- package/plugins/publishings/x-pub/package.json +1 -1
- package/plugins/publishings/x-pub/plugin.json +6 -2
- package/plugins/publishings/xhs-pub/dist/index.js +1993 -11509
- package/plugins/publishings/xhs-pub/dist/profile-check.js +21023 -0
- package/plugins/publishings/xhs-pub/package.json +1 -1
- package/plugins/publishings/xhs-pub/plugin.json +6 -2
- package/plugins/publishings/xhs-pub/src/profile-check.ts +72 -2
- package/plugins/publishings/zenn-pub/dist/index.js +988 -10815
- package/plugins/publishings/zenn-pub/package.json +1 -1
- package/plugins/generations/omni-voice-gen/dist/omni-voice-gen-prr8skpb. +0 -2
- package/plugins/generations/omni-voice-gen/dist/omni-voice-gen.js +0 -6
|
@@ -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,
|
|
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';
|
|
@@ -569,6 +569,30 @@ export function kk(
|
|
|
569
569
|
return status;
|
|
570
570
|
}
|
|
571
571
|
|
|
572
|
+
/**
|
|
573
|
+
* `kk()` sibling with `stdio: 'pipe'`: captures the child's stdout/stderr, reprints both to the
|
|
574
|
+
* parent so the operator log is unchanged, and returns the exit status plus the captured stderr.
|
|
575
|
+
* Only stages that branch on the child's stderr use it (0251: the xhs-render overflow retry);
|
|
576
|
+
* every other call keeps `kk()`'s inherit default.
|
|
577
|
+
*/
|
|
578
|
+
export function kkCaptured(
|
|
579
|
+
args: string[],
|
|
580
|
+
options: { env?: Record<string, string>; unset?: readonly string[]; soft?: boolean } = {},
|
|
581
|
+
): { status: number; stderr: string } {
|
|
582
|
+
const env = { ...process.env, ...(options.env ?? {}) };
|
|
583
|
+
for (const name of options.unset ?? []) delete env[name];
|
|
584
|
+
const runner = spawnSync('kk', args, { stdio: 'pipe', encoding: 'utf-8', env });
|
|
585
|
+
if (runner.error !== undefined) {
|
|
586
|
+
console.error(`kk: ${runner.error.message}`);
|
|
587
|
+
failWith(127);
|
|
588
|
+
}
|
|
589
|
+
process.stdout.write(runner.stdout ?? '');
|
|
590
|
+
process.stderr.write(runner.stderr ?? '');
|
|
591
|
+
const status = runner.status ?? 1;
|
|
592
|
+
if (status !== 0 && options.soft !== true) failWith(status);
|
|
593
|
+
return { status, stderr: runner.stderr ?? '' };
|
|
594
|
+
}
|
|
595
|
+
|
|
572
596
|
export function writeJsonText(path: string, value: unknown): void {
|
|
573
597
|
// `Bun.write(path, JSON.stringify(value, null, 2))` — no trailing newline.
|
|
574
598
|
writeFileSync(path, JSON.stringify(value, null, 2));
|
|
@@ -1147,6 +1171,22 @@ export function firstPostPath(parsed: Record<string, unknown> | null, target: st
|
|
|
1147
1171
|
return '';
|
|
1148
1172
|
}
|
|
1149
1173
|
|
|
1174
|
+
/**
|
|
1175
|
+
* The public URL a target published to, or '' — the sibling of {@link firstPostPath} for consumers
|
|
1176
|
+
* that need the reader-facing address (WeChat's 原文链接 points at the surfdash post).
|
|
1177
|
+
*/
|
|
1178
|
+
export function firstTargetUrl(parsed: Record<string, unknown> | null, target: string): string {
|
|
1179
|
+
const targets = parsed?.targets;
|
|
1180
|
+
if (!Array.isArray(targets)) return '';
|
|
1181
|
+
for (const entry of targets) {
|
|
1182
|
+
if (entry === null || typeof entry !== 'object') continue;
|
|
1183
|
+
const record = entry as Record<string, unknown>;
|
|
1184
|
+
if (record.target !== target || record.ok !== true) continue;
|
|
1185
|
+
if (typeof record.url === 'string' && record.url !== '') return record.url;
|
|
1186
|
+
}
|
|
1187
|
+
return '';
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1150
1190
|
function dailyPublishClassify(args: string[]): void {
|
|
1151
1191
|
const [workDir = '', runDate = ''] = args;
|
|
1152
1192
|
const result = join(workDir, '31-publish', `${runDate}_17_publish_result.json`);
|
|
@@ -1240,10 +1280,24 @@ function publishCompleteness(
|
|
|
1240
1280
|
detail: 'en/ja scaffolded, never synced',
|
|
1241
1281
|
});
|
|
1242
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
|
+
});
|
|
1243
1294
|
rows.push({
|
|
1244
1295
|
channel: 'locales',
|
|
1245
|
-
state: 'done',
|
|
1246
|
-
detail:
|
|
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`,
|
|
1247
1301
|
});
|
|
1248
1302
|
} else {
|
|
1249
1303
|
rows.push({
|
|
@@ -1287,6 +1341,12 @@ function publishCompleteness(
|
|
|
1287
1341
|
function dailyPublishPartial(args: string[]): void {
|
|
1288
1342
|
const [workDir = '', runDate = ''] = args;
|
|
1289
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`);
|
|
1290
1350
|
timed(workDir, 'publish-partial', () => {
|
|
1291
1351
|
const failed = targetsOf(result)
|
|
1292
1352
|
.filter((entry) => entry.ok !== true || entry.status !== 'published')
|
|
@@ -1294,10 +1354,10 @@ function dailyPublishPartial(args: string[]): void {
|
|
|
1294
1354
|
.join(',');
|
|
1295
1355
|
const list = failed === '' ? '(unparsed)' : failed;
|
|
1296
1356
|
console.error(`WARNING: partial publish — failed targets: ${list}`);
|
|
1297
|
-
sted('mkdir',
|
|
1298
|
-
sted('write',
|
|
1357
|
+
sted('mkdir', dirname(marker), () => mkdirSync(dirname(marker), { recursive: true }));
|
|
1358
|
+
sted('write', marker, () =>
|
|
1299
1359
|
writeFileSync(
|
|
1300
|
-
|
|
1360
|
+
marker,
|
|
1301
1361
|
`${runDate} partial publish: failed targets: ${list} (result.json preserved for kk executor fan-out --retry-from)\n`,
|
|
1302
1362
|
),
|
|
1303
1363
|
);
|
|
@@ -1305,10 +1365,16 @@ function dailyPublishPartial(args: string[]): void {
|
|
|
1305
1365
|
}
|
|
1306
1366
|
|
|
1307
1367
|
/**
|
|
1308
|
-
*
|
|
1309
|
-
*
|
|
1310
|
-
*
|
|
1311
|
-
*
|
|
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.
|
|
1312
1378
|
*/
|
|
1313
1379
|
function dailyPatchSurfdash(args: string[]): void {
|
|
1314
1380
|
const [workDir = '', runDate = '', surfdashRoot = '', pluginsPath = ''] = args;
|
|
@@ -1329,149 +1395,53 @@ function dailyPatchSurfdash(args: string[]): void {
|
|
|
1329
1395
|
const sd = targetOf(sdResult);
|
|
1330
1396
|
const pod = targetOf(podcastResult);
|
|
1331
1397
|
const postPath = (sd.metadata as Record<string, unknown> | undefined)?.postPath;
|
|
1332
|
-
if (typeof postPath !== 'string' || postPath === '')
|
|
1398
|
+
if (typeof postPath !== 'string' || postPath === '') {
|
|
1333
1399
|
failWith(1, 'patch-surfdash: surfdash result carries no postPath');
|
|
1334
|
-
// Refuse before anything is written: surfdash's deploy.yml publishes from main, so a commit
|
|
1335
|
-
// on any other branch is invisible to production. Placed here (after the input checks) so a
|
|
1336
|
-
// refusal leaves the working tree untouched.
|
|
1337
|
-
const head = spawnSync('git', ['-C', surfdashRoot, 'rev-parse', '--abbrev-ref', 'HEAD'])
|
|
1338
|
-
.stdout?.toString()
|
|
1339
|
-
.trim();
|
|
1340
|
-
if (head !== 'main')
|
|
1341
|
-
failWith(1, `patch-surfdash: refusing to commit on '${head}' — surfdash deploys from main`);
|
|
1342
|
-
// Paths this run actually wrote, relative to surfdashRoot. `git add`/`commit` take these as a
|
|
1343
|
-
// pathspec so the operator's unrelated staged work is never swept into the commit.
|
|
1344
|
-
const written: string[] = [];
|
|
1345
|
-
const slugDir = dirname(postPath);
|
|
1346
|
-
const showNotesUrl = typeof pod.url === 'string' ? pod.url : '';
|
|
1347
|
-
const coverUrl = ((pod.metadata as Record<string, unknown> | undefined)?.imageUrl as string | undefined) ?? '';
|
|
1348
|
-
const localCover = join(workDir, '23-cover', `${runDate}_07_cover_cover.png`);
|
|
1349
|
-
const dateIso = `${runDate.slice(0, 4)}-${runDate.slice(4, 6)}-${runDate.slice(6, 8)}`;
|
|
1350
|
-
const crossLink: Record<string, string> = {
|
|
1351
|
-
// Task 0189 R4: cross-links carry the column's episode-title pattern, not the retired
|
|
1352
|
-
// news-bulletin name (the VoiceScript episode title is episodeTitle(lead, date)).
|
|
1353
|
-
zh: `🎧 本期已同步制作为播客节目,[点击收听《每日AI乱炖 · ${dateIso}》](${showNotesUrl})。`,
|
|
1354
|
-
en: `🎧 This episode is also available as a podcast: [listen to 每日AI乱炖 · ${dateIso}](${showNotesUrl}).`,
|
|
1355
|
-
ja: `🎧 本エピソードはポッドキャストでも配信中:[每日AI乱炖 · ${dateIso} を聴く](${showNotesUrl})。`,
|
|
1356
|
-
};
|
|
1357
|
-
let changed = 0;
|
|
1358
|
-
for (const [locale, dir] of [
|
|
1359
|
-
// zh posts live under articles/zh/<slug>; en/ja under articles/<locale>/<slug> —
|
|
1360
|
-
// two dirnames up, not one (20260919: `articles/zh/en/...` never existed, so en/ja
|
|
1361
|
-
// were silently skipped by the not-published branch).
|
|
1362
|
-
['zh', slugDir],
|
|
1363
|
-
['en', join(dirname(dirname(slugDir)), 'en', basename(slugDir))],
|
|
1364
|
-
['ja', join(dirname(dirname(slugDir)), 'ja', basename(slugDir))],
|
|
1365
|
-
] as const) {
|
|
1366
|
-
const indexMd = join(dir, 'index.md');
|
|
1367
|
-
if (!existsSync(indexMd)) {
|
|
1368
|
-
console.error(`patch-surfdash: ${locale} post not published (${indexMd}) — skip`);
|
|
1369
|
-
continue;
|
|
1370
|
-
}
|
|
1371
|
-
let md = readFileSync(indexMd, 'utf-8');
|
|
1372
|
-
const fmEnd = md.indexOf('---', 3); // frontmatter is `---\n ... \n---\n`
|
|
1373
|
-
if (fmEnd < 0) failWith(1, `patch-surfdash: no frontmatter in ${indexMd}`);
|
|
1374
|
-
// Upsert a single-line `key: 'value'`; the regex consumes folded-scalar continuation
|
|
1375
|
-
// lines (`og_image: >-\n https://...`) so multi-line publish output is replaced whole.
|
|
1376
|
-
const upsertKey = (src: string, key: string, value: string): string => {
|
|
1377
|
-
// YAML single-quoted scalars escape `'` as `''`; the replacement is a function so
|
|
1378
|
-
// `$&`/`$1`/`$$` in the value are never expanded. Scope the match to the frontmatter
|
|
1379
|
-
// (recomputed per call, since each upsert shifts offsets) so a body line starting with
|
|
1380
|
-
// `image:` is not rewritten.
|
|
1381
|
-
const end = src.indexOf('\n---', 3);
|
|
1382
|
-
const fm = src.slice(0, end);
|
|
1383
|
-
const body = src.slice(end);
|
|
1384
|
-
const line = `${key}: '${value.replaceAll("'", "''")}'`;
|
|
1385
|
-
const re = new RegExp(`^${key}:.*(?:\\n[ \\t]+.*)*`, 'm');
|
|
1386
|
-
const next = re.test(fm)
|
|
1387
|
-
? fm.replace(re, () => line)
|
|
1388
|
-
: fm.replace(/^publishDate: .*$/m, (m0) => `${m0}\n${line}`);
|
|
1389
|
-
return next + body;
|
|
1390
|
-
};
|
|
1391
|
-
if (coverUrl !== '') {
|
|
1392
|
-
// 20260919 prod feedback: the podcast-hosted cover URL wins even when surfdash-pub
|
|
1393
|
-
// already wrote `image:` — its resolved og_image points at /content-posts/<slug>/
|
|
1394
|
-
// assets/cover.png, which the deployed worker does not serve (404).
|
|
1395
|
-
md = upsertKey(md, 'image', coverUrl);
|
|
1396
|
-
md = upsertKey(md, 'og_image', coverUrl);
|
|
1397
|
-
} else if (!/^image:/m.test(md.slice(0, fmEnd))) {
|
|
1398
|
-
// Fallback for runs without a podcast cover: copy the local cover instead.
|
|
1399
|
-
if (existsSync(localCover)) {
|
|
1400
|
-
const assets = join(dir, 'assets');
|
|
1401
|
-
mkdirSync(assets, { recursive: true });
|
|
1402
|
-
copyFileSync(localCover, join(assets, 'cover.png'));
|
|
1403
|
-
written.push(relative(surfdashRoot, join(assets, 'cover.png')));
|
|
1404
|
-
md = upsertKey(md, 'image', './assets/cover.png');
|
|
1405
|
-
md = upsertKey(md, 'og_image', './assets/cover.png');
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
if (showNotesUrl !== '' && !md.includes(showNotesUrl)) {
|
|
1409
|
-
md = `${md.replace(/\s*$/, '')}\n\n${crossLink[locale]}\n`;
|
|
1410
|
-
}
|
|
1411
|
-
if (md !== readFileSync(indexMd, 'utf-8')) {
|
|
1412
|
-
sted('write', indexMd, () => writeFileSync(indexMd, md));
|
|
1413
|
-
written.push(relative(surfdashRoot, indexMd));
|
|
1414
|
-
changed++;
|
|
1415
|
-
}
|
|
1416
1400
|
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
// rewrites files, so re-sync the EmDash rows from the patched files through the same
|
|
1421
|
-
// `kk executor` path as translate-sync (its plugin child gets EMDASH_API_TOKEN from kk
|
|
1422
|
-
// config; a direct orchestrator spawn gets 403 from prod dev-bypass). Runs before the
|
|
1423
|
-
// changed===0 early-return so re-runs heal rows an earlier commit left stale
|
|
1424
|
-
// (20260925/20260929). Fixture tests pass no plugins-path and must not shell out.
|
|
1425
|
-
if (pluginsPath === '') {
|
|
1426
|
-
// A missing plugins-path silently drops the CMS re-sync, and the CMS cards then keep the
|
|
1427
|
-
// pre-patch `./assets/cover.png` the deployed worker cannot serve (20261002 lost its
|
|
1428
|
-
// 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.
|
|
1429
1404
|
const skipMarker = join(workDir, '31-publish', `${runDate}_23_patch-surfdash_emdash-resync.txt`);
|
|
1430
|
-
sted('write', skipMarker, () => writeRaw(skipMarker, 'skipped:
|
|
1431
|
-
console.error('patch-surfdash:
|
|
1432
|
-
} else {
|
|
1433
|
-
const syncIn = join(workDir, '31-publish', `${runDate}_23_patch-surfdash_sync.json`);
|
|
1434
|
-
const syncResult = join(workDir, '31-publish', `${runDate}_23_patch-surfdash_sync-result.json`);
|
|
1435
|
-
sted('write', syncIn, () =>
|
|
1436
|
-
writeJqText(syncIn, { body: '', options: { operation: 'sync', postPath: join(slugDir, 'index.md') } }),
|
|
1437
|
-
);
|
|
1438
|
-
const status = kk(
|
|
1439
|
-
['executor', 'run', 'surfdash-pub', '--in', syncIn, '--out', syncResult, ...pluginsArgs(pluginsPath)],
|
|
1440
|
-
{ unset: NESTED_SPUR_UNSET, soft: true },
|
|
1441
|
-
);
|
|
1442
|
-
const result = isFile(syncResult) ? readJson(syncResult) : null;
|
|
1443
|
-
const ok =
|
|
1444
|
-
status === 0 &&
|
|
1445
|
-
result !== null &&
|
|
1446
|
-
(result.ok === true || (result.targets as Array<{ ok?: boolean }> | undefined)?.[0]?.ok === true);
|
|
1447
|
-
if (!ok) {
|
|
1448
|
-
console.error(
|
|
1449
|
-
`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}`,
|
|
1450
|
-
);
|
|
1451
|
-
} else {
|
|
1452
|
-
console.error('patch-surfdash: emdash rows re-synced from patched files');
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
if (changed === 0) {
|
|
1456
|
-
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)');
|
|
1457
1407
|
return;
|
|
1458
1408
|
}
|
|
1459
|
-
const
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
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
|
+
}),
|
|
1470
1423
|
);
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
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 },
|
|
1474
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}`)}`);
|
|
1475
1445
|
});
|
|
1476
1446
|
}
|
|
1477
1447
|
|
|
@@ -9,7 +9,7 @@ Review-item IDs here are **this skill's bookkeeping numbers**, not official plat
|
|
|
9
9
|
| Effective date | unknown(运营规范版本/生效日期未核验) |
|
|
10
10
|
| Last verified | 2026-09-29(仅事件层面;规则原文未逐条核验) |
|
|
11
11
|
| Evidence status | 规则条文**未核验**;2026-09-29 图文下架事件为**用户报告**,平台未给出书面理由 |
|
|
12
|
-
| Changelog | 2026-09-29 — file created after the takedown report (task 0189) |
|
|
12
|
+
| Changelog | 2026-09-29 — file created after the takedown report (task 0189) · 2026-10-10 — 操作者裁定:WX-02 降级为 note,不再 escalate |
|
|
13
13
|
|
|
14
14
|
## Evidence distinctions
|
|
15
15
|
|
|
@@ -23,7 +23,7 @@ Review-item IDs here are **this skill's bookkeeping numbers**, not official plat
|
|
|
23
23
|
| ID | Item | Grade |
|
|
24
24
|
|----|------|-------|
|
|
25
25
|
| WX-01 | 时政/公共事务主线:政治、外交/贸易管制、军事、公共政策争议、社会公共事件为主的图文或音频 | block |
|
|
26
|
-
| WX-02 | 资质口径:不做新闻资讯播报形态的标题/摘要(未持有相关资质;同小宇宙 XY-01
|
|
26
|
+
| WX-02 | 资质口径:不做新闻资讯播报形态的标题/摘要(未持有相关资质;同小宇宙 XY-01 的教训)。**操作者裁定 2026-10-10**:同小宇宙 XY-01 裁定——本号不持有也不声称持有新闻资质,剩余风险只在单集标题/摘要是否真的退回播报形态,按 note 记录,不要因「账号无新闻资质」而 block 或 `escalate: true`;仅当某集实际呈现新闻播报形态时才升级为 block。 | note |
|
|
27
27
|
| WX-03 | AI 生成声明:图文含 AI 生成内容时按平台入口如实声明(声明入口以平台当前设置为准,未核验 → evidenceGap) | disclose |
|
|
28
28
|
| WX-04 | 可识别个人信息;标题党/夸大断言(未经核实断言按类别 4 处理) | desensitize / fix |
|
|
29
29
|
|
|
@@ -9,7 +9,7 @@ Review-item IDs here are **this skill's bookkeeping numbers**, not official plat
|
|
|
9
9
|
| Effective date | unknown(协议页面未标注可引用的版本生效日期) |
|
|
10
10
|
| Last verified | 2026-09-29(任务 0189 期间人工阅读协议页) |
|
|
11
11
|
| Evidence status | 协议原文已读;**未发现公开的字级禁用词表**;2026-09-29 下架事件为**用户报告的反馈**,非官方说明 |
|
|
12
|
-
| Changelog | 2026-09-29 — file created after the daily-episode takedown report (task 0189) |
|
|
12
|
+
| Changelog | 2026-09-29 — file created after the daily-episode takedown report (task 0189) · 2026-10-10 — 操作者裁定:XY-01 降级为 note,不再 escalate |
|
|
13
13
|
|
|
14
14
|
## Evidence distinctions
|
|
15
15
|
|
|
@@ -23,7 +23,7 @@ Review-item IDs here are **this skill's bookkeeping numbers**, not official plat
|
|
|
23
23
|
|
|
24
24
|
| ID | Item | Grade |
|
|
25
25
|
|----|------|-------|
|
|
26
|
-
| XY-01 |
|
|
26
|
+
| XY-01 | 资质口径:简介/标题不得呈现「新闻资讯播报」形态或暗示持有新闻资质(对齐用户报告的下架理由;新闻资质口径为国内平台语境)。**操作者裁定 2026-10-10**:本档为个人技术分享播客(栏目「AI 乱炖」),不持有也不声称持有新闻资质,因此不得宣称或暗示该资质这一项本身已满足;剩余风险只在单集文案是否真的退回播报形态,按 note 记录,不要因为「账号无新闻资质」而 block 或 `escalate: true`——它对每一集都一样,逐集 escalate 只会让无人值守的每日任务永远无法收尾。仅当某集实际呈现新闻播报形态时才升级为 block。 | note |
|
|
27
27
|
| XY-02 | 时政/公共事务主线:政治、外交/贸易管制、军事、公共政策争议、社会公共事件为主线的单集内容 | block |
|
|
28
28
|
| XY-03 | 字词命中:敏感领域词汇出现时先做上下文判断(hit ≠ verdict);命中且主线无风险 → 记录为 note | note→judge |
|
|
29
29
|
| XY-04 | 可识别个人信息(简介/shownotes 中听众或第三方的私人信息) | desensitize→block if unfixable |
|
|
@@ -33,3 +33,5 @@ Review-item IDs here are **this skill's bookkeeping numbers**, not official plat
|
|
|
33
33
|
- 无官方字级词表 → 任何「词命中即下架」的断言都不成立;同理,无词表不等于安全。
|
|
34
34
|
- 下架真实原因未获官方确认 → 每次提审前按 XY-01/XY-02 复核;`RISK` 或证据缺口一律人工复审
|
|
35
35
|
(`escalate: true`),不得静默 PASS。
|
|
36
|
+
- 裁定 2026-10-10:XY-01 已按上表降级,资质口径本身不再触发人工复审;XY-02(时政主线)
|
|
37
|
+
与证据缺口仍照旧 escalate。
|
|
@@ -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` / `
|
|
28
|
-
| juejin | `juejin-pub` | `browser` / `
|
|
29
|
-
| infoq | `infoq-pub` | `browser` / `
|
|
30
|
-
| substack | `substack-pub` | `browser` / `
|
|
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
|
|
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
|
-
|
|
80
|
-
kk-daily-ai-voice's collect state)
|
|
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
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
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
|
|
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
|
|
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[]
|
|
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; }
|