@gobing-ai/knowledge-kit 0.1.11 → 0.1.13
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/dist/index.js +2372 -1722
- package/package.json +1 -1
- package/plugins/generations/image-gen/README.md +108 -0
- package/plugins/generations/image-gen/assets/sample/cover__clean-webapp-ui.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__cute.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__daily-news.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__editorial.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__fresh.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__minimalist.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__photorealistic.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__sketch.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__technical-diagram.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__vibrant.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__warm.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__xhs-knowledge.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__clean-webapp-ui.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__cute.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__daily-news.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__editorial.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__fresh.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__minimalist.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__photorealistic.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__sketch.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__technical-diagram.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__vibrant.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__warm.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__xhs-knowledge.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__clean-webapp-ui.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__cute.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__daily-news.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__editorial.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__fresh.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__minimalist.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__photorealistic.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__sketch.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__technical-diagram.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__vibrant.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__warm.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__xhs-knowledge.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__clean-webapp-ui.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__cute.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__daily-news.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__editorial.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__fresh.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__minimalist.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__photorealistic.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__sketch.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__technical-diagram.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__vibrant.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__warm.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__xhs-knowledge.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__clean-webapp-ui.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__cute.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__daily-news.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__editorial.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__fresh.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__minimalist.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__photorealistic.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__sketch.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__technical-diagram.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__vibrant.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__warm.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__xhs-knowledge.jpg +0 -0
- package/plugins/generations/image-gen/dist/index.js +102 -58
- package/plugins/generations/image-gen/scripts/compose-e2e-artifact.ts +5 -21
- package/plugins/generations/image-gen/scripts/gen-samples.ts +208 -0
- package/plugins/generations/image-gen/src/compose/chromium.ts +2 -3
- package/plugins/generations/image-gen/src/presets.ts +6 -3
- package/plugins/generations/image-gen/src/providers/errors.ts +16 -1
- package/plugins/generations/image-gen/src/providers/minimax.ts +46 -17
- package/plugins/ingestions/web-search/src/index.ts +1 -1
- package/plugins/kk/README.md +18 -0
- package/plugins/kk/commands/image-extract.md +1 -0
- package/plugins/kk/commands/image-generate.md +1 -0
- package/plugins/kk/commands/tell-me.md +1 -0
- package/plugins/kk/commands/workflow-run.md +7 -1
- package/plugins/kk/decisions/extract/it-topics.yaml +24 -0
- package/plugins/kk/decisions/kk-itc.yaml +6 -4
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/publish-presets/daily-ai-voice-migration.json +146 -0
- package/plugins/kk/publish-presets/daily-ai-voice.json +15 -7
- package/plugins/kk/schedules/kk-collect.grok-bot.md.tmpl +69 -0
- package/plugins/kk/schedules/kk-daily-ai-voice3.cron.tmpl +18 -0
- package/plugins/kk/schedules/kk-daily-publish3-cn-peak.cron.tmpl +15 -5
- package/plugins/kk/schedules/kk-daily-publish3-overseas.cron.tmpl +9 -4
- package/plugins/kk/scripts/daily-scheduled-run.ts +91 -41
- package/plugins/kk/scripts/daily-stages.ts +394 -53
- package/plugins/kk/scripts/kk-workflow-stages.ts +190 -0
- package/plugins/kk/scripts/safety-artifact-check.ts +70 -28
- package/plugins/kk/scripts/voice3-publish-window.ts +202 -0
- package/plugins/kk/skills/article-adapt/SKILL.md +1 -0
- package/plugins/kk/skills/audio-authoring/SKILL.md +1 -0
- package/plugins/kk/skills/content-judge/SKILL.md +1 -0
- package/plugins/kk/skills/content-safety/SKILL.md +1 -0
- package/plugins/kk/skills/content-safety/references/platforms/podcast-salty.md +41 -0
- package/plugins/kk/skills/content-safety/references/platforms/surfdash.md +36 -0
- package/plugins/kk/skills/content-safety/references/platforms/xhs.md +39 -0
- package/plugins/kk/skills/explain-things/SKILL.md +1 -0
- package/plugins/kk/skills/image-authoring/SKILL.md +1 -0
- package/plugins/kk/skills/itc-generating/SKILL.md +1 -0
- package/plugins/kk/skills/publish/SKILL.md +1 -0
- package/plugins/kk/skills/smart-seo/SKILL.md +1 -0
- package/plugins/kk/skills/storm-research/SKILL.md +1 -0
- package/plugins/kk/skills/taste-unslop/SKILL.md +1 -0
- package/plugins/kk/skills/topic/SKILL.md +1 -0
- package/plugins/kk/skills/web-automation/SKILL.md +1 -0
- package/plugins/kk/skills/xhs-images/SKILL.md +1 -0
- package/plugins/kk/workflows/kk-collect.yaml +3 -3
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +21 -19
- package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +163 -63
- package/plugins/kk/workflows/kk-daily-publish3.yaml +82 -26
- package/plugins/kk/workflows/kk-daily-safety.yaml +39 -31
- package/plugins/kk/workflows/kk-itc-author.yaml +75 -0
- package/plugins/kk/workflows/kk-itc-publish-tail.yaml +25 -3
- package/plugins/kk/workflows/kk-itc.yaml +36 -4
- package/plugins/publishings/linkedin-pub/dist/index.js +21333 -0
- package/plugins/publishings/linkedin-pub/dist/profile-check.js +21005 -0
- package/plugins/publishings/linkedin-pub/package.json +19 -0
- package/plugins/publishings/linkedin-pub/plugin.json +12 -0
- package/plugins/publishings/linkedin-pub/src/index.ts +705 -0
- package/plugins/publishings/linkedin-pub/src/profile-check.ts +17 -0
- package/plugins/publishings/linkedin-pub/tsconfig.json +4 -0
- package/plugins/publishings/surfdash-pub/dist/index.js +53 -24
- package/plugins/publishings/surfdash-pub/src/index.ts +72 -25
- package/plugins/publishings/threads-pub/dist/index.js +7 -0
- package/plugins/publishings/threads-pub/src/index.ts +12 -0
- package/plugins/publishings/wechatmp-pub/dist/index.js +176 -32
- package/plugins/publishings/wechatmp-pub/src/flow.ts +298 -52
- package/plugins/publishings/weibo-pub/dist/index.js +100 -7
- package/plugins/publishings/weibo-pub/src/index.ts +111 -11
- package/plugins/publishings/x-pub/dist/index.js +7 -0
- package/plugins/publishings/x-pub/src/index.ts +11 -0
- package/plugins/publishings/xhs-pub/dist/index.js +164 -12
- package/plugins/publishings/xhs-pub/dist/profile-check.js +11 -1
- package/plugins/publishings/xhs-pub/src/flow.ts +194 -23
- package/plugins/publishings/xhs-pub/src/page.ts +26 -0
|
@@ -89,7 +89,6 @@ import {
|
|
|
89
89
|
PII_FINDING_CATEGORY,
|
|
90
90
|
pluginsArgs,
|
|
91
91
|
readJson,
|
|
92
|
-
readSafetyReviseCount,
|
|
93
92
|
readTopicBoundary,
|
|
94
93
|
requireJson,
|
|
95
94
|
requireSidecar,
|
|
@@ -716,13 +715,23 @@ function refuseScriptBelowFloor(
|
|
|
716
715
|
}
|
|
717
716
|
|
|
718
717
|
export function dailyEpisodeDuration(args: string[]): void {
|
|
719
|
-
const [workDir = '', runDate = '', minArg = '', maxArg = '', vdriver = ''] = args;
|
|
718
|
+
const [workDir = '', runDate = '', minArg = '', maxArg = '', vdriver = '', runId = ''] = args;
|
|
720
719
|
if (!workDir) fail('missing work_dir');
|
|
721
720
|
if (!runDate) fail('missing run_date');
|
|
722
721
|
const minMinutes = Number(minArg);
|
|
723
722
|
const maxMinutes = Number(maxArg);
|
|
724
723
|
if (!Number.isFinite(minMinutes) || minMinutes <= 0) fail(`invalid min_minutes: ${minArg || '(empty)'}`);
|
|
725
724
|
if (!Number.isFinite(maxMinutes) || maxMinutes < minMinutes) fail(`invalid max_minutes: ${maxArg || '(empty)'}`);
|
|
725
|
+
// 0329 F06: the publish path must not survive a refusal. The pool-gate token flips to EMPTY so
|
|
726
|
+
// this run's own remaining fan-out skips, and a stale bundle.json from an earlier run (whose
|
|
727
|
+
// runs[] a publish window could still consume) is dropped.
|
|
728
|
+
const invalidatePublishPath = (): void => {
|
|
729
|
+
if (runId !== '') {
|
|
730
|
+
const token = join('.spur', 'run', `${runId}-pool-gate.status`);
|
|
731
|
+
if (isFile(token)) sted('write', token, () => writeRaw(token, 'EMPTY\n'));
|
|
732
|
+
}
|
|
733
|
+
rmSync(join(workDir, 'bundle.json'), { force: true });
|
|
734
|
+
};
|
|
726
735
|
const contentPath = join(workDir, '30-audio', `${runDate}_11_generate_content.json`);
|
|
727
736
|
if (!isFile(contentPath)) failWith(1, `episode-duration: audio content not found: ${contentPath}`);
|
|
728
737
|
let raw: unknown;
|
|
@@ -751,7 +760,8 @@ export function dailyEpisodeDuration(args: string[]): void {
|
|
|
751
760
|
// let the pool-gated publish fan package a sub-floor episode — so finish the emptying here.
|
|
752
761
|
if (isFile(refusal)) {
|
|
753
762
|
if (docs.length > 0) emptyCandidates(workDir, runDate);
|
|
754
|
-
|
|
763
|
+
invalidatePublishPath();
|
|
764
|
+
console.error(`episode-duration: duration refusal already recorded (${refusal}) — nothing to do`);
|
|
755
765
|
return;
|
|
756
766
|
}
|
|
757
767
|
const estimated = estimateSpokenMinutes(docs, vdriver);
|
|
@@ -767,6 +777,7 @@ export function dailyEpisodeDuration(args: string[]): void {
|
|
|
767
777
|
}),
|
|
768
778
|
);
|
|
769
779
|
emptyCandidates(workDir, runDate);
|
|
780
|
+
invalidatePublishPath();
|
|
770
781
|
console.error(
|
|
771
782
|
`episode-duration: episode ran ${shown} min (estimate ${estimated.minutes} min), below the ${minMinutes}–${maxMinutes} min requirement — candidates emptied, the run closes as no-content (see ${refusal})`,
|
|
772
783
|
);
|
|
@@ -1715,7 +1726,15 @@ export function buildWechatArticleContent(
|
|
|
1715
1726
|
return {
|
|
1716
1727
|
...article,
|
|
1717
1728
|
body: options.appendOutro === false ? body : `${body}${WECHAT_OUTRO}`,
|
|
1718
|
-
metadata: {
|
|
1729
|
+
metadata: {
|
|
1730
|
+
...metadataOf(article),
|
|
1731
|
+
// wechatmp-pub's 播客 operation resolves the episode mp3 through the generic media key
|
|
1732
|
+
// `metadata.audio` (`modes.ts` resolveMediaPath), while the 文章 audio-embed rail keeps
|
|
1733
|
+
// reading `wechatmp.audioPath` — carry both spellings so one variant serves both rows.
|
|
1734
|
+
...(options.audioPath === undefined ? {} : { audio: options.audioPath }),
|
|
1735
|
+
publishStatus: options.publishStatus,
|
|
1736
|
+
wechatmp,
|
|
1737
|
+
},
|
|
1719
1738
|
};
|
|
1720
1739
|
}
|
|
1721
1740
|
|
|
@@ -1789,9 +1808,24 @@ export function buildPodcastContent(
|
|
|
1789
1808
|
*
|
|
1790
1809
|
* surfdash is the article's own publisher, so it consumes what `22-article` already composed — the
|
|
1791
1810
|
* builder exists so the variant set is uniform, not because it reshapes anything.
|
|
1811
|
+
*
|
|
1812
|
+
* surfdash-pub's `post` operation refuses a digest post without `metadata.coverImage`
|
|
1813
|
+
* (surfdash-pub/src/index.ts), so the run's cover rides along exactly as the episode/wechat
|
|
1814
|
+
* builders stamp it. voice2's prepare mode never exercised this gate; the frozen-bundle matrix
|
|
1815
|
+
* send surfaced it on 2026-11-05 (empty run → 'digest post has no cover image').
|
|
1792
1816
|
*/
|
|
1793
|
-
export function buildSurfdashContent(
|
|
1794
|
-
|
|
1817
|
+
export function buildSurfdashContent(
|
|
1818
|
+
article: Record<string, unknown>,
|
|
1819
|
+
_links: PublishLinks,
|
|
1820
|
+
options: { coverPath?: string } = {},
|
|
1821
|
+
): Record<string, unknown> {
|
|
1822
|
+
return {
|
|
1823
|
+
...article,
|
|
1824
|
+
metadata: {
|
|
1825
|
+
...metadataOf(article),
|
|
1826
|
+
...(options.coverPath === undefined ? {} : { coverImage: options.coverPath }),
|
|
1827
|
+
},
|
|
1828
|
+
};
|
|
1795
1829
|
}
|
|
1796
1830
|
|
|
1797
1831
|
/**
|
|
@@ -1878,12 +1912,13 @@ export function buildSocialContent(
|
|
|
1878
1912
|
|
|
1879
1913
|
/**
|
|
1880
1914
|
* Length budgets the social authoring prompt targets, enforced here as a backstop (task 0327 R3).
|
|
1881
|
-
* X counts CJK at 2 (`weighted`); weibo and
|
|
1915
|
+
* X counts CJK at 2 (`weighted`); weibo, threads and linkedin count characters.
|
|
1882
1916
|
*/
|
|
1883
1917
|
const SOCIAL_LIMITS: Record<string, SocialLimit> = {
|
|
1884
1918
|
x: { max: 280, weighted: true },
|
|
1885
1919
|
weibo: { max: 2000 },
|
|
1886
1920
|
threads: { max: 500 },
|
|
1921
|
+
linkedin: { max: 3000 },
|
|
1887
1922
|
};
|
|
1888
1923
|
|
|
1889
1924
|
/** Variant key → file name: `social:x` becomes `social_x.content.json`. */
|
|
@@ -1892,27 +1927,36 @@ function variantFileName(key: string): string {
|
|
|
1892
1927
|
}
|
|
1893
1928
|
|
|
1894
1929
|
/**
|
|
1895
|
-
* `daily-bundle-variants <workDir> <runDate> <pluginsPath
|
|
1896
|
-
* (task 0327 R2/R3).
|
|
1930
|
+
* `daily-bundle-variants <workDir> <runDate> <pluginsPath> [cardsEnabled] [coverEnabled]` — the
|
|
1931
|
+
* generate-side payload builder (task 0327 R2/R3).
|
|
1897
1932
|
*
|
|
1898
1933
|
* Everything the publish side used to assemble inline is produced here instead, once, as bundle
|
|
1899
1934
|
* variants under `30-variants/`. Links are the `{{article_url}}` / `{{podcast_url}}` placeholders,
|
|
1900
1935
|
* because at generate time nothing is published yet; `publish-matrix-send` resolves them from the
|
|
1901
1936
|
* tier-0 receipts. A variant whose source artifact is absent (channel disabled, cards never
|
|
1902
1937
|
* rendered, social copy not authored) is skipped with a log line, never invented.
|
|
1938
|
+
*
|
|
1939
|
+
* 0329 F16: the caller forwards the run's `cards_enabled` / `cover_enabled` switches — a disabled
|
|
1940
|
+
* producer's variant is a skip, not a stale re-read of a previous run's file — and every stale
|
|
1941
|
+
* variant file is wiped before writing, so `30-variants/` after this stage contains only what
|
|
1942
|
+
* THIS pass produced.
|
|
1903
1943
|
*/
|
|
1904
1944
|
export function dailyBundleVariants(args: string[]): void {
|
|
1905
|
-
const [workDir = '', runDate = '', _pluginsPath = ''] = args;
|
|
1945
|
+
const [workDir = '', runDate = '', _pluginsPath = '', cardsEnabledArg = '', coverEnabledArg = ''] = args;
|
|
1906
1946
|
timed(workDir, 'bundle-variants', () => {
|
|
1907
1947
|
const outDir = join(workDir, '30-variants');
|
|
1908
1948
|
sted('mkdir', outDir, () => mkdirSync(outDir, { recursive: true }));
|
|
1949
|
+
// 0329 F16: a rerun after a thinner day must not leave yesterday's variants behind.
|
|
1950
|
+
for (const entry of readdirSync(outDir)) {
|
|
1951
|
+
if (entry.endsWith('.content.json')) rmSync(join(outDir, entry), { force: true });
|
|
1952
|
+
}
|
|
1909
1953
|
const articlePath = join(workDir, '22-article', `${runDate}_06_article_content.json`);
|
|
1910
1954
|
const prepPath = join(workDir, '31-publish', `${runDate}_13_publish-prep_content.json`);
|
|
1911
1955
|
const notesPath = join(workDir, '31-publish', `${runDate}_15_show-notes_show-notes.md`);
|
|
1912
1956
|
const cardsPath = join(workDir, '25-xhs', 'content.json');
|
|
1913
1957
|
const coverPath = join(workDir, '23-cover', `${runDate}_07_cover_cover.png`);
|
|
1914
1958
|
const socialDir = join(workDir, '26-social');
|
|
1915
|
-
const cover = isFile(coverPath) ? coverPath : undefined;
|
|
1959
|
+
const cover = coverEnabledArg !== 'false' && isFile(coverPath) ? resolve(coverPath) : undefined;
|
|
1916
1960
|
const links = PLACEHOLDER_LINKS;
|
|
1917
1961
|
const written: string[] = [];
|
|
1918
1962
|
const skipped: string[] = [];
|
|
@@ -1929,8 +1973,8 @@ export function dailyBundleVariants(args: string[]): void {
|
|
|
1929
1973
|
// shared copy must exist for the channels the preset does not specialise.
|
|
1930
1974
|
if (article === null) skipped.push('article (no 22-article content)');
|
|
1931
1975
|
else {
|
|
1932
|
-
write('article', buildSurfdashContent(article, links));
|
|
1933
|
-
write('article@surfdash-pub', buildSurfdashContent(article, links));
|
|
1976
|
+
write('article', buildSurfdashContent(article, links, { coverPath: cover }));
|
|
1977
|
+
write('article@surfdash-pub', buildSurfdashContent(article, links, { coverPath: cover }));
|
|
1934
1978
|
write('article@xhs-pub', buildXhsArticleContent(article, { publishStatus: 'draft' }));
|
|
1935
1979
|
write(
|
|
1936
1980
|
'article@wechatmp-pub',
|
|
@@ -1966,9 +2010,12 @@ export function dailyBundleVariants(args: string[]): void {
|
|
|
1966
2010
|
);
|
|
1967
2011
|
}
|
|
1968
2012
|
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
2013
|
+
if (cardsEnabledArg === 'false') skipped.push('cards (cards_enabled=false)');
|
|
2014
|
+
else {
|
|
2015
|
+
const cards = readJson(cardsPath);
|
|
2016
|
+
if (cards === null) skipped.push('cards (no rendered card series)');
|
|
2017
|
+
else write('cards@xhs-pub', buildXhsCardsContent(cards, { publishStatus: 'draft' }));
|
|
2018
|
+
}
|
|
1972
2019
|
|
|
1973
2020
|
for (const platform of Object.keys(SOCIAL_LIMITS)) {
|
|
1974
2021
|
const copyPath = join(socialDir, `${platform}.md`);
|
|
@@ -2048,6 +2095,8 @@ export function readKeyPointItems(filePath: string): string[] | undefined {
|
|
|
2048
2095
|
flush();
|
|
2049
2096
|
current = item[1].trim();
|
|
2050
2097
|
folded = current === '>' || current === '>-' || current === '|-' || current === '|';
|
|
2098
|
+
// The block indicator is YAML syntax, not text: the item starts on the next line.
|
|
2099
|
+
if (folded) current = '';
|
|
2051
2100
|
continue;
|
|
2052
2101
|
}
|
|
2053
2102
|
if (current === undefined || !folded) continue;
|
|
@@ -3081,6 +3130,30 @@ export function dailyQualityReport(args: string[]): void {
|
|
|
3081
3130
|
});
|
|
3082
3131
|
}
|
|
3083
3132
|
|
|
3133
|
+
/**
|
|
3134
|
+
* 0329 F23 — the verdict half of `quality`. The report only printed, so a failed audio qc sailed
|
|
3135
|
+
* on into show-notes, the bundle and the publish windows (2026-10-25: an episode at 41/100 was
|
|
3136
|
+
* packaged and queued for tier-0 sends). Reads the same `metadata.qc` as `daily-quality-report`
|
|
3137
|
+
* and fails loud when the verdict is missing or `passed` is false.
|
|
3138
|
+
*/
|
|
3139
|
+
export function dailyQualityVerdict(args: string[]): void {
|
|
3140
|
+
const [workDir = '', runDate = ''] = args;
|
|
3141
|
+
const content = join(workDir, '30-audio', `${runDate}_11_generate_content.json`);
|
|
3142
|
+
timed(workDir, 'quality-verdict', () => {
|
|
3143
|
+
if (!isFile(content)) failWith(1, `quality-verdict: no audio content: ${content}`);
|
|
3144
|
+
const qc = metadataOf(readJson(content)).qc as Record<string, unknown> | undefined;
|
|
3145
|
+
if (qc === undefined) failWith(1, `quality-verdict: no qc verdict in ${content}`);
|
|
3146
|
+
if (qc.passed !== true) {
|
|
3147
|
+
const issues = Array.isArray(qc.criticalIssues) ? qc.criticalIssues : [];
|
|
3148
|
+
failWith(
|
|
3149
|
+
1,
|
|
3150
|
+
`quality-verdict: audio qc failed (overallScore ${String(qc.overallScore ?? '?')}) — ${issues.map(String).join('; ') || 'no issue detail on the verdict'}`,
|
|
3151
|
+
);
|
|
3152
|
+
}
|
|
3153
|
+
console.log(`quality-verdict: audio qc passed (overallScore ${String(qc.overallScore ?? '?')})`);
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3084
3157
|
/**
|
|
3085
3158
|
* 0171 R3: Apple Podcasts reads the enclosure's ID3 tags, so the published MP3 carries
|
|
3086
3159
|
* title/artist/album/date. `-c copy` moves the audio frames without a re-encode, and the temp
|
|
@@ -3111,18 +3184,19 @@ export function dailyQualityReport(args: string[]): void {
|
|
|
3111
3184
|
* Fail-closed where it matters: a missing report, verdict or confidence still blocks.
|
|
3112
3185
|
*/
|
|
3113
3186
|
export function dailySafetyGate(args: string[]): void {
|
|
3114
|
-
const [workDir = '', runDate = '', maxReviseArg = '0'] = args;
|
|
3187
|
+
const [workDir = '', runDate = '', maxReviseArg = '0', scopeDir = ''] = args;
|
|
3188
|
+
const drafts = isDraftsScope(scopeDir);
|
|
3115
3189
|
const maxRevise = Number.parseInt(maxReviseArg, 10);
|
|
3116
|
-
const statusFile =
|
|
3117
|
-
const stampFile =
|
|
3190
|
+
const statusFile = scopeArtifact(workDir, scopeDir, runDate, 'safety-judge_status.txt');
|
|
3191
|
+
const stampFile = scopeArtifact(workDir, scopeDir, runDate, 'safety-judge_stamp.json');
|
|
3118
3192
|
timed(workDir, 'safety-judge', () => {
|
|
3119
|
-
const request = readJson(
|
|
3120
|
-
const report = readJson(
|
|
3121
|
-
const verdict = readJson(
|
|
3193
|
+
const request = readJson(scopeArtifact(workDir, scopeDir, runDate, 'safety-review_request.json'));
|
|
3194
|
+
const report = readJson(scopeArtifact(workDir, scopeDir, runDate, 'safety-review_report.json'));
|
|
3195
|
+
const verdict = readJson(scopeArtifact(workDir, scopeDir, runDate, 'safety-judge_verdict.json'));
|
|
3122
3196
|
const platforms = Array.isArray(report?.platforms) ? (report.platforms as Record<string, unknown>[]) : [];
|
|
3123
3197
|
const decisions = platforms.map((platform) => platform?.decision);
|
|
3124
|
-
const digest =
|
|
3125
|
-
const revisions =
|
|
3198
|
+
const digest = scopeDigest(workDir, scopeDir, runDate);
|
|
3199
|
+
const revisions = readScopeCounter(workDir, scopeDir);
|
|
3126
3200
|
const platformFindings = (platform: Record<string, unknown>): Record<string, unknown>[] =>
|
|
3127
3201
|
Array.isArray(platform?.findings) ? (platform.findings as Record<string, unknown>[]) : [];
|
|
3128
3202
|
// 20261007 narrowing (operator decision): on a clean pass, only a finding the reviewer
|
|
@@ -3153,9 +3227,12 @@ export function dailySafetyGate(args: string[]): void {
|
|
|
3153
3227
|
// data-handling), which is where an independent reviewer can grade it.
|
|
3154
3228
|
const blockedPlatforms = platforms.filter((platform) => platform?.decision === 'BLOCK');
|
|
3155
3229
|
const excludedPlatforms = blockedPlatforms.map((platform) => String(platform?.platform ?? '')).filter(Boolean);
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
)
|
|
3230
|
+
// 20261104: a category-3 `note` is the reviewer recording that it checked and found NO PII —
|
|
3231
|
+
// counting it turned an admission-only BLOCK into a whole-run block. Any other disposition,
|
|
3232
|
+
// or none (legacy reports), still counts: fail-closed.
|
|
3233
|
+
const isPiiFinding = (finding: Record<string, unknown>): boolean =>
|
|
3234
|
+
Number(finding?.category) === PII_FINDING_CATEGORY && finding?.disposition !== 'note';
|
|
3235
|
+
const piiBlock = blockedPlatforms.some((platform) => platformFindings(platform).some(isPiiFinding));
|
|
3159
3236
|
// A BLOCK that names no finding is a contract violation, not a signal: there is nothing to
|
|
3160
3237
|
// remediate and nothing to exclude, so it blocks (fail-closed).
|
|
3161
3238
|
const unnamedBlock =
|
|
@@ -3219,7 +3296,7 @@ export function dailySafetyGate(args: string[]): void {
|
|
|
3219
3296
|
status = 'block';
|
|
3220
3297
|
const named = blockedPlatforms
|
|
3221
3298
|
.flatMap((platform) => platformFindings(platform))
|
|
3222
|
-
.filter(
|
|
3299
|
+
.filter(isPiiFinding)
|
|
3223
3300
|
.map((finding) => `${String(finding?.item ?? '?')}(category ${String(finding?.category)})`)
|
|
3224
3301
|
.join(', ');
|
|
3225
3302
|
const criteria = verdictFails.map((finding) => String(finding?.criterion)).join(', ');
|
|
@@ -3246,11 +3323,12 @@ export function dailySafetyGate(args: string[]): void {
|
|
|
3246
3323
|
if (status === 'pass' || status === 'escalate' || status === 'exclude') {
|
|
3247
3324
|
sted('write', stampFile, () => writeJsonText(stampFile, { digest, status, excludedPlatforms }));
|
|
3248
3325
|
}
|
|
3249
|
-
if (status === 'block' && !edited) {
|
|
3326
|
+
if (status === 'block' && drafts && !edited) {
|
|
3250
3327
|
// A quality block poisons the isFile() draft cache: every same-day retry would
|
|
3251
3328
|
// re-fail on unchanged bytes (20261002 dogfood — two judge-fail runs replayed until
|
|
3252
3329
|
// the drafts were moved by hand). Quarantine (mv, never rm) so the next run
|
|
3253
3330
|
// regenerates from scratch; the 09_* verdict artifacts stay in 24-script for audit.
|
|
3331
|
+
// Variants scope never quarantines: the bundle is regenerated, not hand-repaired.
|
|
3254
3332
|
const q = join(workDir, '24-script', `.quarantine-${runDate}-judge-block`);
|
|
3255
3333
|
mkdirSync(q, { recursive: true });
|
|
3256
3334
|
for (const path of safetyDraftPaths(workDir, runDate)) {
|
|
@@ -3291,10 +3369,27 @@ export function dailyPreflight(args: string[]): void {
|
|
|
3291
3369
|
String(match[1]),
|
|
3292
3370
|
),
|
|
3293
3371
|
);
|
|
3372
|
+
// 0329 F21: the `publish-matrix-*` stages never appear in kk-workflow-stages' switch —
|
|
3373
|
+
// `kk stage` routes them through apps/cli's publish-matrix-stage.ts — so without this
|
|
3374
|
+
// roster every workflow invoking them died as runner-stale.
|
|
3375
|
+
const PUBLISH_MATRIX_STAGES = [
|
|
3376
|
+
'publish-matrix-plan',
|
|
3377
|
+
'publish-matrix-check',
|
|
3378
|
+
'publish-matrix-send',
|
|
3379
|
+
'publish-matrix-classify',
|
|
3380
|
+
'publish-matrix-bundle',
|
|
3381
|
+
'publish-matrix-rss-gate',
|
|
3382
|
+
'publish-matrix-wait-until',
|
|
3383
|
+
'publish-matrix-validate',
|
|
3384
|
+
'publish-matrix-settle',
|
|
3385
|
+
] as const;
|
|
3386
|
+
for (const name of PUBLISH_MATRIX_STAGES) known.add(name);
|
|
3294
3387
|
const missing: string[] = [];
|
|
3295
3388
|
for (const file of files) {
|
|
3296
3389
|
if (!isFile(file)) failWith(1, `daily-preflight.workflow-missing: ${file}`);
|
|
3297
|
-
|
|
3390
|
+
// 0329 F21: accept both spellings — `kk stage <name>` and the ADR-037 canonical
|
|
3391
|
+
// `kk stage run <name>`.
|
|
3392
|
+
for (const match of readFileSync(file, 'utf-8').matchAll(/\bkk stage (?:run )?([a-z][a-z0-9-]*)/g)) {
|
|
3298
3393
|
const name = String(match[1]);
|
|
3299
3394
|
if (!known.has(name)) missing.push(`${name} (needed by ${basename(file)})`);
|
|
3300
3395
|
}
|
|
@@ -3325,11 +3420,12 @@ export function dailyPreflight(args: string[]): void {
|
|
|
3325
3420
|
* offending value on stderr. Never exits non-zero: routing belongs to the transitions.
|
|
3326
3421
|
*/
|
|
3327
3422
|
export function dailySafetyVerdict(args: string[]): void {
|
|
3328
|
-
const [workDir = '', runDate = ''] = args;
|
|
3329
|
-
const
|
|
3330
|
-
const
|
|
3331
|
-
const
|
|
3332
|
-
const
|
|
3423
|
+
const [workDir = '', runDate = '', scopeDir = ''] = args;
|
|
3424
|
+
const drafts = isDraftsScope(scopeDir);
|
|
3425
|
+
const statusFile = scopeArtifact(workDir, scopeDir, runDate, 'safety-judge_status.txt');
|
|
3426
|
+
const stampFile = scopeArtifact(workDir, scopeDir, runDate, 'safety-judge_stamp.json');
|
|
3427
|
+
const decisionFile = scopeArtifact(workDir, scopeDir, runDate, 'safety-verdict.decision');
|
|
3428
|
+
const token = scopeArtifact(workDir, scopeDir, runDate, 'safety-verdict.status');
|
|
3333
3429
|
const ADMITTED = new Set(['pass', 'escalate', 'exclude']);
|
|
3334
3430
|
timed(workDir, 'safety-verdict-check', () => {
|
|
3335
3431
|
const status = isFile(statusFile) ? readFileSync(statusFile, 'utf-8').trim() : '';
|
|
@@ -3349,8 +3445,51 @@ export function dailySafetyVerdict(args: string[]): void {
|
|
|
3349
3445
|
} else if (!ADMITTED.has(verdict)) {
|
|
3350
3446
|
refusal = `kk-daily-safety.judge.decision-refused: kk-safety-verdict [${verdict}] must admit pass|escalate|exclude, so these drafts must not publish`;
|
|
3351
3447
|
}
|
|
3448
|
+
// 0331 variants scope: an admitted outcome also writes the evidence the publish matrix
|
|
3449
|
+
// freezes into the bundle — status, the exclusion record and the sha256 of every reviewed
|
|
3450
|
+
// variant's CURRENT bytes. Recomputed here (not copied from the stamp) so bytes edited
|
|
3451
|
+
// after the gate's verdict are caught at the last instant; a mismatch is a refusal, never
|
|
3452
|
+
// silently re-bound evidence.
|
|
3453
|
+
let evidence: Record<string, unknown> | null = null;
|
|
3454
|
+
if (refusal === '' && !drafts) {
|
|
3455
|
+
const stampJson = readJson(stampFile) as {
|
|
3456
|
+
digest?: unknown;
|
|
3457
|
+
status?: unknown;
|
|
3458
|
+
excludedPlatforms?: unknown;
|
|
3459
|
+
} | null;
|
|
3460
|
+
const manifest = readManifest(workDir);
|
|
3461
|
+
const hash = createHash('sha256');
|
|
3462
|
+
const variants: Record<string, string> = {};
|
|
3463
|
+
for (const key of Object.keys(manifest.variants).sort()) {
|
|
3464
|
+
const path = join(workDir, manifest.variants[key].path);
|
|
3465
|
+
if (!isFile(path)) {
|
|
3466
|
+
refusal = `kk-daily-safety.judge.verdict-refused: registered variant file is missing: ${path} — re-run the review on a fresh manifest`;
|
|
3467
|
+
break;
|
|
3468
|
+
}
|
|
3469
|
+
const sha = createHash('sha256').update(readFileSync(path)).digest('hex');
|
|
3470
|
+
variants[key] = sha;
|
|
3471
|
+
hash.update(key).update('\0').update(readFileSync(path)).update('\0');
|
|
3472
|
+
}
|
|
3473
|
+
const digest = hash.digest('hex');
|
|
3474
|
+
if (refusal === '' && stampJson?.digest !== digest) {
|
|
3475
|
+
refusal = `kk-daily-safety.judge.verdict-refused: variant bytes changed after the review (reviewed ${String(stampJson?.digest ?? 'missing')}, on disk ${digest}) — re-run the variants safety review`;
|
|
3476
|
+
}
|
|
3477
|
+
if (refusal === '') {
|
|
3478
|
+
evidence = {
|
|
3479
|
+
status: stampJson?.status,
|
|
3480
|
+
digest,
|
|
3481
|
+
excludedPlatforms: Array.isArray(stampJson?.excludedPlatforms) ? stampJson.excludedPlatforms : [],
|
|
3482
|
+
variants,
|
|
3483
|
+
};
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3352
3486
|
if (refusal !== '') console.error(refusal);
|
|
3353
3487
|
sted('write', token, () => writeRaw(token, refusal === '' ? 'accept' : 'refuse'));
|
|
3488
|
+
if (evidence !== null) {
|
|
3489
|
+
const evidencePath = join(scopeDirOf(workDir, scopeDir), 'safety.json');
|
|
3490
|
+
sted('write', evidencePath, () => writeJsonText(evidencePath, evidence));
|
|
3491
|
+
console.log(`safety-verdict: evidence written to ${evidencePath}`);
|
|
3492
|
+
}
|
|
3354
3493
|
});
|
|
3355
3494
|
}
|
|
3356
3495
|
|
|
@@ -3361,41 +3500,187 @@ export function dailySafetyVerdict(args: string[]): void {
|
|
|
3361
3500
|
* here fails inside the loop instead of at `wrap-docs`, after the run has moved on. A broken
|
|
3362
3501
|
* revision spends the budget too: an unparsable draft must not be retried forever.
|
|
3363
3502
|
*/
|
|
3503
|
+
// ---- 0331 scope machinery -------------------------------------------------
|
|
3504
|
+
// The nested kk-daily-safety machine serves two review surfaces: the classic drafts scope
|
|
3505
|
+
// (article + VoiceScript under 24-script) and the variants scope (final publish variants under
|
|
3506
|
+
// 30-variants, registered by daily-safety-manifest, artifacts under 4-safety-variants). The scope
|
|
3507
|
+
// is ONE trailing `scope_dir` argument: `24-script` (or empty) keeps every path byte-identical to
|
|
3508
|
+
// the pre-0331 behavior, any other value re-roots the artifact set and switches the digest to the
|
|
3509
|
+
// manifest-listed variant bytes.
|
|
3510
|
+
|
|
3511
|
+
export const SAFETY_VARIANTS_DIR = '4-safety-variants';
|
|
3512
|
+
|
|
3513
|
+
function isDraftsScope(scopeDir: string): boolean {
|
|
3514
|
+
const dir = scopeDir.trim();
|
|
3515
|
+
return dir === '' || dir === '24-script';
|
|
3516
|
+
}
|
|
3517
|
+
|
|
3518
|
+
function scopeDirOf(workDir: string, scopeDir: string): string {
|
|
3519
|
+
return isDraftsScope(scopeDir) ? join(workDir, '24-script') : join(workDir, scopeDir.trim());
|
|
3520
|
+
}
|
|
3521
|
+
|
|
3522
|
+
function scopeArtifact(workDir: string, scopeDir: string, runDate: string, name: string): string {
|
|
3523
|
+
return join(scopeDirOf(workDir, scopeDir), `${runDate}_09_${name}`);
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
/** The remediation budget is per scope: a variants review never inherits the drafts pass count. */
|
|
3527
|
+
function scopeCounter(workDir: string, scopeDir: string): string {
|
|
3528
|
+
return isDraftsScope(scopeDir)
|
|
3529
|
+
? join(workDir, SAFETY_REVISE_COUNT)
|
|
3530
|
+
: join(workDir, `${SAFETY_REVISE_COUNT}-${scopeDir.trim().replace(/[^\w-]/g, '_')}`);
|
|
3531
|
+
}
|
|
3532
|
+
|
|
3533
|
+
function readScopeCounter(workDir: string, scopeDir: string): number {
|
|
3534
|
+
const path = scopeCounter(workDir, scopeDir);
|
|
3535
|
+
if (!isFile(path)) return 0;
|
|
3536
|
+
const value = Number.parseInt(readFileSync(path, 'utf-8'), 10);
|
|
3537
|
+
return Number.isNaN(value) ? 0 : value;
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
interface SafetyManifest {
|
|
3541
|
+
schemaVersion: number;
|
|
3542
|
+
runDate: string;
|
|
3543
|
+
variants: Record<string, { path: string; sha256: string }>;
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3546
|
+
function readManifest(workDir: string): SafetyManifest {
|
|
3547
|
+
const path = join(workDir, SAFETY_VARIANTS_DIR, 'manifest.json');
|
|
3548
|
+
const manifest = readJson(path) as SafetyManifest | null;
|
|
3549
|
+
if (manifest === null || !manifest.variants || typeof manifest.variants !== 'object') {
|
|
3550
|
+
failWith(
|
|
3551
|
+
1,
|
|
3552
|
+
`safety-variants: ${path} is missing or malformed — run "kk stage run daily-safety-manifest <work_dir> <run_date>" first`,
|
|
3553
|
+
);
|
|
3554
|
+
}
|
|
3555
|
+
return manifest;
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
/** Digest over the exact bytes under review: drafts = safetyDigest; variants = sorted manifest keys + current bytes. */
|
|
3559
|
+
function scopeDigest(workDir: string, scopeDir: string, runDate: string): string {
|
|
3560
|
+
if (isDraftsScope(scopeDir)) return safetyDigest(workDir, runDate);
|
|
3561
|
+
const manifest = readManifest(workDir);
|
|
3562
|
+
const hash = createHash('sha256');
|
|
3563
|
+
for (const key of Object.keys(manifest.variants).sort()) {
|
|
3564
|
+
const path = join(workDir, manifest.variants[key].path);
|
|
3565
|
+
if (!isFile(path)) {
|
|
3566
|
+
failWith(
|
|
3567
|
+
1,
|
|
3568
|
+
`safety-variants: registered variant file is missing: ${path} — re-run daily-bundle-variants and the manifest`,
|
|
3569
|
+
);
|
|
3570
|
+
}
|
|
3571
|
+
hash.update(key).update('\0').update(readFileSync(path)).update('\0');
|
|
3572
|
+
}
|
|
3573
|
+
return hash.digest('hex');
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
/** The `content` array the review request hands the agents: what to read, per scope. */
|
|
3577
|
+
function scopeContent(workDir: string, scopeDir: string, runDate: string): unknown[] {
|
|
3578
|
+
if (isDraftsScope(scopeDir)) return safetyDraftPaths(workDir, runDate);
|
|
3579
|
+
return Object.entries(readManifest(workDir).variants)
|
|
3580
|
+
.sort(([a], [b]) => (a < b ? -1 : 1))
|
|
3581
|
+
.map(([key, entry]) => ({ key, path: entry.path, sha256: entry.sha256 }));
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3584
|
+
/**
|
|
3585
|
+
* Register the run's final publish variants for the variants-scope safety review (0331): every
|
|
3586
|
+
* `30-variants/*.content.json` becomes one manifest entry keyed by its file name with `_` read as
|
|
3587
|
+
* the plugin.operation separator. The manifest is the review contract — the request digest, the
|
|
3588
|
+
* gate's edit check and the bundle's frozen evidence all resolve through it, so an empty or
|
|
3589
|
+
* missing variants directory fails here, before any reviewer spends tokens.
|
|
3590
|
+
*/
|
|
3591
|
+
export function dailySafetyManifest(args: string[]): void {
|
|
3592
|
+
const [workDir = '', runDate = ''] = args;
|
|
3593
|
+
timed(workDir, 'safety-manifest', () => {
|
|
3594
|
+
const dir = join(workDir, '30-variants');
|
|
3595
|
+
if (!existsSync(dir)) {
|
|
3596
|
+
failWith(1, `safety-manifest: ${dir} does not exist — run the bundle-variants stage first`);
|
|
3597
|
+
}
|
|
3598
|
+
const files = readdirSync(dir)
|
|
3599
|
+
.filter((file) => file.endsWith('.content.json'))
|
|
3600
|
+
.sort();
|
|
3601
|
+
if (files.length === 0) {
|
|
3602
|
+
failWith(1, `safety-manifest: no *.content.json under ${dir} — nothing to review; was the bundle built?`);
|
|
3603
|
+
}
|
|
3604
|
+
const variants: Record<string, { path: string; sha256: string }> = {};
|
|
3605
|
+
for (const file of files) {
|
|
3606
|
+
const text = readFileSync(join(dir, file), 'utf-8');
|
|
3607
|
+
variants[file.replace(/\.content\.json$/, '').replace(/_/g, ':')] = {
|
|
3608
|
+
path: `30-variants/${file}`,
|
|
3609
|
+
sha256: createHash('sha256').update(text).digest('hex'),
|
|
3610
|
+
};
|
|
3611
|
+
}
|
|
3612
|
+
mkdirSync(join(workDir, SAFETY_VARIANTS_DIR), { recursive: true });
|
|
3613
|
+
const manifestPath = join(workDir, SAFETY_VARIANTS_DIR, 'manifest.json');
|
|
3614
|
+
sted('write', manifestPath, () => writeJsonText(manifestPath, { schemaVersion: 1, runDate, variants }));
|
|
3615
|
+
console.log(`safety-manifest: ${files.length} variant(s) registered for review`);
|
|
3616
|
+
});
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3364
3619
|
/**
|
|
3365
3620
|
* Open a content-safety review of the current drafts: drop every artifact of an earlier review
|
|
3366
3621
|
* (a retry must be re-reviewed on its final version) and write the request the two agent.run
|
|
3367
3622
|
* steps read. The program passes context only — platform policy lives in the kk:content-safety
|
|
3368
|
-
* skill's reference files, never here.
|
|
3623
|
+
* skill's reference files, never here. 0331: an optional trailing scope_dir re-roots the whole
|
|
3624
|
+
* artifact set for the variants review (see the scope machinery above).
|
|
3369
3625
|
*/
|
|
3370
3626
|
export function dailySafetyPrep(args: string[]): void {
|
|
3371
|
-
const [workDir = '', runDate = '', platforms = ''] = args;
|
|
3627
|
+
const [workDir = '', runDate = '', platforms = '', scopeDir = ''] = args;
|
|
3628
|
+
const drafts = isDraftsScope(scopeDir);
|
|
3372
3629
|
timed(workDir, 'safety-review', () => {
|
|
3373
|
-
const digest =
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3630
|
+
const digest = scopeDigest(workDir, scopeDir, runDate);
|
|
3631
|
+
if (drafts) {
|
|
3632
|
+
// Structural snapshot of the VoiceScript BEFORE any remediation edit. `daily-safety-revise`
|
|
3633
|
+
// restores it when the remediation pass changes the item-section structure without a recorded
|
|
3634
|
+
// exclusion: the agent is allowed to drop a 播报形态 line, not an item the article kept,
|
|
3635
|
+
// because `daily-wrap-docs` derives the spoken item sections from `gap_ms: 1200` and fails the
|
|
3636
|
+
// run when they no longer match the episode's included items (2026-10-21 04:30 run: 3
|
|
3637
|
+
// segments removed → 5 sections vs a 6-item plan → `generate` refused at 05:03 and the whole
|
|
3638
|
+
// day published nothing).
|
|
3639
|
+
const [, scriptPath] = safetyDraftPaths(workDir, runDate);
|
|
3640
|
+
const snapshot = safetyPath(workDir, runDate, 'safety-prep_script-snapshot.yaml');
|
|
3641
|
+
if (isFile(scriptPath))
|
|
3642
|
+
sted('write', snapshot, () => writeRaw(snapshot, readFileSync(scriptPath, 'utf-8')));
|
|
3643
|
+
} else {
|
|
3644
|
+
// The remediation budget belongs to this scope's attempt, not to the day. The machine
|
|
3645
|
+
// re-enters this state once per remediation loop, so initializing unconditionally
|
|
3646
|
+
// reset the budget every cycle and the loop could never classify its residue
|
|
3647
|
+
// (20261105 dogfood: the nested variants review revised forever). Like the drafts
|
|
3648
|
+
// scope, only the FIRST entry initializes.
|
|
3649
|
+
const counter = scopeCounter(workDir, scopeDir);
|
|
3650
|
+
if (!isFile(counter)) sted('write', counter, () => writeFileSync(counter, '0\n'));
|
|
3651
|
+
}
|
|
3384
3652
|
for (const name of [
|
|
3385
3653
|
'safety-review_report.json',
|
|
3386
3654
|
'safety-judge_verdict.json',
|
|
3387
3655
|
'safety-judge_status.txt',
|
|
3388
3656
|
'safety-judge_stamp.json',
|
|
3657
|
+
'safety-verdict.decision',
|
|
3658
|
+
'safety-verdict.status',
|
|
3659
|
+
// A previous review's evidence must never survive into a new request: the bundle
|
|
3660
|
+
// freezes exactly what the current request's verdict admits. Evidence is the one
|
|
3661
|
+
// unprefixed artifact, so the runDate-prefixed sweep would miss it.
|
|
3662
|
+
'safety.json',
|
|
3389
3663
|
]) {
|
|
3390
|
-
const stale =
|
|
3664
|
+
const stale =
|
|
3665
|
+
name === 'safety.json' && !drafts
|
|
3666
|
+
? join(scopeDirOf(workDir, scopeDir), name)
|
|
3667
|
+
: scopeArtifact(workDir, scopeDir, runDate, name);
|
|
3391
3668
|
if (isFile(stale)) unlinkSync(stale);
|
|
3392
3669
|
}
|
|
3393
|
-
const request =
|
|
3670
|
+
const request = scopeArtifact(workDir, scopeDir, runDate, 'safety-review_request.json');
|
|
3394
3671
|
const targets = platforms
|
|
3395
3672
|
.split(',')
|
|
3396
3673
|
.map((platform) => platform.trim())
|
|
3397
3674
|
.filter(Boolean);
|
|
3398
|
-
sted('write', request, () =>
|
|
3675
|
+
sted('write', request, () =>
|
|
3676
|
+
writeJsonText(request, {
|
|
3677
|
+
digest,
|
|
3678
|
+
platforms: targets,
|
|
3679
|
+
mode: 'detect-only',
|
|
3680
|
+
scope: drafts ? 'drafts' : 'variants',
|
|
3681
|
+
content: scopeContent(workDir, scopeDir, runDate),
|
|
3682
|
+
}),
|
|
3683
|
+
);
|
|
3399
3684
|
});
|
|
3400
3685
|
}
|
|
3401
3686
|
|
|
@@ -3429,8 +3714,46 @@ export function dailySafetyPrep(args: string[]): void {
|
|
|
3429
3714
|
* revision spends the budget too: an unparsable draft must not be retried forever.
|
|
3430
3715
|
*/
|
|
3431
3716
|
export function dailySafetyRevise(args: string[]): void {
|
|
3432
|
-
const [workDir = '', runDate = ''] = args;
|
|
3717
|
+
const [workDir = '', runDate = '', scopeDir = ''] = args;
|
|
3718
|
+
const drafts = isDraftsScope(scopeDir);
|
|
3433
3719
|
timed(workDir, 'safety-revise', () => {
|
|
3720
|
+
if (!drafts) {
|
|
3721
|
+
// Variants scope: every manifest-listed file must stay parseable Content with the
|
|
3722
|
+
// fields the publish half reads; the structural VoiceScript machinery does not apply.
|
|
3723
|
+
// Refresh each entry's sha256 to the post-remediation bytes: the manifest is the
|
|
3724
|
+
// contract the request/digest/stamp resolve through, so leaving the pre-remediation
|
|
3725
|
+
// digests in place made every re-review report a request-integrity mismatch and the
|
|
3726
|
+
// loop spin (20261105 dogfood). Structural drift above the hash level stays owned by
|
|
3727
|
+
// daily-bundle-variants (never run here).
|
|
3728
|
+
const manifestPath = join(workDir, SAFETY_VARIANTS_DIR, 'manifest.json');
|
|
3729
|
+
const manifest = readManifest(workDir);
|
|
3730
|
+
for (const [key, entry] of Object.entries(manifest.variants)) {
|
|
3731
|
+
const path = join(workDir, entry.path);
|
|
3732
|
+
const value = requireJson(path, 'safety-revise') as {
|
|
3733
|
+
title?: unknown;
|
|
3734
|
+
body?: unknown;
|
|
3735
|
+
};
|
|
3736
|
+
for (const field of ['title', 'body'] as const) {
|
|
3737
|
+
if (typeof value[field] !== 'string' || String(value[field]).trim() === '') {
|
|
3738
|
+
failWith(1, `safety-revise: variant ${path} ${field} must be a non-empty string`);
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
entry.sha256 = createHash('sha256').update(readFileSync(path)).digest('hex');
|
|
3742
|
+
manifest.variants[key] = entry;
|
|
3743
|
+
}
|
|
3744
|
+
sted('write', manifestPath, () =>
|
|
3745
|
+
writeJsonText(manifestPath, {
|
|
3746
|
+
schemaVersion: manifest.schemaVersion,
|
|
3747
|
+
runDate: manifest.runDate,
|
|
3748
|
+
variants: manifest.variants,
|
|
3749
|
+
}),
|
|
3750
|
+
);
|
|
3751
|
+
const counter = scopeCounter(workDir, scopeDir);
|
|
3752
|
+
const next = readScopeCounter(workDir, scopeDir) + 1;
|
|
3753
|
+
sted('write', counter, () => writeFileSync(counter, `${next}\n`));
|
|
3754
|
+
console.log(`safety-revise: remediation pass ${next} applied to manifest variants; re-validated`);
|
|
3755
|
+
return;
|
|
3756
|
+
}
|
|
3434
3757
|
const [articlePath, scriptPath] = safetyDraftPaths(workDir, runDate);
|
|
3435
3758
|
const article = requireJson(articlePath, 'safety-revise');
|
|
3436
3759
|
for (const field of ['title', 'body'] as const) {
|
|
@@ -3442,7 +3765,7 @@ export function dailySafetyRevise(args: string[]): void {
|
|
|
3442
3765
|
assertScriptContent(scriptPath);
|
|
3443
3766
|
restoreStructureIfBroken(workDir, runDate, scriptPath);
|
|
3444
3767
|
const path = join(workDir, SAFETY_REVISE_COUNT);
|
|
3445
|
-
const next =
|
|
3768
|
+
const next = readScopeCounter(workDir, scopeDir) + 1;
|
|
3446
3769
|
sted('write', path, () => writeFileSync(path, `${next}\n`));
|
|
3447
3770
|
console.log(`safety-revise: remediation pass ${next} applied; drafts re-validated`);
|
|
3448
3771
|
});
|
|
@@ -3945,6 +4268,23 @@ function normalizeTranslatedLocales(zhPostPath: string, failUnsafe = true): void
|
|
|
3945
4268
|
}
|
|
3946
4269
|
}
|
|
3947
4270
|
|
|
4271
|
+
/**
|
|
4272
|
+
* The translate agents run with cwd = this repo, and both executor sandboxes (codex workspace-write,
|
|
4273
|
+
* claude acceptEdits) refuse writes into the Surfdash checkout — on 2026-11-04 both exited 0 with
|
|
4274
|
+
* en/ja untouched. So they write `<runDate>_21_translate_<locale>.md` under the work dir and this
|
|
4275
|
+
* installs each over its scaffold. The output is consumed (renamed) so a rerun never clobbers an
|
|
4276
|
+
* operator's later repair of the installed post.
|
|
4277
|
+
*/
|
|
4278
|
+
function installTranslatedLocales(workDir: string, runDate: string, zhPostPath: string): void {
|
|
4279
|
+
for (const locale of ['en', 'ja'] as const) {
|
|
4280
|
+
const translated = join(workDir, '31-publish', `${runDate}_21_translate_${locale}.md`);
|
|
4281
|
+
if (!isFile(translated)) continue;
|
|
4282
|
+
const target = zhPostPath.replace('/zh/', `/${locale}/`);
|
|
4283
|
+
sted('write', target, () => writeRaw(target, readFileSync(translated, 'utf-8')));
|
|
4284
|
+
renameSync(translated, `${translated}.installed`);
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
|
|
3948
4288
|
export function dailySurfdashRefine(args: string[]): void {
|
|
3949
4289
|
const [workDir = '', runDate = '', pluginsPath = ''] = args;
|
|
3950
4290
|
const marker = join(workDir, '31-publish', `${runDate}_20_translate-localize_zh-post-path.txt`);
|
|
@@ -3957,6 +4297,7 @@ export function dailySurfdashRefine(args: string[]): void {
|
|
|
3957
4297
|
failWith(1, `surfdash-refine: zh marker must name an existing post: ${marker}`);
|
|
3958
4298
|
}
|
|
3959
4299
|
validateLocaleMarkers(workDir, runDate, zhPostPath, 'surfdash-refine');
|
|
4300
|
+
installTranslatedLocales(workDir, runDate, zhPostPath);
|
|
3960
4301
|
// The validator is intentionally read-only. Repair known series spellings before
|
|
3961
4302
|
// site-owned refine computes managed fields, otherwise the final publisher would
|
|
3962
4303
|
// rewrite the refined bytes after the locale fingerprint was admitted.
|