@gobing-ai/knowledge-kit 0.0.29 → 0.0.30
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 +114 -38
- package/package.json +1 -1
- package/plugins/generations/daily-article-gen/dist/index.js +24 -7
- package/plugins/generations/daily-article-gen/src/index.ts +36 -12
- package/plugins/generations/dailynews-gen/dist/index.js +19 -2
- package/plugins/generations/dailynews-gen/src/index.ts +7 -0
- package/plugins/generations/dailynews-gen/src/script-plan.ts +21 -3
- package/plugins/generations/episode-plan-gen/dist/index.js +55 -4
- package/plugins/generations/episode-plan-gen/src/index.ts +80 -2
- package/plugins/generations/image-gen/dist/index.js +122 -20
- package/plugins/generations/image-gen/src/bytes.ts +106 -18
- package/plugins/generations/image-gen/src/index.ts +46 -6
- package/plugins/generations/image-gen/src/job.ts +6 -0
- package/plugins/generations/image-gen/src/providers/minimax.ts +12 -4
- package/plugins/generations/image-gen/src/providers/types.ts +12 -1
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/scripts/daily-voice-style.zh.md +5 -5
- package/plugins/kk/scripts/itc-stages.ts +21 -3
- package/plugins/kk/scripts/kk-workflow-stages.ts +199 -7
- package/plugins/kk/scripts/topic-boundary.json +3 -12
- package/plugins/kk/skills/content-judge/SKILL.md +30 -0
- package/plugins/kk/skills/content-judge/references/workflow-integration.md +9 -2
- package/plugins/kk/skills/publish/SKILL.md +1 -1
- package/plugins/kk/workflows/judge-gated-publish-example.yaml +18 -10
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +39 -14
- package/plugins/kk/workflows/kk-itc.yaml +21 -8
- package/plugins/publishings/xhs-pub/dist/index.js +1 -1
- package/plugins/publishings/xhs-pub/src/automation-guard.ts +2 -2
- package/plugins/publishings/xhs-pub/src/index.ts +1 -1
- package/plugins/publishings/xhs-pub/src/profile-check.ts +2 -2
- package/plugins/sp/lib/env.ts +0 -46
- package/plugins/sp/lib/inline-run.generated.mjs +0 -1489
- package/plugins/sp/scripts/batch-preflight.mjs +0 -346
- package/plugins/sp/scripts/batch-preflight.ts +0 -459
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +0 -615
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +0 -846
- package/plugins/sp/scripts/daily-summary/logger.ts +0 -28
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +0 -223
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +0 -367
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +0 -132
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +0 -169
- package/plugins/sp/scripts/feature-dev-precheck.mjs +0 -171
- package/plugins/sp/scripts/feature-dev-precheck.ts +0 -238
- package/plugins/sp/scripts/feature-sync-bounded.mjs +0 -285
- package/plugins/sp/scripts/feature-sync-bounded.ts +0 -478
- package/plugins/sp/scripts/feature-verification-steps.ts +0 -275
- package/plugins/sp/scripts/history-anatomy-cache.mjs +0 -902
- package/plugins/sp/scripts/history-anatomy-cache.ts +0 -1028
- package/plugins/sp/scripts/idea-handoff.mjs +0 -22
- package/plugins/sp/scripts/idea-handoff.ts +0 -44
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +0 -185
- package/plugins/sp/scripts/inline-run-setup.ts +0 -722
- package/plugins/sp/scripts/pr-reviewing.mjs +0 -769
- package/plugins/sp/scripts/pr-reviewing.ts +0 -925
- package/plugins/sp/scripts/quality-gate.mjs +0 -179
- package/plugins/sp/scripts/quality-gate.ts +0 -217
- package/plugins/sp/scripts/script-contract-check.ts +0 -319
- package/plugins/sp/scripts/stage-registry-adapter.ts +0 -1533
- package/plugins/sp/scripts/surface-drift-inventory.ts +0 -929
- package/plugins/sp/scripts/task-evidence-precheck.ts +0 -181
- package/plugins/sp/scripts/task-size-precheck.ts +0 -175
- package/plugins/sp/scripts/transition-shim-check.ts +0 -238
- package/plugins/sp/scripts/validate-commands.ts +0 -689
- package/plugins/sp/scripts/validate-flag-contracts.ts +0 -878
- package/plugins/sp/scripts/verify-answer-lint.ts +0 -530
- package/plugins/sp/scripts/workflow-step-profile.mjs +0 -316
- package/plugins/sp/scripts/workflow-step-profile.ts +0 -456
- package/plugins/sp/scripts/wrapup-drift-probe.mjs +0 -190
- package/plugins/sp/scripts/wrapup-drift-probe.ts +0 -258
- package/plugins/sp/scripts/wrapup-steps.mjs +0 -444
- package/plugins/sp/scripts/wrapup-steps.ts +0 -552
package/plugins/kk/plugin.json
CHANGED
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
|
|
26
26
|
## 段落角色
|
|
27
27
|
|
|
28
|
-
- `intro`:开场导语,串起今天几个主题,接在固定问候之后,不超过
|
|
29
|
-
- `lead`:一句话口语桥段,引出下一个主题的【标题】;只做铺垫,不得复述标题本身,不超过
|
|
30
|
-
- `insight
|
|
31
|
-
- `outro`:简短收束语,接在固定致谢之前,不超过
|
|
28
|
+
- `intro`:开场导语,串起今天几个主题,接在固定问候之后,不超过 150 字。
|
|
29
|
+
- `lead`:一句话口语桥段,引出下一个主题的【标题】;只做铺垫,不得复述标题本身,不超过 80 字。
|
|
30
|
+
- `insight`:只基于这一条自身可核事实的点评(3–6 句),展开这一条自身的机制→意义→实用角度;不得挪用其他条目的话题,不超过 380 字。时长契约:全片按 15–20 分钟口播设计,insight 应写到接近上限——宁可深挖这一条的事实,也不要浅尝辄止。字数按配音驱动折算:gemini ≈3.3 字/秒(正文约 4300–5800 字);Voicebox robin-news ≈6.3 字/秒(正文约 5700–7600 字,或增加条目数)。
|
|
31
|
+
- `outro`:简短收束语,接在固定致谢之前,不超过 150 字。
|
|
32
32
|
|
|
33
33
|
## 机器可读契约
|
|
34
34
|
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
|
|
40
40
|
```json
|
|
41
41
|
{
|
|
42
|
-
"caps": { "intro":
|
|
42
|
+
"caps": { "intro": 150, "outro": 150, "lead": 80, "insight": 380 },
|
|
43
43
|
"banned": [
|
|
44
44
|
"在各种技术光环背后,正是这样专注而不知疲倦的底层攻坚者,为今天整个大模型生态奠定了最坚实的技术基石。",
|
|
45
45
|
"这其实释放了一个非常明确的信号:AI竞争的下半场,已经不再只是单纯拼模型参数,更是电力、能源和全栈工业基础设施的硬核较量。",
|
|
@@ -131,7 +131,18 @@ export function itcReport(args: string[]): void {
|
|
|
131
131
|
const [workDir = '', maxRevisions = ''] = args;
|
|
132
132
|
if (!workDir) fail('usage: itc-report <work_dir> [max_revisions]');
|
|
133
133
|
const verdictPath = join(workDir, '.itc-verdict.json');
|
|
134
|
-
const
|
|
134
|
+
const verdictJson = existsSync(verdictPath)
|
|
135
|
+
? (readJson(verdictPath) as { verdict?: unknown; confidence?: unknown })
|
|
136
|
+
: {};
|
|
137
|
+
const verdict = verdictJson.verdict ?? '';
|
|
138
|
+
// Same enum as the workflow guards and `daily-safety-gate`: a PASS without a HIGH/MEDIUM/LOW
|
|
139
|
+
// confidence never cleared the judge gate, so the report must not claim a publish-ready draft.
|
|
140
|
+
// An absent verdict file stays the dry-run PASS convention (verdict '').
|
|
141
|
+
const unusablePass =
|
|
142
|
+
verdict === 'PASS' &&
|
|
143
|
+
verdictJson.confidence !== 'HIGH' &&
|
|
144
|
+
verdictJson.confidence !== 'MEDIUM' &&
|
|
145
|
+
verdictJson.confidence !== 'LOW';
|
|
135
146
|
// Auto runs count on .auto-revise-count; interactive runs keep the existing .revise-count.
|
|
136
147
|
const revisions = readCount(join(workDir, '.auto-revise-count')) ?? readCount(join(workDir, '.revise-count')) ?? 0;
|
|
137
148
|
// Exhaustion only fails the run when the last verdict still asks for another revision —
|
|
@@ -186,7 +197,8 @@ export function itcReport(args: string[]): void {
|
|
|
186
197
|
}
|
|
187
198
|
|
|
188
199
|
const publishFailed = targets.some((t) => !t.ok);
|
|
189
|
-
const outcome =
|
|
200
|
+
const outcome =
|
|
201
|
+
verdict === 'FAIL' || unusablePass || exhausted || publishFailed ? 'failed' : declined ? 'declined' : 'done';
|
|
190
202
|
const reportDir = join(workDir, '5-report');
|
|
191
203
|
const jsonPath = join(reportDir, 'report.json');
|
|
192
204
|
const publish = hasPlan ? { targets, retryCommands } : undefined;
|
|
@@ -198,7 +210,13 @@ export function itcReport(args: string[]): void {
|
|
|
198
210
|
);
|
|
199
211
|
writeFileSync(
|
|
200
212
|
join(reportDir, 'report.md'),
|
|
201
|
-
`# kk-itc run report\n\n- Outcome: ${outcome}\n- Verdict: ${
|
|
213
|
+
`# kk-itc run report\n\n- Outcome: ${outcome}\n- Verdict: ${
|
|
214
|
+
verdict === ''
|
|
215
|
+
? 'PASS (empty)'
|
|
216
|
+
: unusablePass
|
|
217
|
+
? 'PASS (no HIGH/MEDIUM/LOW confidence — the judge gate never cleared)'
|
|
218
|
+
: verdict
|
|
219
|
+
}\n- Revisions: ${revisions}\n` +
|
|
202
220
|
(declined
|
|
203
221
|
? '\n## Publish\n\n- Declined by the operator — nothing was sent.\n'
|
|
204
222
|
: targets.length > 0
|
|
@@ -840,6 +840,7 @@ function dailyQcContent(args: string[]): void {
|
|
|
840
840
|
minImportance = '',
|
|
841
841
|
minUrgency = '',
|
|
842
842
|
minImpact = '',
|
|
843
|
+
minEpisodeItems = '',
|
|
843
844
|
] = args;
|
|
844
845
|
const candidates = join(workDir, '21-plan', `${runDate}_04_quality-control-content_candidates.json`);
|
|
845
846
|
const annotated = join(workDir, '21-plan', `${runDate}_04_quality-control-content_annotated.json`);
|
|
@@ -870,6 +871,7 @@ function dailyQcContent(args: string[]): void {
|
|
|
870
871
|
QC_MIN_IMPORTANCE: minImportance,
|
|
871
872
|
QC_MIN_URGENCY: minUrgency,
|
|
872
873
|
QC_MIN_IMPACT: minImpact,
|
|
874
|
+
QC_MIN_EPISODE_ITEMS: minEpisodeItems,
|
|
873
875
|
},
|
|
874
876
|
},
|
|
875
877
|
);
|
|
@@ -1524,16 +1526,21 @@ function safetyPath(workDir: string, runDate: string, name: string): string {
|
|
|
1524
1526
|
*/
|
|
1525
1527
|
function safetyDigest(workDir: string, runDate: string): string {
|
|
1526
1528
|
const hash = createHash('sha256');
|
|
1527
|
-
for (const path of
|
|
1528
|
-
join(workDir, '22-article', `${runDate}_06_article_content.json`),
|
|
1529
|
-
join(workDir, '24-script', `${runDate}_08_script_voicescript.yaml`),
|
|
1530
|
-
]) {
|
|
1529
|
+
for (const path of safetyDraftPaths(workDir, runDate)) {
|
|
1531
1530
|
if (!isFile(path)) failWith(1, `safety-review: required draft not found: ${path}`);
|
|
1532
1531
|
hash.update(readFileSync(path)).update('\0');
|
|
1533
1532
|
}
|
|
1534
1533
|
return hash.digest('hex');
|
|
1535
1534
|
}
|
|
1536
1535
|
|
|
1536
|
+
/** The drafts under review — the digest's bytes, and the quarantine set on a quality block. */
|
|
1537
|
+
function safetyDraftPaths(workDir: string, runDate: string): [string, string] {
|
|
1538
|
+
return [
|
|
1539
|
+
join(workDir, '22-article', `${runDate}_06_article_content.json`),
|
|
1540
|
+
join(workDir, '24-script', `${runDate}_08_script_voicescript.yaml`),
|
|
1541
|
+
];
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1537
1544
|
/**
|
|
1538
1545
|
* Open a content-safety review of the current drafts: drop every artifact of an earlier review
|
|
1539
1546
|
* (a retry must be re-reviewed on its final version) and write the request the two agent.run
|
|
@@ -1581,17 +1588,39 @@ function dailySafetyGate(args: string[]): void {
|
|
|
1581
1588
|
const digest = safetyDigest(workDir, runDate);
|
|
1582
1589
|
let status = 'block';
|
|
1583
1590
|
let reason = 'verdict PASS on unchanged drafts, no BLOCK/RISK';
|
|
1584
|
-
|
|
1585
|
-
|
|
1591
|
+
// A digest-mismatch block means the drafts were deliberately edited (HITL): keep them so
|
|
1592
|
+
// a re-run of safety-prep re-reviews the edited version. Any other block means the
|
|
1593
|
+
// content itself is rejected — quarantine the drafts below.
|
|
1594
|
+
let edited = false;
|
|
1595
|
+
if (request?.digest !== digest) {
|
|
1596
|
+
reason = 'drafts changed after the review was requested';
|
|
1597
|
+
edited = true;
|
|
1598
|
+
} else if (decisions.length === 0 || decisions.some((d) => d !== 'OK' && d !== 'RISK' && d !== 'BLOCK'))
|
|
1586
1599
|
reason = 'content-safety report missing or malformed';
|
|
1587
1600
|
else if (decisions.includes('BLOCK')) reason = 'content-safety report carries a BLOCK decision';
|
|
1588
1601
|
else if (verdict?.verdict !== 'PASS')
|
|
1589
1602
|
reason = `content-judge verdict is ${typeof verdict?.verdict === 'string' ? verdict.verdict : 'missing or unreadable'}`;
|
|
1603
|
+
else if (verdict?.confidence !== 'HIGH' && verdict?.confidence !== 'MEDIUM' && verdict?.confidence !== 'LOW')
|
|
1604
|
+
reason = 'content-judge verdict is missing a HIGH/MEDIUM/LOW confidence level';
|
|
1590
1605
|
else if (decisions.includes('RISK') || report?.escalate === true) {
|
|
1591
1606
|
status = 'escalate';
|
|
1592
1607
|
reason = 'RISK decision or escalation — human review required';
|
|
1593
1608
|
} else status = 'pass';
|
|
1594
1609
|
if (status !== 'block') sted('write', stampFile, () => writeJsonText(stampFile, { digest, status }));
|
|
1610
|
+
if (status === 'block' && !edited) {
|
|
1611
|
+
// A quality block poisons the isFile() draft cache: every same-day retry would
|
|
1612
|
+
// re-fail on unchanged bytes (20261002 dogfood — two judge-fail runs replayed until
|
|
1613
|
+
// the drafts were moved by hand). Quarantine (mv, never rm) so the next run
|
|
1614
|
+
// regenerates from scratch; the 09_* verdict artifacts stay in 24-script for audit.
|
|
1615
|
+
const q = join(workDir, '24-script', `.quarantine-${runDate}-judge-block`);
|
|
1616
|
+
mkdirSync(q, { recursive: true });
|
|
1617
|
+
for (const path of safetyDraftPaths(workDir, runDate)) {
|
|
1618
|
+
if (!isFile(path)) continue;
|
|
1619
|
+
let dest = join(q, basename(path));
|
|
1620
|
+
for (let n = 2; isFile(dest); n += 1) dest = join(q, `${n}-${basename(path)}`);
|
|
1621
|
+
renameSync(path, dest);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1595
1624
|
sted('write', statusFile, () => writeRaw(statusFile, status));
|
|
1596
1625
|
console.log(`safety: status=${status} (${reason}); routing is transition-owned`);
|
|
1597
1626
|
});
|
|
@@ -1657,6 +1686,54 @@ function dailyGenerate(args: string[]): void {
|
|
|
1657
1686
|
* `intro_music` empty -> skip. Writes `<stem>.intro.wav` (original render kept), points
|
|
1658
1687
|
* `audioPath` at the merged file, extends `duration`, marks `introMerged` (idempotent).
|
|
1659
1688
|
*/
|
|
1689
|
+
/**
|
|
1690
|
+
* Episode duration gate (standing requirement, 20261002): a daily episode must run 15–20 minutes
|
|
1691
|
+
* (duration is authoritative; the 8–10-item figure is only an estimate). Reads the final audio
|
|
1692
|
+
* content's `metadata.duration` — after the intro sting merged, this is the true episode length —
|
|
1693
|
+
* and fails loud outside `[min, max]` seconds-worth of minutes. A missing duration is a failure,
|
|
1694
|
+
* not a pass: the gate must never silently let an under-length episode through again.
|
|
1695
|
+
*/
|
|
1696
|
+
function dailyEpisodeDuration(args: string[]): void {
|
|
1697
|
+
const [workDir = '', runDate = '', minArg = '', maxArg = ''] = args;
|
|
1698
|
+
if (!workDir) fail('missing work_dir');
|
|
1699
|
+
if (!runDate) fail('missing run_date');
|
|
1700
|
+
const minMinutes = Number(minArg);
|
|
1701
|
+
const maxMinutes = Number(maxArg);
|
|
1702
|
+
if (!Number.isFinite(minMinutes) || minMinutes <= 0) fail(`invalid min_minutes: ${minArg || '(empty)'}`);
|
|
1703
|
+
if (!Number.isFinite(maxMinutes) || maxMinutes < minMinutes) fail(`invalid max_minutes: ${maxArg || '(empty)'}`);
|
|
1704
|
+
const contentPath = join(workDir, '30-audio', `${runDate}_11_generate_content.json`);
|
|
1705
|
+
if (!isFile(contentPath)) failWith(1, `episode-duration: audio content not found: ${contentPath}`);
|
|
1706
|
+
let raw: unknown;
|
|
1707
|
+
try {
|
|
1708
|
+
const parsed = JSON.parse(readFileSync(contentPath, 'utf-8')) as { metadata?: { duration?: unknown } };
|
|
1709
|
+
raw = parsed.metadata?.duration;
|
|
1710
|
+
} catch (error) {
|
|
1711
|
+
if ((error as { code?: string }).code !== undefined) {
|
|
1712
|
+
sted('read', contentPath, () => readFileSync(contentPath, 'utf-8'));
|
|
1713
|
+
}
|
|
1714
|
+
raw = undefined;
|
|
1715
|
+
}
|
|
1716
|
+
const seconds = typeof raw === 'number' ? raw : typeof raw === 'string' && raw.trim() !== '' ? Number(raw) : NaN;
|
|
1717
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
1718
|
+
failWith(1, `episode-duration: audio metadata.duration missing or not a number in ${contentPath}`);
|
|
1719
|
+
}
|
|
1720
|
+
const minutes = seconds / 60;
|
|
1721
|
+
const shown = minutes.toFixed(1);
|
|
1722
|
+
if (minutes < minMinutes) {
|
|
1723
|
+
failWith(
|
|
1724
|
+
1,
|
|
1725
|
+
`episode-duration: episode runs ${shown} min, below the ${minMinutes}–${maxMinutes} min requirement (duration is authoritative). The candidate pool is too thin — deepen per-item treatment or add items; do not ship a short episode silently.`,
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
if (minutes > maxMinutes) {
|
|
1729
|
+
failWith(
|
|
1730
|
+
1,
|
|
1731
|
+
`episode-duration: episode runs ${shown} min, above the ${minMinutes}–${maxMinutes} min requirement. Trim items or per-item treatment.`,
|
|
1732
|
+
);
|
|
1733
|
+
}
|
|
1734
|
+
console.log(`episode-duration ok: ${shown} min within ${minMinutes}–${maxMinutes} min`);
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1660
1737
|
function dailyIntro(args: string[]): void {
|
|
1661
1738
|
const [workDir = '', runDate = '', introSrc = ''] = args;
|
|
1662
1739
|
if (!introSrc || introSrc.trim() === '') {
|
|
@@ -1928,8 +2005,47 @@ function dailyShowNotes(args: string[]): void {
|
|
|
1928
2005
|
timed(workDir, 'show-notes', () => {
|
|
1929
2006
|
const effective = readJson(article)?.title;
|
|
1930
2007
|
const titleArg = title !== '' ? title : typeof effective === 'string' ? effective : '';
|
|
2008
|
+
// Boundary-excluded docs must not render into show notes (judge finding 2026-10-02): the
|
|
2009
|
+
// article body's 未写入 note names the dropped ids; filter the plan copy podcast-pub sees.
|
|
2010
|
+
// Inlined parse — this file must not import @gobing-ai/* (header contract); the same note
|
|
2011
|
+
// grammar lives in daily-article-gen's excludedIdsFromNote.
|
|
1931
2012
|
const postPath = firstPostPath(readJson(result), 'surfdash-pub');
|
|
1932
|
-
|
|
2013
|
+
const articleBody = readJson(article)?.body;
|
|
2014
|
+
const excluded = new Set<string>();
|
|
2015
|
+
if (typeof articleBody === 'string') {
|
|
2016
|
+
for (const note of articleBody.matchAll(/未写入[::](.*)/g)) {
|
|
2017
|
+
for (const id of note[1].matchAll(/(?:[a-zA-Z0-9_-]+:)?[0-9a-f]{16}/g)) {
|
|
2018
|
+
excluded.add(id[0]);
|
|
2019
|
+
const colon = id[0].indexOf(':');
|
|
2020
|
+
if (colon !== -1) excluded.add(id[0].slice(colon + 1));
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
let planIn = plan;
|
|
2025
|
+
if (excluded.size > 0) {
|
|
2026
|
+
const parsed: unknown = readJson(plan);
|
|
2027
|
+
if (Array.isArray(parsed)) {
|
|
2028
|
+
const kept = parsed.filter(
|
|
2029
|
+
(d) =>
|
|
2030
|
+
!(
|
|
2031
|
+
d !== null &&
|
|
2032
|
+
typeof d === 'object' &&
|
|
2033
|
+
(excluded.has(String((d as Record<string, unknown>).id)) ||
|
|
2034
|
+
(String((d as Record<string, unknown>).id).includes(':') &&
|
|
2035
|
+
excluded.has(
|
|
2036
|
+
String((d as Record<string, unknown>).id).slice(
|
|
2037
|
+
String((d as Record<string, unknown>).id).indexOf(':') + 1,
|
|
2038
|
+
),
|
|
2039
|
+
)))
|
|
2040
|
+
),
|
|
2041
|
+
);
|
|
2042
|
+
if (kept.length !== parsed.length) {
|
|
2043
|
+
planIn = plan.replace(/\.json$/, '-shownotes.json');
|
|
2044
|
+
writeJsonText(planIn, kept);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
kk(['executor', 'run', 'podcast-pub', '--in', planIn, '--out', out, ...pluginsArgs(pluginsPath)], {
|
|
1933
2049
|
env: {
|
|
1934
2050
|
PODCAST_PUB_TITLE: titleArg,
|
|
1935
2051
|
PODCAST_PUB_RUN_DATE: runDate,
|
|
@@ -2018,7 +2134,22 @@ function dailyPublishClassify(args: string[]): void {
|
|
|
2018
2134
|
const status = okCount > 0 && okCount === total ? 'full' : okCount > 0 ? 'partial' : 'none';
|
|
2019
2135
|
sted('write', result, () => writeJqText(result, { ok: okCount === total && total > 0, targets }));
|
|
2020
2136
|
sted('write', statusFile, () => writeRaw(statusFile, status));
|
|
2137
|
+
const completeness = publishCompleteness(workDir, runDate);
|
|
2138
|
+
const completenessFile = join(workDir, '31-publish', `${runDate}_17_publish_completeness.json`);
|
|
2139
|
+
sted('write', completenessFile, () => writeJqText(completenessFile, { runDate, ...completeness }));
|
|
2021
2140
|
console.log(`publish: status=${status} (${okCount}/${total} targets published); routing is transition-owned`);
|
|
2141
|
+
console.log(`publish-completeness: ${completeness.rows.map((row) => `${row.channel}=${row.state}`).join(' ')}`);
|
|
2142
|
+
if (completeness.missing.length > 0) {
|
|
2143
|
+
const details = completeness.rows
|
|
2144
|
+
.filter((row) => row.state !== 'done')
|
|
2145
|
+
.map((row) => `${row.channel}=${row.state}${row.detail === '' ? '' : ` (${row.detail})`}`)
|
|
2146
|
+
.join('; ');
|
|
2147
|
+
console.error(
|
|
2148
|
+
`WARNING: publish completeness — ${completeness.missing.join(', ')} did not complete: ${details}. ` +
|
|
2149
|
+
'Independent channels never enter publish_status; re-run their stages for this work dir (see ' +
|
|
2150
|
+
`${completenessFile})`,
|
|
2151
|
+
);
|
|
2152
|
+
}
|
|
2022
2153
|
});
|
|
2023
2154
|
}
|
|
2024
2155
|
|
|
@@ -2029,6 +2160,59 @@ function targetsOf(path: string): Record<string, unknown>[] {
|
|
|
2029
2160
|
: [];
|
|
2030
2161
|
}
|
|
2031
2162
|
|
|
2163
|
+
/**
|
|
2164
|
+
* The publish tail's independent channels (locales, XHS, WeChat) and the surfdash CMS re-sync never
|
|
2165
|
+
* enter `publish_status` — they are non-blocking by design (ADR-022/025/026). That is also why a
|
|
2166
|
+
* tail driven by hand can skip every one of them and still leave a `full` status file behind:
|
|
2167
|
+
* 20261002 shipped zh-only surfdash with no XHS and no WeChat draft, and nothing in the run's own
|
|
2168
|
+
* artifacts said so. Record each channel's state beside the status file so the gap is visible.
|
|
2169
|
+
*/
|
|
2170
|
+
function publishCompleteness(
|
|
2171
|
+
workDir: string,
|
|
2172
|
+
runDate: string,
|
|
2173
|
+
): { rows: { channel: string; state: string; detail: string }[]; missing: string[] } {
|
|
2174
|
+
const at = (name: string): string => join(workDir, '31-publish', `${runDate}_${name}`);
|
|
2175
|
+
const markerFilled = (path: string): boolean => isFile(path) && readFileSync(path, 'utf-8').trim() !== '';
|
|
2176
|
+
const rows: { channel: string; state: string; detail: string }[] = [];
|
|
2177
|
+
const localesDone = ['en', 'ja'].every((locale) =>
|
|
2178
|
+
markerFilled(at(`20_translate-localize_${locale}-post-path.txt`)),
|
|
2179
|
+
);
|
|
2180
|
+
rows.push({
|
|
2181
|
+
channel: 'locales',
|
|
2182
|
+
state: localesDone ? 'done' : 'not-run',
|
|
2183
|
+
detail: localesDone ? 'en/ja drafts scaffolded' : 'translate markers absent (en/ja never scaffolded)',
|
|
2184
|
+
});
|
|
2185
|
+
const resultChannels: [string, string, string][] = [
|
|
2186
|
+
['xhs', '19_publish-xhs_result.json', ''],
|
|
2187
|
+
['wechat', '20_publish-wechat_result.json', ''],
|
|
2188
|
+
['cms-resync', '23_patch-surfdash_sync-result.json', '23_patch-surfdash_emdash-resync.txt'],
|
|
2189
|
+
];
|
|
2190
|
+
for (const [channel, resultName, skipMarkerName] of resultChannels) {
|
|
2191
|
+
const result = isFile(at(resultName)) ? readJson(at(resultName)) : null;
|
|
2192
|
+
if (result !== null) {
|
|
2193
|
+
const metadata = (result.metadata ?? {}) as Record<string, unknown>;
|
|
2194
|
+
rows.push({
|
|
2195
|
+
channel,
|
|
2196
|
+
state: result.ok === true ? 'done' : 'failed',
|
|
2197
|
+
detail:
|
|
2198
|
+
result.ok === true
|
|
2199
|
+
? typeof metadata.publishStatus === 'string'
|
|
2200
|
+
? metadata.publishStatus
|
|
2201
|
+
: 'ok'
|
|
2202
|
+
: String(result.error ?? 'ok=false'),
|
|
2203
|
+
});
|
|
2204
|
+
continue;
|
|
2205
|
+
}
|
|
2206
|
+
const skipMarker = skipMarkerName === '' ? '' : at(skipMarkerName);
|
|
2207
|
+
rows.push(
|
|
2208
|
+
skipMarker !== '' && isFile(skipMarker)
|
|
2209
|
+
? { channel, state: 'skipped', detail: readFileSync(skipMarker, 'utf-8').trim() }
|
|
2210
|
+
: { channel, state: 'not-run', detail: 'no result artifact' },
|
|
2211
|
+
);
|
|
2212
|
+
}
|
|
2213
|
+
return { rows, missing: rows.filter((row) => row.state !== 'done').map((row) => row.channel) };
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2032
2216
|
function dailyPublishPartial(args: string[]): void {
|
|
2033
2217
|
const [workDir = '', runDate = ''] = args;
|
|
2034
2218
|
const result = join(workDir, '31-publish', `${runDate}_17_publish_result.json`);
|
|
@@ -2168,6 +2352,11 @@ function dailyPatchSurfdash(args: string[]): void {
|
|
|
2168
2352
|
// changed===0 early-return so re-runs heal rows an earlier commit left stale
|
|
2169
2353
|
// (20260925/20260929). Fixture tests pass no plugins-path and must not shell out.
|
|
2170
2354
|
if (pluginsPath === '') {
|
|
2355
|
+
// A missing plugins-path silently drops the CMS re-sync, and the CMS cards then keep the
|
|
2356
|
+
// pre-patch `./assets/cover.png` the deployed worker cannot serve (20261002 lost its
|
|
2357
|
+
// cover this way). Leave a marker for the classify step's completeness report.
|
|
2358
|
+
const skipMarker = join(workDir, '31-publish', `${runDate}_23_patch-surfdash_emdash-resync.txt`);
|
|
2359
|
+
sted('write', skipMarker, () => writeRaw(skipMarker, 'skipped: no plugins-path\n'));
|
|
2171
2360
|
console.log('patch-surfdash: no plugins-path — skip emdash re-sync');
|
|
2172
2361
|
} else {
|
|
2173
2362
|
const syncIn = join(workDir, '31-publish', `${runDate}_23_patch-surfdash_sync.json`);
|
|
@@ -2460,6 +2649,9 @@ if (import.meta.main)
|
|
|
2460
2649
|
case 'daily-intro':
|
|
2461
2650
|
dailyIntro(rest);
|
|
2462
2651
|
break;
|
|
2652
|
+
case 'daily-episode-duration':
|
|
2653
|
+
dailyEpisodeDuration(rest);
|
|
2654
|
+
break;
|
|
2463
2655
|
case 'daily-quality-report':
|
|
2464
2656
|
dailyQualityReport(rest);
|
|
2465
2657
|
break;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"note": "Task 0189 R2 topic admission for the AI 乱炖 column
|
|
3
|
+
"note": "Task 0189 R2 topic admission for the AI 乱炖 column, with the 2026-10-02 operator scope correction: the political-diplomatic rule is China-only (中国政治/外交主线). Foreign public-affairs items are NOT floor-excluded — they flow to the plan and are handled by the article-write/script-write/cover 选题边界 prompts (skip + note / plan `excluded`) and the content-safety review. Excluded items are recorded with id, rule, reason and the matched keyword. Sources are never deleted or modified. Scan covers the title plus the first scanChars chars of the body. Precision first: these keywords are a floor, not a sieve — a rare false negative is caught by the prompt-layer boundaries and the content-safety skill review, and a suspected false positive is restored by hand with a recorded reason (manual review), never silently.",
|
|
4
4
|
"scanChars": 200,
|
|
5
5
|
"rules": [
|
|
6
6
|
{
|
|
7
7
|
"id": "political-diplomatic",
|
|
8
|
-
"reason": "
|
|
8
|
+
"reason": "中国政治/外交主线:涉及中国党政机构、领导人、外交部门或台海/中美关系的条目,不属于 AI 乱炖的技术分享范围(0189 R2;2026-10-02 操作者裁定本规则仅限中国,外国政治条目由选题边界提示与内容安全评审处理)。",
|
|
9
9
|
"keywords": [
|
|
10
10
|
"领导人",
|
|
11
11
|
"主席访",
|
|
@@ -16,18 +16,9 @@
|
|
|
16
16
|
"国事访问",
|
|
17
17
|
"政治局",
|
|
18
18
|
"中南海",
|
|
19
|
-
"总统",
|
|
20
|
-
"白宫",
|
|
21
|
-
"国会",
|
|
22
|
-
"议会",
|
|
23
|
-
"选举",
|
|
24
19
|
"台海",
|
|
25
20
|
"两岸关系",
|
|
26
|
-
"中美关系"
|
|
27
|
-
"president",
|
|
28
|
-
"parliament",
|
|
29
|
-
"election",
|
|
30
|
-
"diplomat"
|
|
21
|
+
"中美关系"
|
|
31
22
|
]
|
|
32
23
|
},
|
|
33
24
|
{
|
|
@@ -30,6 +30,7 @@ Every evaluation writes a JSON verdict file. The default path is
|
|
|
30
30
|
```json
|
|
31
31
|
{
|
|
32
32
|
"verdict": "PASS | FAIL | NEEDS_REVISION",
|
|
33
|
+
"confidence": "HIGH | MEDIUM | LOW",
|
|
33
34
|
"rubric": "<rubric-id>",
|
|
34
35
|
"score": 0.0,
|
|
35
36
|
"summary": "<one-paragraph reasoning summary>",
|
|
@@ -57,6 +58,35 @@ Every evaluation writes a JSON verdict file. The default path is
|
|
|
57
58
|
`PASS` is the only verdict that unblocks publish. The shell guard treats `FAIL` and
|
|
58
59
|
`NEEDS_REVISION` identically (exit 1); the distinction is for the feedback loop, not the gate.
|
|
59
60
|
|
|
61
|
+
### Confidence level
|
|
62
|
+
|
|
63
|
+
Every verdict carries a `confidence` level — `HIGH`, `MEDIUM`, or `LOW` — self-assessed by the
|
|
64
|
+
judge from evidence coverage:
|
|
65
|
+
|
|
66
|
+
- `HIGH` — every criterion grounded in directly verified evidence (sources opened, claims
|
|
67
|
+
cross-checked); no unverified rule text or assumption entered the reasoning.
|
|
68
|
+
- `MEDIUM` — verdict grounded but with known evidence gaps (e.g. unverified platform rule text,
|
|
69
|
+
single-source claims), none strong enough to flip the verdict.
|
|
70
|
+
- `LOW` — verdict rests on assumptions or unverified evidence that could plausibly flip it.
|
|
71
|
+
|
|
72
|
+
Every judge-verdict **consumer** enforces the enum, so a `PASS` whose `confidence` is missing or
|
|
73
|
+
outside `HIGH|MEDIUM|LOW` is not a usable verdict anywhere:
|
|
74
|
+
|
|
75
|
+
- `kk stage daily-safety-gate` blocks the run (`missing a HIGH/MEDIUM/LOW confidence level`).
|
|
76
|
+
- The spur workflow gates (`kk-itc`, `judge-gated-publish-example`) read the verdict through a
|
|
77
|
+
confidence-aware `jq` filter that turns such a `PASS` into an out-of-contract value (`PASS!`),
|
|
78
|
+
which no accept guard matches and the state machine's catch-all routes to `failed` — the import
|
|
79
|
+
neither publishes nor deadlocks.
|
|
80
|
+
- `kk stage itc-report` reports `outcome: failed` (and says why in `report.md`) for a `PASS` without
|
|
81
|
+
a valid confidence; an absent verdict file keeps the dry-run convention.
|
|
82
|
+
|
|
83
|
+
Add that filter to any new judge gate; `references/workflow-integration.md` carries the pattern and
|
|
84
|
+
`plugins/kk/tests/verdict-confidence.test.ts` fails when a workflow accepts a judge `PASS` without
|
|
85
|
+
checking the enum.
|
|
86
|
+
|
|
87
|
+
The level itself does not change routing — escalation stays owned by the review's `RISK` decisions
|
|
88
|
+
and `escalate` flag; treat `LOW` as a signal to demand human review before publishing.
|
|
89
|
+
|
|
60
90
|
## Evaluation procedure
|
|
61
91
|
|
|
62
92
|
1. **Read the Content** from the path given in the evaluation request (the `content.json` written
|
|
@@ -33,7 +33,10 @@ if [ ! -f "$verdict_path" ]; then
|
|
|
33
33
|
echo "judge: verdict file not found at $verdict_path"
|
|
34
34
|
exit 1
|
|
35
35
|
fi
|
|
36
|
-
|
|
36
|
+
# Confidence-aware read: a PASS without a HIGH|MEDIUM|LOW confidence becomes `PASS!`, which no
|
|
37
|
+
# accept guard matches and the catch-all below rejects. Keep this filter byte-identical wherever a
|
|
38
|
+
# guard reads a judge verdict (`plugins/kk/tests/verdict-confidence.test.ts` enforces that).
|
|
39
|
+
verdict=$(jq -r 'if (.verdict // "") == "PASS" then (.verdict + (if ((.confidence // "") | test("^(HIGH|MEDIUM|LOW)$")) then "" else "!" end)) else (.verdict // "") end' "$verdict_path")
|
|
37
40
|
case "$verdict" in
|
|
38
41
|
PASS)
|
|
39
42
|
echo "judge: PASS"
|
|
@@ -45,7 +48,7 @@ case "$verdict" in
|
|
|
45
48
|
exit 1
|
|
46
49
|
;;
|
|
47
50
|
*)
|
|
48
|
-
echo "judge: unrecognized verdict '$verdict' in $verdict_path"
|
|
51
|
+
echo "judge: unrecognized verdict or a PASS without a valid confidence ('$verdict') in $verdict_path"
|
|
49
52
|
exit 1
|
|
50
53
|
;;
|
|
51
54
|
esac
|
|
@@ -57,6 +60,10 @@ esac
|
|
|
57
60
|
2. **Unrecognized value → block.** Only `PASS` unblocks; anything else exits 1.
|
|
58
61
|
3. **`FAIL` and `NEEDS_REVISION` both block** — the distinction is for the feedback loop, not the
|
|
59
62
|
gate. The workflow topology decides whether to retry (NEEDS_REVISION) or halt (FAIL).
|
|
63
|
+
4. **A `PASS` without a `HIGH | MEDIUM | LOW` confidence is not a verdict.** The confidence-aware
|
|
64
|
+
read above makes it arrive as `PASS!`, so it fails rule 2 like any other out-of-contract value.
|
|
65
|
+
5. **Leave a catch-all.** A guard set with no matching transition deadlocks the machine; every
|
|
66
|
+
judge state needs a terminal `*`/catch-all edge for verdicts outside the contract.
|
|
60
67
|
|
|
61
68
|
## Retry topology (NEEDS_REVISION)
|
|
62
69
|
|
|
@@ -50,7 +50,7 @@ Overrides: `--profile <dir>`, `--timeout <ms>`; env fallback `XHS_PROFILE_DIR` /
|
|
|
50
50
|
**ADR-025 (2026-09-28, after an XHS warning about automated activity).** Publish now runs this same
|
|
51
51
|
check as a **preflight** before opening anything headed — so a missing or expired session is a fast
|
|
52
52
|
refusal (`ok: false`) rather than a headed window sitting on the login gate for 120 s — and both
|
|
53
|
-
console visits are capped at **one headed publish cycle + one session check per
|
|
53
|
+
console visits are capped at **one headed publish cycle + one session check per 2 h**
|
|
54
54
|
(`~/.config/kk/xhs-automation.json`, override `KK_XHS_AUTOMATION_STATE`; a manual `check` draws on
|
|
55
55
|
the same `check` budget). A capped or sessionless publish fails with `ok: false` and a named reason.
|
|
56
56
|
`login` is never automated and never capped. Do not schedule XHS publishing: publish deliberately,
|
|
@@ -25,7 +25,7 @@ states:
|
|
|
25
25
|
# Seed the verdict so the judge gate guards can be evaluated in --dry-run
|
|
26
26
|
# (dry-run skips actions but still evaluates shell guards). A real run's
|
|
27
27
|
# judge step overwrites this file before the gate is read.
|
|
28
|
-
command: 'echo ''{"verdict":"PASS","score":0.0}'' > ${vars.verdictFile} && echo ''generating content...'''
|
|
28
|
+
command: 'echo ''{"verdict":"PASS","confidence":"HIGH","score":0.0}'' > ${vars.verdictFile} && echo ''generating content...'''
|
|
29
29
|
- id: judge
|
|
30
30
|
description: "Evaluate generated content via the judge skill (agent.run)"
|
|
31
31
|
onEnter:
|
|
@@ -51,7 +51,7 @@ states:
|
|
|
51
51
|
- id: done
|
|
52
52
|
description: "Publish completed successfully"
|
|
53
53
|
- id: failed
|
|
54
|
-
description: "Judge rejected content (FAIL) or retries exhausted (NEEDS_REVISION)"
|
|
54
|
+
description: "Judge rejected content (FAIL or an out-of-contract verdict) or retries exhausted (NEEDS_REVISION)"
|
|
55
55
|
|
|
56
56
|
transitions:
|
|
57
57
|
- from: generate
|
|
@@ -63,14 +63,18 @@ transitions:
|
|
|
63
63
|
# PASS -> publish.
|
|
64
64
|
- from: judge
|
|
65
65
|
to: publish
|
|
66
|
-
description: "Judge verdict is PASS (or file absent under --dry-run) -> proceed to publish"
|
|
66
|
+
description: "Judge verdict is PASS with a HIGH/MEDIUM/LOW confidence (or file absent under --dry-run) -> proceed to publish"
|
|
67
67
|
guard:
|
|
68
68
|
kind: shell
|
|
69
69
|
options:
|
|
70
|
-
# Fail-closed on an explicit non-PASS verdict
|
|
71
|
-
# absent, which happens under --dry-run (actions are skipped,
|
|
72
|
-
# never writes its verdict). A real run always has the file.
|
|
73
|
-
|
|
70
|
+
# Fail-closed on an explicit non-PASS verdict or a PASS without a valid confidence level;
|
|
71
|
+
# fail-open only when the file is absent, which happens under --dry-run (actions are skipped,
|
|
72
|
+
# so the judge never writes its verdict). A real run always has the file.
|
|
73
|
+
# The confidence-aware read appends `!` to an out-of-contract PASS, so the guards below treat
|
|
74
|
+
# it as a rejection instead of letting it through.
|
|
75
|
+
command: >-
|
|
76
|
+
v="$(jq -r 'if (.verdict // "") == "PASS" then (.verdict + (if ((.confidence // "") | test("^(HIGH|MEDIUM|LOW)$")) then "" else "!" end)) else (.verdict // "") end' ${vars.verdictFile} 2>/dev/null)";
|
|
77
|
+
test -z "$v" -o "$v" = PASS
|
|
74
78
|
|
|
75
79
|
# NEEDS_REVISION -> back to generate with a bounded retry budget.
|
|
76
80
|
- from: judge
|
|
@@ -84,14 +88,18 @@ transitions:
|
|
|
84
88
|
# responsible for incrementing attempt on each loop in a real run.
|
|
85
89
|
command: 'v="$(jq -r .verdict ${vars.verdictFile} 2>/dev/null)"; test "$v" = NEEDS_REVISION -a "$attempt" -lt "$maxRetries"'
|
|
86
90
|
|
|
87
|
-
# FAIL, or NEEDS_REVISION with no retries left -> failed.
|
|
91
|
+
# FAIL, out-of-contract verdict, or NEEDS_REVISION with no retries left -> failed.
|
|
88
92
|
- from: judge
|
|
89
93
|
to: failed
|
|
90
|
-
description: "Judge verdict is FAIL, or NEEDS_REVISION with retries exhausted -> block publish"
|
|
94
|
+
description: "Judge verdict is FAIL, out-of-contract, or NEEDS_REVISION with retries exhausted -> block publish"
|
|
91
95
|
guard:
|
|
92
96
|
kind: shell
|
|
93
97
|
options:
|
|
94
|
-
|
|
98
|
+
# Anything that is neither a usable PASS nor a retryable NEEDS_REVISION fails closed.
|
|
99
|
+
command: >-
|
|
100
|
+
v="$(jq -r 'if (.verdict // "") == "PASS" then (.verdict + (if ((.confidence // "") | test("^(HIGH|MEDIUM|LOW)$")) then "" else "!" end)) else (.verdict // "") end' ${vars.verdictFile} 2>/dev/null)";
|
|
101
|
+
{ test -n "$v" -a "$v" != PASS -a "$v" != NEEDS_REVISION; } ||
|
|
102
|
+
{ test "$v" = NEEDS_REVISION -a "$attempt" -ge "$maxRetries"; }
|
|
95
103
|
|
|
96
104
|
- from: publish
|
|
97
105
|
to: done
|