@gobing-ai/knowledge-kit 0.1.9 → 0.1.11
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 +132 -25
- package/package.json +1 -1
- package/plugins/generations/content-gen/dist/index.js +4 -0
- package/plugins/generations/core-facts-gen/dist/index.js +4 -0
- package/plugins/generations/daily-article-gen/dist/index.js +4 -0
- package/plugins/generations/dailynews-gen/dist/index.js +4 -0
- package/plugins/generations/episode-plan-gen/dist/index.js +102 -1
- package/plugins/generations/episode-plan-gen/src/index.ts +2 -4
- package/plugins/generations/gemini-tts/dist/index.js +4 -0
- package/plugins/generations/image-gen/dist/index.js +4 -0
- package/plugins/generations/news-report-gen/dist/index.js +4 -0
- package/plugins/generations/voice-gen/dist/index.js +4 -0
- package/plugins/generations/voxcpm-tts/dist/index.js +4 -0
- package/plugins/ingestions/aihot-ingest/dist/index.js +4 -0
- package/plugins/ingestions/horizon-ingest/dist/index.js +4 -0
- package/plugins/ingestions/last30days-ingest/dist/index.js +11 -3
- package/plugins/ingestions/last30days-ingest/src/index.ts +14 -3
- package/plugins/ingestions/web-search/dist/index.js +4 -0
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/scripts/daily-scheduled-run.ts +298 -0
- package/plugins/kk/scripts/daily-stages.ts +1083 -49
- package/plugins/kk/scripts/kk-workflow-stages.ts +76 -10
- package/plugins/kk/scripts/safety-artifact-check.ts +235 -0
- package/plugins/kk/scripts/sentences.ts +134 -0
- package/plugins/kk/scripts/series-names.ts +124 -0
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +194 -113
- package/plugins/kk/workflows/kk-daily-safety.yaml +160 -36
- package/plugins/publishings/bilibili-pub/dist/index.js +6 -1
- package/plugins/publishings/bilibili-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/cnblogs-pub/dist/index.js +6 -1
- package/plugins/publishings/cnblogs-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/csdn-pub/dist/index.js +6 -1
- package/plugins/publishings/csdn-pub/dist/profile-check.js +6 -1
- package/plugins/publishings/emdash-pub/dist/index.js +4 -0
- package/plugins/publishings/infoq-pub/dist/index.js +6 -1
- package/plugins/publishings/infoq-pub/dist/profile-check.js +6 -1
- package/plugins/publishings/jike-pub/dist/index.js +6 -1
- package/plugins/publishings/jike-pub/dist/profile-check.js +6 -1
- package/plugins/publishings/juejin-pub/dist/index.js +6 -1
- package/plugins/publishings/juejin-pub/dist/profile-check.js +6 -1
- package/plugins/publishings/medium-pub/dist/index.js +4 -0
- package/plugins/publishings/podcast-pub/dist/index.js +151 -16
- package/plugins/publishings/podcast-pub/src/index.ts +62 -5
- package/plugins/publishings/podcast-pub/src/seo.ts +2 -4
- package/plugins/publishings/podcast-pub/src/show-notes.ts +3 -8
- package/plugins/publishings/qiita-pub/dist/index.js +4 -0
- package/plugins/publishings/shipinhao-pub/dist/index.js +6 -1
- package/plugins/publishings/shipinhao-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/sspai-pub/dist/index.js +6 -1
- package/plugins/publishings/sspai-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/substack-pub/dist/index.js +6 -1
- package/plugins/publishings/substack-pub/dist/profile-check.js +6 -1
- package/plugins/publishings/surfdash-pub/dist/index.js +691 -63
- package/plugins/publishings/surfdash-pub/src/digest-check.ts +284 -0
- package/plugins/publishings/surfdash-pub/src/index.ts +656 -84
- package/plugins/publishings/surfdash-pub/src/patch.ts +44 -7
- package/plugins/publishings/threads-pub/dist/index.js +4 -0
- package/plugins/publishings/wechatmp-pub/dist/index.js +937 -98
- package/plugins/publishings/wechatmp-pub/dist/profile-check.js +64 -3
- package/plugins/publishings/wechatmp-pub/src/flow.ts +1125 -85
- package/plugins/publishings/wechatmp-pub/src/index.ts +112 -30
- package/plugins/publishings/wechatmp-pub/src/page.ts +105 -0
- package/plugins/publishings/weibo-pub/dist/index.js +6 -1
- package/plugins/publishings/weibo-pub/dist/profile-check.js +6 -1
- package/plugins/publishings/x-pub/dist/index.js +6 -1
- package/plugins/publishings/x-pub/dist/profile-check.js +6 -1
- package/plugins/publishings/xhs-pub/dist/index.js +6 -1
- package/plugins/publishings/xhs-pub/dist/profile-check.js +6 -1
- package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +6 -1
- package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/zenn-pub/dist/index.js +4 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
5
5
|
import { type Doc, DocListSchema } from '@gobing-ai/kk-core';
|
|
6
6
|
import { atomicWriteJson, createNodeFileSystem, readJsonFile } from '@gobing-ai/ts-runtime';
|
|
@@ -36,6 +36,14 @@ export type RunLast30Days = (
|
|
|
36
36
|
|
|
37
37
|
export const defaultRunner: RunLast30Days = async (config, outputPath) => {
|
|
38
38
|
const skillDir = process.env.LAST30DAYS_SKILL_DIR ?? join(homedir(), '.agents', 'skills', 'last30days');
|
|
39
|
+
// The skill dir is the spawn cwd: when the ~/.agents/skills/last30days symlink dangles,
|
|
40
|
+
// Bun reports it as `ENOENT ... posix_spawn 'python3'` — pointing at the interpreter, not
|
|
41
|
+
// the dir (2026-10-20 incident lost 9h of diagnosis to that). Fail with the real problem.
|
|
42
|
+
if (!createNodeFileSystem().exists(skillDir)) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`skill dir missing at ${skillDir} — restore the last30days skill checkout/symlink or set LAST30DAYS_SKILL_DIR`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
39
47
|
const argv = [
|
|
40
48
|
process.env.LAST30DAYS_PYTHON ?? 'python3',
|
|
41
49
|
join(skillDir, 'scripts', 'last30days.py'),
|
|
@@ -109,7 +117,10 @@ export async function processIngestionIO(
|
|
|
109
117
|
runner: RunLast30Days = defaultRunner,
|
|
110
118
|
): Promise<{ docs: Doc[] }> {
|
|
111
119
|
const fs = createNodeFileSystem();
|
|
112
|
-
|
|
120
|
+
// Relative --out would split cwd semantics: the engine writes its capture relative to the
|
|
121
|
+
// skill dir (its cwd) while we read it relative to ours. Pin everything to an absolute path.
|
|
122
|
+
const out = resolve(options.out);
|
|
123
|
+
const outDir = dirname(out);
|
|
113
124
|
if (outDir && outDir !== '.') await fs.ensureDir(outDir);
|
|
114
125
|
const capturePath = join(outDir || '.', '.last30days-agent.json');
|
|
115
126
|
const run = await runner(config, capturePath);
|
|
@@ -117,7 +128,7 @@ export async function processIngestionIO(
|
|
|
117
128
|
throw new Error(`last30days exited ${run.exitCode}: ${run.stderr.trim() || 'no stderr'}`);
|
|
118
129
|
}
|
|
119
130
|
const docs = mapResultsToDocs(await readJsonFile(capturePath), config.topic);
|
|
120
|
-
await atomicWriteJson(
|
|
131
|
+
await atomicWriteJson(out, docs, fs);
|
|
121
132
|
return { docs };
|
|
122
133
|
}
|
|
123
134
|
|
|
@@ -12275,6 +12275,10 @@ init_config();
|
|
|
12275
12275
|
|
|
12276
12276
|
// ../../packages/kk-core/src/render-md.ts
|
|
12277
12277
|
if (false) {}
|
|
12278
|
+
// ../../packages/kk-core/src/sentences.ts
|
|
12279
|
+
var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
|
|
12280
|
+
var LATIN_TERMINATORS = new Set([".", "!", "?"]);
|
|
12281
|
+
var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
|
|
12278
12282
|
// ../../node_modules/.bun/@logtape+logtape@2.3.1/node_modules/@logtape/logtape/dist/context.js
|
|
12279
12283
|
var categoryPrefixSymbol = Symbol.for("logtape.categoryPrefix");
|
|
12280
12284
|
function getCategoryPrefix() {
|
package/plugins/kk/plugin.json
CHANGED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/** Supervise one scheduled daily run and persist its terminal outcome even after a deadline. */
|
|
3
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
4
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { parseArgs } from 'node:util';
|
|
7
|
+
|
|
8
|
+
const { values } = parseArgs({
|
|
9
|
+
options: {
|
|
10
|
+
repo: { type: 'string', default: process.cwd() },
|
|
11
|
+
'works-dir': { type: 'string' },
|
|
12
|
+
'run-id': { type: 'string' },
|
|
13
|
+
'timeout-ms': { type: 'string', default: '13500000' },
|
|
14
|
+
receipt: { type: 'string' },
|
|
15
|
+
agent: { type: 'string', default: 'inline' },
|
|
16
|
+
'retry-agent': { type: 'string', default: 'claude' },
|
|
17
|
+
'required-channels': { type: 'string', default: 'wechat,locales,cms-resync' },
|
|
18
|
+
'wechat-live': { type: 'boolean', default: false },
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
const timeoutMs = Number(values['timeout-ms']);
|
|
22
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
|
|
23
|
+
console.error('daily-scheduled-run: --timeout-ms must be a positive integer');
|
|
24
|
+
process.exit(2);
|
|
25
|
+
}
|
|
26
|
+
const repo = resolve(values.repo);
|
|
27
|
+
const now = new Date();
|
|
28
|
+
const stamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`;
|
|
29
|
+
const runId = values['run-id'] ?? `daily-ai-voice-${stamp}-${now.getTime()}`;
|
|
30
|
+
const workDir = join(resolve(values['works-dir'] ?? join(repo, '.works')), 'kk-daily-ai-voice2', stamp);
|
|
31
|
+
const receiptPath = resolve(values.receipt ?? join(workDir, 'schedule-receipt.json'));
|
|
32
|
+
const startedAt = now.toISOString();
|
|
33
|
+
mkdirSync(dirname(receiptPath), { recursive: true });
|
|
34
|
+
const vars = {
|
|
35
|
+
publish_enabled: 'true',
|
|
36
|
+
skip_review: 'true',
|
|
37
|
+
agent: values.agent,
|
|
38
|
+
agent_retry: values['retry-agent'],
|
|
39
|
+
wechat_live: values['wechat-live'] === true ? 'true' : 'false',
|
|
40
|
+
work_dir: workDir,
|
|
41
|
+
run_date: stamp,
|
|
42
|
+
};
|
|
43
|
+
writeFileSync(receiptPath, JSON.stringify({ runId, startedAt, status: 'running', workDir }, null, 2));
|
|
44
|
+
let interrupted = false;
|
|
45
|
+
let timedOut = false;
|
|
46
|
+
let spawnError = '';
|
|
47
|
+
let forceKill: ReturnType<typeof setTimeout> | undefined;
|
|
48
|
+
const child = spawn(
|
|
49
|
+
'spur',
|
|
50
|
+
[
|
|
51
|
+
'--no-logo',
|
|
52
|
+
'workflow',
|
|
53
|
+
'run',
|
|
54
|
+
'plugins/kk/workflows/kk-daily-ai-voice2.yaml',
|
|
55
|
+
'--run-id',
|
|
56
|
+
runId,
|
|
57
|
+
'--vars',
|
|
58
|
+
JSON.stringify(vars),
|
|
59
|
+
'--quiet',
|
|
60
|
+
],
|
|
61
|
+
{ cwd: repo, detached: true, stdio: 'inherit' },
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const ownedPids = new Set<number>();
|
|
65
|
+
/** Snapshot descendants before the root exits, including workers that created their own groups. */
|
|
66
|
+
function rememberDescendants(): void {
|
|
67
|
+
if (child.pid === undefined) return;
|
|
68
|
+
ownedPids.add(child.pid);
|
|
69
|
+
const snapshot = spawnSync('ps', ['-axo', 'pid=,ppid='], { encoding: 'utf-8', timeout: 5000 });
|
|
70
|
+
const rows = snapshot.stdout
|
|
71
|
+
.trim()
|
|
72
|
+
.split('\n')
|
|
73
|
+
.map((line) => line.trim().split(/\s+/).map(Number));
|
|
74
|
+
const descendants = new Set([child.pid]);
|
|
75
|
+
let added = true;
|
|
76
|
+
while (added) {
|
|
77
|
+
added = false;
|
|
78
|
+
for (const [pid, parent] of rows) {
|
|
79
|
+
if (pid !== undefined && parent !== undefined && descendants.has(parent) && !descendants.has(pid)) {
|
|
80
|
+
descendants.add(pid);
|
|
81
|
+
added = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const pid of descendants) ownedPids.add(pid);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Stop only the recorded owned tree; process-group signals also catch late same-group forks. */
|
|
89
|
+
function signalOwned(signal: NodeJS.Signals): void {
|
|
90
|
+
if (child.pid === undefined) return;
|
|
91
|
+
rememberDescendants();
|
|
92
|
+
for (const pid of [...ownedPids].reverse()) {
|
|
93
|
+
try {
|
|
94
|
+
process.kill(-pid, signal);
|
|
95
|
+
} catch {}
|
|
96
|
+
try {
|
|
97
|
+
process.kill(pid, signal);
|
|
98
|
+
} catch {}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function terminate(): void {
|
|
103
|
+
signalOwned('SIGTERM');
|
|
104
|
+
if (forceKill === undefined) {
|
|
105
|
+
forceKill = setTimeout(() => {
|
|
106
|
+
signalOwned('SIGKILL');
|
|
107
|
+
}, 300);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const onSignal = (): void => {
|
|
111
|
+
interrupted = true;
|
|
112
|
+
terminate();
|
|
113
|
+
};
|
|
114
|
+
process.once('SIGTERM', onSignal);
|
|
115
|
+
process.once('SIGINT', onSignal);
|
|
116
|
+
const deadline = setTimeout(() => {
|
|
117
|
+
timedOut = true;
|
|
118
|
+
terminate();
|
|
119
|
+
}, timeoutMs);
|
|
120
|
+
const exitCode = await new Promise<number>((done) => {
|
|
121
|
+
child.once('error', (error) => {
|
|
122
|
+
spawnError = error.message;
|
|
123
|
+
done(1);
|
|
124
|
+
});
|
|
125
|
+
child.once('close', (code) => done(code ?? 1));
|
|
126
|
+
});
|
|
127
|
+
clearTimeout(deadline);
|
|
128
|
+
if (forceKill !== undefined) clearTimeout(forceKill);
|
|
129
|
+
// A killed shell can leave owned descendants that ignore TERM. Always finish the group before
|
|
130
|
+
// recording a terminal outcome; no detached grandchild may continue to publish after a timeout.
|
|
131
|
+
if (child.pid !== undefined && (timedOut || interrupted || exitCode !== 0)) {
|
|
132
|
+
terminate();
|
|
133
|
+
await Bun.sleep(300);
|
|
134
|
+
signalOwned('SIGKILL');
|
|
135
|
+
await Bun.sleep(100);
|
|
136
|
+
}
|
|
137
|
+
const workflowStatus = timedOut ? 'timed-out' : interrupted ? 'interrupted' : exitCode === 0 ? 'done' : 'failed';
|
|
138
|
+
const primaryPublishStatus = readOptional(join(workDir, '31-publish', `${stamp}_17_publish_publish-status.txt`));
|
|
139
|
+
const runSummary = readOptional(join(workDir, 'run-report.summary.txt'));
|
|
140
|
+
const cleanup: { command: string; exitCode: number | null }[] = [];
|
|
141
|
+
let reservationRecovery: { status: string; reason: string; released?: number } = {
|
|
142
|
+
status: 'retained',
|
|
143
|
+
reason: 'workflow completed; publication settlement owns its reservations',
|
|
144
|
+
};
|
|
145
|
+
if (workflowStatus !== 'done' || runSummary?.startsWith('no-content:')) {
|
|
146
|
+
// The caller gives the nested safety run this exact id. Unknown/already-terminal ids are
|
|
147
|
+
// harmless cleanup failures; they never override the original workflow failure.
|
|
148
|
+
if (workflowStatus !== 'done') {
|
|
149
|
+
for (const id of [`${runId}-safety`, runId]) {
|
|
150
|
+
const result = spawnSync('spur', ['workflow', 'cancel', id, '--json'], {
|
|
151
|
+
cwd: repo,
|
|
152
|
+
encoding: 'utf-8',
|
|
153
|
+
timeout: 20000,
|
|
154
|
+
});
|
|
155
|
+
cleanup.push({ command: `spur workflow cancel ${id}`, exitCode: result.status });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Only a complete trace proving no channel send began permits recycling reservations. A
|
|
159
|
+
// missing receipt alone is insufficient: a remote send may have succeeded before it crashed.
|
|
160
|
+
const trace = spawnSync('spur', ['workflow', 'trace', runId, '--json'], {
|
|
161
|
+
cwd: repo,
|
|
162
|
+
encoding: 'utf-8',
|
|
163
|
+
timeout: 20000,
|
|
164
|
+
});
|
|
165
|
+
reservationRecovery = { status: 'retained', reason: 'publication state could not be established' };
|
|
166
|
+
try {
|
|
167
|
+
const parsed = JSON.parse(trace.stdout) as {
|
|
168
|
+
run?: { runId?: string; workflowName?: string; status?: string; startedAt?: string };
|
|
169
|
+
events?: { kind?: string; node?: string; status?: string }[];
|
|
170
|
+
};
|
|
171
|
+
const sendNodes = new Set([
|
|
172
|
+
'publish-surfdash',
|
|
173
|
+
'publish-podcast',
|
|
174
|
+
'xhs-publish',
|
|
175
|
+
'wechat-publish',
|
|
176
|
+
'translate-localize',
|
|
177
|
+
'translate',
|
|
178
|
+
'translate-retry',
|
|
179
|
+
'translate-sync',
|
|
180
|
+
'publish-classify',
|
|
181
|
+
]);
|
|
182
|
+
const validTrace =
|
|
183
|
+
trace.status === 0 &&
|
|
184
|
+
parsed.run?.runId === runId &&
|
|
185
|
+
parsed.run?.workflowName === 'kk-daily-ai-voice2' &&
|
|
186
|
+
parsed.run?.status === (workflowStatus === 'done' ? 'done' : 'failed') &&
|
|
187
|
+
Date.parse(parsed.run.startedAt ?? '') >= Date.parse(startedAt) - 1000 &&
|
|
188
|
+
Date.parse(parsed.run.startedAt ?? '') <= Date.now() &&
|
|
189
|
+
Array.isArray(parsed.events) &&
|
|
190
|
+
parsed.events.length > 0;
|
|
191
|
+
const sendStarted = parsed.events?.some(
|
|
192
|
+
(event) => event.kind === 'action' && sendNodes.has(event.node ?? '') && event.status !== 'skipped',
|
|
193
|
+
);
|
|
194
|
+
if (validTrace && !sendStarted) {
|
|
195
|
+
// Cancellation is durable before reuse. Require a second stable snapshot after the
|
|
196
|
+
// owned processes are gone, so an in-flight or partially finalized row cannot pass.
|
|
197
|
+
await Bun.sleep(200);
|
|
198
|
+
const secondTrace = spawnSync('spur', ['workflow', 'trace', runId, '--json'], {
|
|
199
|
+
cwd: repo,
|
|
200
|
+
encoding: 'utf-8',
|
|
201
|
+
timeout: 20000,
|
|
202
|
+
});
|
|
203
|
+
const second = JSON.parse(secondTrace.stdout) as typeof parsed;
|
|
204
|
+
if (
|
|
205
|
+
secondTrace.status !== 0 ||
|
|
206
|
+
JSON.stringify(second.run) !== JSON.stringify(parsed.run) ||
|
|
207
|
+
JSON.stringify(second.events) !== JSON.stringify(parsed.events)
|
|
208
|
+
) {
|
|
209
|
+
throw new Error('failed-run trace is not stable after cancellation');
|
|
210
|
+
}
|
|
211
|
+
const release = spawnSync('kk', ['store', 'release', '--topic', 'ai-news', '--run', runId], {
|
|
212
|
+
cwd: repo,
|
|
213
|
+
encoding: 'utf-8',
|
|
214
|
+
timeout: 20000,
|
|
215
|
+
});
|
|
216
|
+
cleanup.push({ command: 'kk store release (verified pre-publication failure)', exitCode: release.status });
|
|
217
|
+
const result = release.status === 0 ? (JSON.parse(release.stdout) as { released?: unknown }) : null;
|
|
218
|
+
reservationRecovery =
|
|
219
|
+
result !== null && typeof result.released === 'number'
|
|
220
|
+
? { status: 'released', reason: 'trace proves no channel send began', released: result.released }
|
|
221
|
+
: { status: 'retained', reason: 'reservation release did not produce a valid receipt' };
|
|
222
|
+
} else if (sendStarted) {
|
|
223
|
+
reservationRecovery = { status: 'retained', reason: 'a channel send began; preserve uncertain delivery' };
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
// Unreadable/truncated evidence or an unexpected CLI shape must never cause reuse of a
|
|
227
|
+
// potentially aired story. Keep the reservation and the original failure visible.
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const ledger = spawnSync('kk', ['ledger', 'sync', '--workflow', 'kk-daily-ai-voice2', '--work-dir', workDir], {
|
|
231
|
+
cwd: repo,
|
|
232
|
+
encoding: 'utf-8',
|
|
233
|
+
timeout: 20000,
|
|
234
|
+
});
|
|
235
|
+
cleanup.push({ command: 'kk ledger sync', exitCode: ledger.status });
|
|
236
|
+
function readOptional(path: string): string | null {
|
|
237
|
+
try {
|
|
238
|
+
return readFileSync(path, 'utf-8').trim();
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const requiredChannels = values['required-channels']
|
|
244
|
+
.split(',')
|
|
245
|
+
.map((channel) => channel.trim())
|
|
246
|
+
.filter(Boolean);
|
|
247
|
+
let channelCompleteness: { channel: string; state: string; detail?: string }[] = [];
|
|
248
|
+
try {
|
|
249
|
+
const parsed = JSON.parse(
|
|
250
|
+
readOptional(join(workDir, '31-publish', `${stamp}_17_publish_completeness.json`)) ?? 'null',
|
|
251
|
+
) as { rows?: unknown } | null;
|
|
252
|
+
if (Array.isArray(parsed?.rows)) {
|
|
253
|
+
channelCompleteness = parsed.rows.filter(
|
|
254
|
+
(row): row is { channel: string; state: string; detail?: string } =>
|
|
255
|
+
row !== null &&
|
|
256
|
+
typeof row === 'object' &&
|
|
257
|
+
typeof row.channel === 'string' &&
|
|
258
|
+
typeof row.state === 'string',
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
} catch {}
|
|
262
|
+
const missingRequiredChannels = requiredChannels.filter((channel) => {
|
|
263
|
+
const rows = channelCompleteness.filter((row) => row.channel === channel);
|
|
264
|
+
return rows.length !== 1 || rows[0]?.state !== 'done';
|
|
265
|
+
});
|
|
266
|
+
// A terminal green workflow can mean "no content" or failed delivery. Make that observable to
|
|
267
|
+
// the scheduler instead of equating the workflow's process exit with a published edition.
|
|
268
|
+
const status =
|
|
269
|
+
workflowStatus !== 'done'
|
|
270
|
+
? workflowStatus
|
|
271
|
+
: runSummary?.startsWith('no-content:')
|
|
272
|
+
? 'no-content'
|
|
273
|
+
: primaryPublishStatus === 'full' && missingRequiredChannels.length === 0
|
|
274
|
+
? 'done'
|
|
275
|
+
: 'incomplete';
|
|
276
|
+
const receipt = {
|
|
277
|
+
runId,
|
|
278
|
+
startedAt,
|
|
279
|
+
completedAt: new Date().toISOString(),
|
|
280
|
+
status,
|
|
281
|
+
exitCode: timedOut ? 124 : interrupted ? 143 : exitCode !== 0 ? exitCode : status === 'done' ? 0 : 4,
|
|
282
|
+
workDir,
|
|
283
|
+
timeoutMs,
|
|
284
|
+
agent: values.agent,
|
|
285
|
+
retryAgent: values['retry-agent'],
|
|
286
|
+
error: spawnError || null,
|
|
287
|
+
primaryPublishStatus,
|
|
288
|
+
channelCompleteness,
|
|
289
|
+
missingRequiredChannels,
|
|
290
|
+
runSummary,
|
|
291
|
+
reservationRecovery,
|
|
292
|
+
cleanup,
|
|
293
|
+
};
|
|
294
|
+
writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
295
|
+
console.log(JSON.stringify(receipt));
|
|
296
|
+
process.removeListener('SIGTERM', onSignal);
|
|
297
|
+
process.removeListener('SIGINT', onSignal);
|
|
298
|
+
process.exit(receipt.exitCode);
|