@gobing-ai/knowledge-kit 0.0.32 → 0.0.33

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.
Files changed (65) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +32320 -15021
  3. package/package.json +4 -4
  4. package/plugins/generations/content-gen/dist/index.js +10 -9
  5. package/plugins/generations/core-facts-gen/dist/index.js +13 -12
  6. package/plugins/generations/daily-article-gen/dist/index.js +14 -13
  7. package/plugins/generations/daily-article-gen/src/index.ts +1 -1
  8. package/plugins/generations/dailynews-gen/dist/index.js +10 -9
  9. package/plugins/generations/episode-plan-gen/dist/index.js +58 -15
  10. package/plugins/generations/episode-plan-gen/src/index.ts +76 -6
  11. package/plugins/generations/gemini-tts/README.md +11 -0
  12. package/plugins/generations/gemini-tts/dist/index.js +10 -9
  13. package/plugins/generations/image-gen/dist/index.js +154 -151
  14. package/plugins/generations/image-gen/src/compose/chromium.ts +60 -42
  15. package/plugins/generations/news-report-gen/dist/index.js +13 -12
  16. package/plugins/generations/voice-gen/dist/index.js +10 -9
  17. package/plugins/ingestions/aihot-ingest/dist/index.js +13 -12
  18. package/plugins/ingestions/horizon-ingest/dist/index.js +13 -12
  19. package/plugins/ingestions/karakeep-local/dist/index.js +13 -13
  20. package/plugins/ingestions/last30days-ingest/dist/index.js +13 -12
  21. package/plugins/ingestions/web-search/dist/index.js +11 -10
  22. package/plugins/kk/README.md +38 -0
  23. package/plugins/kk/decisions/extract/ai-news.yaml +21 -0
  24. package/plugins/kk/decisions/kk-items.yaml +113 -0
  25. package/plugins/kk/plugin.json +1 -1
  26. package/plugins/kk/schedules/cron.tmpl +15 -0
  27. package/plugins/kk/schedules/launchd.plist.tmpl +77 -0
  28. package/plugins/kk/scripts/daily-stages.ts +1533 -0
  29. package/plugins/kk/scripts/kk-workflow-stages.ts +247 -1365
  30. package/plugins/kk/skills/content-safety/SKILL.md +38 -7
  31. package/plugins/kk/skills/web-automation/SKILL.md +100 -0
  32. package/plugins/kk/skills/web-automation/references/browser-launch.md +86 -0
  33. package/plugins/kk/workflows/kk-collect.yaml +258 -0
  34. package/plugins/kk/workflows/kk-daily-ai-voice.yaml +9 -3
  35. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +655 -0
  36. package/plugins/kk/workflows/kk-daily-safety.yaml +170 -0
  37. package/plugins/publishings/emdash-pub/dist/index.js +10 -9
  38. package/plugins/publishings/infoq-pub/dist/index.js +152 -59
  39. package/plugins/publishings/infoq-pub/plugin.json +1 -0
  40. package/plugins/publishings/infoq-pub/src/index.ts +167 -55
  41. package/plugins/publishings/infoq-pub/src/profile-check.ts +4 -2
  42. package/plugins/publishings/juejin-pub/dist/index.js +111 -30
  43. package/plugins/publishings/juejin-pub/plugin.json +1 -0
  44. package/plugins/publishings/juejin-pub/src/index.ts +49 -9
  45. package/plugins/publishings/medium-pub/dist/index.js +10 -9
  46. package/plugins/publishings/podcast-pub/dist/index.js +10 -9
  47. package/plugins/publishings/qiita-pub/dist/index.js +10 -9
  48. package/plugins/publishings/substack-pub/dist/index.js +105 -28
  49. package/plugins/publishings/substack-pub/plugin.json +1 -0
  50. package/plugins/publishings/substack-pub/src/index.ts +39 -8
  51. package/plugins/publishings/substack-pub/src/profile-check.ts +2 -0
  52. package/plugins/publishings/surfdash-pub/dist/index.js +10 -9
  53. package/plugins/publishings/wechatmp-pub/dist/index.js +115 -31
  54. package/plugins/publishings/wechatmp-pub/plugin.json +1 -0
  55. package/plugins/publishings/wechatmp-pub/src/index.ts +42 -9
  56. package/plugins/publishings/wechatmp-pub/src/profile-check.ts +84 -40
  57. package/plugins/publishings/x-pub/dist/index.js +114 -35
  58. package/plugins/publishings/x-pub/plugin.json +1 -0
  59. package/plugins/publishings/x-pub/src/index.ts +68 -14
  60. package/plugins/publishings/xhs-pub/dist/index.js +100 -54
  61. package/plugins/publishings/xhs-pub/plugin.json +1 -0
  62. package/plugins/publishings/xhs-pub/src/index.ts +8 -4
  63. package/plugins/publishings/xhs-pub/src/page.ts +4 -17
  64. package/plugins/publishings/xhs-pub/src/profile-check.ts +28 -15
  65. package/plugins/publishings/zenn-pub/dist/index.js +10 -9
@@ -67,13 +67,14 @@ it as a category-4 finding (quoted as evidence) and apply the decision rules exa
67
67
 
68
68
  A platform file is a snapshot. When its `Last verified` date is older than the content's run
69
69
  date by more than 90 days, or a takedown/feedback event post-dates it, treat its rules as
70
- `unverified` for this review (`evidenceGap: true`, escalate) and say so in the report.
70
+ `unverified` for this review (`evidenceGap: true`, `disposition: "gap"`) and say so in the report.
71
71
 
72
72
  ## Decision per platform
73
73
 
74
74
  For each requested platform, load `references/platforms/<platform>.md` and decide. A requested
75
75
  platform with **no reference file** is unsupported: emit `BLOCK` with `evidenceGap: true` and
76
- `escalate: true` for it; rules come only from that platform's own reference file.
76
+ `disposition: "escalate"` for it (no rules at all is a genuine operator decision); rules come
77
+ only from that platform's own reference file.
77
78
 
78
79
  - `OK` — no finding reaches a block rule.
79
80
  - `RISK` — findings exist but are warn-grade (e.g. evidence gap on this platform); submission is
@@ -84,8 +85,9 @@ platform with **no reference file** is unsupported: emit `BLOCK` with `evidenceG
84
85
  never offsets a high-risk finding.**
85
86
 
86
87
  If the platform file's evidence status is `unknown`/`unverified` for the rule a finding rests on,
87
- the finding is reported with `evidenceGap: true` and the decision escalates to manual review —
88
- a silent auto-`OK` on gapped evidence is a contract violation.
88
+ the finding is reported with `evidenceGap: true` and `disposition: "gap"` — risk recorded on the
89
+ report, never a silent auto-`OK` (that combination is a contract violation). A gap alone does not
90
+ require pre-publish human review; only `disposition: "escalate"` does (20261007).
89
91
 
90
92
  ## Output contract
91
93
 
@@ -96,6 +98,7 @@ One JSON report per invocation, written to the path the caller provides (default
96
98
  {
97
99
  "content_ref": "<path or id of reviewed Content>",
98
100
  "mode": "detect-only | detect+revise",
101
+ "confidence": "HIGH | MEDIUM | LOW",
99
102
  "platforms": [
100
103
  {
101
104
  "platform": "<reference file name>",
@@ -106,7 +109,8 @@ One JSON report per invocation, written to the path the caller provides (default
106
109
  "item": "<platform review-item id, or generic>",
107
110
  "evidence": "<quoted passage / location>",
108
111
  "detail": "<why this is a finding>",
109
- "evidenceGap": false
112
+ "evidenceGap": false,
113
+ "disposition": "note | fix | gap | escalate"
110
114
  }
111
115
  ],
112
116
  "revision": {
@@ -126,6 +130,30 @@ One JSON report per invocation, written to the path the caller provides (default
126
130
  `escalate: true` + printed report path is required whenever evidence is missing for a finding's
127
131
  rule. Never emit `OK` with `evidenceGap` findings; that combination is a contract violation.
128
132
 
133
+ ### Finding dispositions (20261007)
134
+
135
+ Every finding carries `disposition`, telling the consumer what answering it requires:
136
+
137
+ | `disposition` | Meaning | Set when |
138
+ |---|---|---|
139
+ | `note` | Recorded context judgment / verified-OK observation; no action | 字词命中 without mainline risk, checks that passed with commentary |
140
+ | `fix` | Fixable by deletion / desensitization / disclosure; revision proposed in `detect+revise` | Categories 3–5 findings the drafts can be edited to resolve |
141
+ | `gap` | Evidence gap: the rule text is unverified for this review; risk recorded, review not forced | `evidenceGap: true` findings on an otherwise sound draft |
142
+ | `escalate` | Unresolved admission risk an operator must decide before submission | Category-2 residue that survives exclusion, no-reference-file platforms, anything the operator must answer by hand |
143
+
144
+ Report-level `escalate: true` is set **iff at least one finding carries `disposition:
145
+ "escalate"`**; `escalation_reason` names those findings. Consumers that key on this field: the
146
+ `kk-daily-ai-voice` gate escalates only on `disposition: "escalate"`; a report with no
147
+ `disposition` field anywhere keeps the legacy behavior (any RISK decision or report-level
148
+ escalate pauses a run) — fail-closed back-compat.
149
+
150
+ Report-level `confidence` (`HIGH | MEDIUM | LOW`) is the reviewer's self-graded certainty in the
151
+ report's decisions and dispositions, graded the same way as the content-judge verdict confidence.
152
+ A disposition-contract report **must** carry a valid `confidence`: the daily gate blocks a report
153
+ that classifies with dispositions whose confidence is missing or outside the enum (fail-closed,
154
+ mirroring the 20261003 verdict-confidence hardening). Legacy reports (no `disposition` field)
155
+ remain exempt.
156
+
129
157
  ## Relation to content-judge
130
158
 
131
159
  - content-judge scores **quality**; content-safety decides **admission risk**. Different questions.
@@ -163,8 +191,11 @@ travel with the draft still block (a PII finding on a blocked platform, or a jud
163
191
  outside the admission criteria: prohibited claims / disclosure / data-handling), while
164
192
  **admission-only findings** (the CN 资质口径 / 播报形态 rules) drop the platform
165
193
  they name from that run and let every other target publish; the drop is recorded in the verdict
166
- stamp's `excludedPlatforms` and honoured by the per-target publish stages. A `RISK` decision or
167
- `escalate: true` still forces the human review even when the run is otherwise unattended.
194
+ stamp's `excludedPlatforms` and honoured by the per-target publish stages. On an otherwise clean
195
+ pass (PASS verdict, no BLOCK), only findings marked `disposition: "escalate"` force the human
196
+ review (20261007); evidence-gap and note/fix-class RISK stay recorded and publish at recorded
197
+ risk (gate status `exclude`, empty `excludedPlatforms`). A report with no `disposition` field
198
+ keeps the legacy behavior — any RISK decision or `escalate: true` pauses.
168
199
 
169
200
  The report is bound to the exact draft bytes: editing a draft after the verdict invalidates it, and
170
201
  the remediation loop re-requests a fresh review for that reason. The earlier deterministic gate is
@@ -0,0 +1,100 @@
1
+ ---
2
+ name: web-automation
3
+ description: >-
4
+ Discover, enhance, and automate web tasks with Playwright (or agent-browser
5
+ over CDP) — "walk this console and record evidence", "figure out this site's
6
+ UI", "automate this web flow", "reuse my cached login on <channel>", "check
7
+ whether my session on <channel> is still valid", "fix this channel's
8
+ selectors", or when a spur workflow `agent.run` step targets the
9
+ web-automation skill. Owns method and evidence; transport stays in kk
10
+ plugins; orchestration is a @gobing-ai/ts-dual-workflow-engine
11
+ transition-flow (see docs/design/web-automation-engine.md).
12
+ ---
13
+
14
+ # web-automation — discover, enhance, automate with a real browser
15
+
16
+ ## Purpose
17
+
18
+ Three operations over one method: drive a **real Chromium on the channel's
19
+ cached login profile**, record **evidence** (screenshots, DOM signals, JSON),
20
+ and never report an outcome that was not observed.
21
+
22
+ | Operation | When | Output |
23
+ | --- | --- | --- |
24
+ | **discover** | new/changed site; "figure out this UI" | evidence dir + drift list (`docs/design/<channel>-ui-discovery.md` convention) |
25
+ | **enhance** | selectors/signals drifted; a plugin or workflow fails on a live console | fixed selectors/signals in the owning plugin + updated discovery doc, same commit |
26
+ | **automate** | a repeatable web task (login-gated save/draft/fill/read) | workflow-driven run with per-step Result/evidence; draft-safe by default |
27
+
28
+ This skill owns **method**. It does not own transport (that is the
29
+ `kind: publish`/`ingestion` plugins) or orchestration (that is the engine —
30
+ `spur workflow` runs `@gobing-ai/ts-dual-workflow-engine`; see
31
+ `docs/design/web-automation-engine.md` for the layering and the YAML).
32
+
33
+ ## Cached login (`auth: cached` — the default)
34
+
35
+ Login state is cached **per channel** as a persistent Playwright profile dir —
36
+ cookies, localStorage, and session live on disk outside git:
37
+
38
+ ```
39
+ ~/.config/kk/browser-profiles/<channel> # xhs, infoq, juejin, substack, x, wechatmp
40
+ ```
41
+
42
+ | `auth` | Behavior |
43
+ | --- | --- |
44
+ | `cached` (default) | Preflight the profile; reuse it read-only. Launch with `launchPersistentContext` (see `references/browser-launch.md`). No credentials are typed, read, or stored by any script. |
45
+ | `fresh` | Profile missing/expired → **headed** launch, operator completes login by hand (QR/OAuth/password), then the profile is the cache again. Never auto-type credentials; OAuth gates need the stealth args. |
46
+
47
+ Preflight before any drive — cheap and headless:
48
+
49
+ ```bash
50
+ test -d ~/.config/kk/browser-profiles/<channel> && echo cached || echo fresh-required
51
+ ```
52
+
53
+ A profile dir existing is necessary, not sufficient: before `automate` runs,
54
+ do a read-only session probe (open the console URL, check a signed-in-only
55
+ selector) and route to `fresh` on failure. Age-guessing timestamps is banned —
56
+ probe, don't predict.
57
+
58
+ ## Method
59
+
60
+ 1. **Launch** — `chromium.launchPersistentContext(profileDir, { headless, args: [CDP port, stealth] })`.
61
+ Decision table and the exact snippet: `references/browser-launch.md`.
62
+ 2. **Discover** — screenshot first, then read DOM (selectors, shadow roots,
63
+ iframes, counts) with small throwaway scripts; save frames + a signals JSON
64
+ into the run's evidence dir; write/refresh the channel's
65
+ `docs/design/<channel>-ui-discovery.md` (selectors, drift list, date).
66
+ 3. **Enhance** — diff observed DOM against the plugin's encoded
67
+ selectors/signals; fix the plugin (root cause — one shared helper, not
68
+ every caller), re-walk to prove it, update the discovery doc in the same
69
+ commit.
70
+ 4. **Automate** — encode the flow as engine workflow nodes (preflight →
71
+ launch → drive → verify → record); `drive` steps run the measured
72
+ selectors and **assert an observed outcome signal** (toast, counter delta,
73
+ URL change). An unobserved outcome is reported as failure, never success.
74
+ 5. **Confidence-verified verdicts** — every outcome verdict carries
75
+ `confidence: HIGH | MEDIUM | LOW` (HIGH = measured signal observed;
76
+ MEDIUM = indirect signal, e.g. counter delta without toast; LOW = partial
77
+ evidence, one check passed). **Missing or out-of-enum confidence is itself
78
+ a failure** — the verify gate blocks it, mirroring the safety-verdict
79
+ convention (`kk-daily-ai-voice.md`, 20261003).
80
+
81
+ ## Safety rails (non-negotiable)
82
+
83
+ - **Never click publish/send/submit/delete** unless the task explicitly
84
+ authorizes live publishing. Default target: draft/save/暂存.
85
+ - Bot-walled channels (xhs) and fresh logins stay **headed**; headless only
86
+ after the session is proven.
87
+ - Every reported success cites observed evidence (signal, screenshot, JSON).
88
+ No evidence → it is a failure with the reason — and every verdict states
89
+ its `HIGH | MEDIUM | LOW` confidence; a verdict without a valid level
90
+ fails the gate like a missing verdict.
91
+ - Secrets stay out of repo, YAML, and vars. Login state lives only in the
92
+ profile dir under `~/.config` (outside git); scripts print paths, never tokens.
93
+ - One browser per channel/profile at a time; close contexts you launched.
94
+
95
+ ## Engine invocation (summary)
96
+
97
+ Orchestration is a `kind: transition-flow` workflow; `startable` nodes let a
98
+ run re-enter at `drive` after a manual login without redoing earlier nodes.
99
+ Full YAML, node contract, and the option-B (in-process runners) design:
100
+ `docs/design/web-automation-engine.md`.
@@ -0,0 +1,86 @@
1
+ # Browser launch & cached-login recipes
2
+
3
+ Everything here is measured on the kk channels (E92 discovery, 2026-10).
4
+ The launcher is the proven pattern from `.spur/run/discovery/xhs-launch.ts` /
5
+ `walk-wechatmp*.ts`; the profile matrix is the live
6
+ `~/.config/kk/browser-profiles/` tree.
7
+
8
+ ## The launcher (persistent context + CDP + stealth)
9
+
10
+ ```ts
11
+ import { chromium } from 'playwright';
12
+
13
+ const profileDir = process.env.KK_WEB_PROFILE ?? `${process.env.HOME}/.config/kk/browser-profiles/<channel>`;
14
+ const port = process.env.KK_WEB_CDP ?? '9222';
15
+
16
+ const ctx = await chromium.launchPersistentContext(profileDir, {
17
+ headless: process.env.KK_WEB_HEADLESS === 'true', // default headed
18
+ viewport: null,
19
+ args: [
20
+ `--remote-debugging-port=${port}`, // agent-browser / external CDP clients attach here
21
+ '--disable-blink-features=AutomationControlled', // required at OAuth/bot-wall gates
22
+ ],
23
+ });
24
+ const page = ctx.pages()[0] ?? (await ctx.newPage());
25
+ console.log(JSON.stringify({ cdpPort: port, profile: profileDir, url: page.url() }));
26
+ process.on('SIGTERM', async () => { await ctx.close(); process.exit(0); });
27
+ await new Promise(() => {}); // stay up for CDP drivers
28
+ ```
29
+
30
+ - The **profile dir is the credential cache** — cookies + storage survive runs;
31
+ nothing else is persisted, no tokens are printed.
32
+ - `agent-browser` (or any CDP client) attaches to the printed port; plain
33
+ Playwright scripts can also import the same process's `page`.
34
+ - One context per channel. Two concurrent launches on one profile corrupt it.
35
+
36
+ ## Headless vs headed
37
+
38
+ | Situation | Mode | Why |
39
+ | --- | --- | --- |
40
+ | Session probe / preflight | headless OK | read-only, cheap |
41
+ | Automated drive on a proven session | headless OK | measured: infoq/juejin/substack/x/wechatmp drives work headless post-login |
42
+ | `auth: fresh` login | **headed** | operator completes QR/OAuth by hand |
43
+ | xhs any operation | **headed** | bot-walls headless loads (measured); shadow-root clicks need CDP anyway |
44
+ | Any step that might trip a bot wall first time | **headed** | watch it once, record, then automate |
45
+
46
+ ## Profile matrix (as shipped)
47
+
48
+ | Channel | Profile dir | Notes |
49
+ | --- | --- | --- |
50
+ | xhs | `browser-profiles/xhs` | headed always; shadow-root controls via CDP box models |
51
+ | infoq | `browser-profiles/infoq` | draft save = `/draft/<id>` URL |
52
+ | juejin | `browser-profiles/juejin` | draft id from post-save URL |
53
+ | substack | `browser-profiles/substack` | robinmin.substack.com; post id in `/publish/post/<id>` |
54
+ | x | `browser-profiles/x` | article edit URL carries draft id |
55
+ | wechatmp | `browser-profiles/wechatmp` | QR login; draft id from draft/add response + editor URL |
56
+
57
+ New channel ⇒ one-time headed `fresh` login on that dir, then it becomes the cache.
58
+
59
+ ## Session probe (cached-auth validity)
60
+
61
+ Don't age-guess — probe a signed-in-only selector on the channel's console URL:
62
+
63
+ ```ts
64
+ const ok = await page.locator('<signed-in-only selector>').first().isVisible({ timeout: 10_000 });
65
+ if (!ok) throw new Error('auth=expired: fresh login required');
66
+ ```
67
+
68
+ Per-channel selectors live in the channel's `docs/design/<channel>-ui-discovery.md`.
69
+
70
+ ## storageState vs profile dir
71
+
72
+ Prefer the **profile dir** (this is what every shipped plugin uses): it caches
73
+ service workers, IndexedDB, and cookie chaos that JSON `storageState` loses and
74
+ that OAuth flows notice. Reach for `storageState` only when a tool requires a
75
+ portable JSON snapshot — export once from the live context, mark it
76
+ time-sensitive, keep it out of git, and treat expiry as probe-verified, never
77
+ assumed.
78
+
79
+ ## Failure semantics
80
+
81
+ - No observed outcome signal (toast / counter delta / URL change) ⇒ report
82
+ **failure with the reason**; never infer success from "no exception".
83
+ - Save actions assert the channel's measured signal within its measured
84
+ timeout (e.g. wechatmp `.weui-desktop-toast` ≤ 15 s) — loud on miss.
85
+ - Always `ctx.close()` on SIGTERM/exit paths; orphaned Chromiums hold the
86
+ profile lock and the next run fails confusingly.
@@ -0,0 +1,258 @@
1
+ ---
2
+ "$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
3
+ name: kk-collect
4
+ kind: state-machine
5
+ description: >-
6
+ Standalone 6-hourly (04:00/10:00/16:00/22:00 local) collection into the kk store (task 0208, feature G31): take the single-flight lock
7
+ → fan-in ingest every source (aihot + last30days + horizon) + render the core-facts markdown → upsert the
8
+ Doc[] into the store → label the new and label-invalidated items (0209; a no-op until then) → release the
9
+ lock and record the run. It never generates, renders audio or publishes; the store is the only state it
10
+ shares with the daily voice workflow, which reads it back through the extract verb.
11
+ initialState: lock
12
+ terminalStates:
13
+ - done
14
+ - skipped
15
+ - failed
16
+ failureStates:
17
+ - failed
18
+
19
+ vars:
20
+ # Effective workspace, projected by `lock` from `.spur/runs/kk-collect/work-dir.txt` (shell actions
21
+ # cannot set vars). Empty at launch: `.works/kk-collect/<stamp>` is composed in the lock state (R4).
22
+ work_dir: ""
23
+ # Run stamp, `$(date -u +%Y%m%dT%H%MZ)` by default. A caller may pin it (an in-minute re-run, a
24
+ # test); the empty default keeps the scheduled derivation authoritative.
25
+ stamp: ""
26
+ # Run id, injected by spur at launch (0674 R5 / R8; the engine's equivalent of a `run.id`
27
+ # builtin — a shell command may only reference declared vars). It is the lock owner and the
28
+ # `collect_runs` / ledger identity.
29
+ __runId: ""
30
+ # Per-source collection parameters, seeded into the plugin source configs by `lock`.
31
+ limit: "10"
32
+ last30days_topic: "AI news"
33
+ # Matches the 6h tick cadence: a trailing lookback shorter than the interval leaves a blind
34
+ # gap between ticks (dogfood 2026-10-08 P3); equal windows just dedup on re-ingest.
35
+ horizon_hours: "6"
36
+ plugins_path: ""
37
+
38
+ states:
39
+ - id: lock
40
+ description: >-
41
+ Sync the ledger, compose the run workspace and acquire the store lock — the single-flight guard that
42
+ makes two overlapping ticks one collection (R1, AC3). Writes <work_dir>/lock.json; the transition guard
43
+ routes a held lock to `skipped`.
44
+ onEnter:
45
+ - kind: shell
46
+ options:
47
+ # Reporting only: a ledger failure must never abort a tick before the lock is even taken.
48
+ onError: continue
49
+ command: >-
50
+ kk ledger sync --workflow kk-collect
51
+ - kind: shell
52
+ options:
53
+ # stamp: the pinned var, else the UTC minute (R4). work_dir: `.works/kk-collect/<stamp>` (R4),
54
+ # written to the anchor file the engine reads back into vars.work_dir below — shell actions
55
+ # cannot set vars. The stage also writes the three source configs (daily-prepare's shape),
56
+ # because `kk executor fan-in --source <plugin>:<file>` refuses a missing input file.
57
+ command: >-
58
+ kk stage collect-prepare "${vars.stamp}" "${vars.limit}" "${vars.last30days_topic}" "${vars.horizon_hours}"
59
+ - kind: file.read.into-var
60
+ options:
61
+ path: .spur/runs/kk-collect/work-dir.txt
62
+ var: work_dir
63
+ - kind: file.read.into-var
64
+ options:
65
+ path: .spur/runs/kk-collect/stamp.txt
66
+ var: stamp
67
+ - kind: shell
68
+ options:
69
+ # The claim is re-entrant for the same run and is stolen once its TTL has
70
+ # passed, so a crashed holder costs at most one tick (the TTL below covers the
71
+ # 6 h cadence and every plausible collect duration, so a slow run is never overtaken). stdout
72
+ # IS the lock contract, and the guard below reads it back:
73
+ # `{"acquired":true}` or `{"skipped":"locked","holder":<run>}` (R1).
74
+ command: >-
75
+ kk store lock acquire --name collect --run "${vars.__runId}" --ttl 21600000 > "${vars.work_dir}/lock.json"
76
+
77
+ - id: collect
78
+ description: >-
79
+ Fan-in every ingestion source into the run's blended Doc[] (same sources and timeout overrides as
80
+ kk-daily-ai-voice's collect state) and render the core-facts markdown with the existing stage (R3/R4).
81
+ onEnter:
82
+ - kind: shell
83
+ # onError: continue on both actions, with the artifact guard below as the positive success
84
+ # contract: a state-machine aborts the run on a failed action without taking a transition, so a
85
+ # hard failure here would leave the lock behind. The run still ends `failed` — through
86
+ # `release-failed`, which releases the lock first.
87
+ onError: continue
88
+ options:
89
+ # horizon-ingest's latency straddles the fan-in 30m per-source default (0098/0099), so it gets
90
+ # the same 45m leash the daily workflow pins. Writes <work_dir>/10-ingest/<stamp>_02_collect_blended.json.
91
+ # A single failed source degrades to an error entry in the merge (exit 0, surviving docs kept);
92
+ # only an all-sources failure exits 1 and leaves the file unwritten.
93
+ command: >-
94
+ kk executor fan-in
95
+ --source "aihot-ingest:${vars.work_dir}/10-ingest/source.json"
96
+ --source "last30days-ingest:${vars.work_dir}/10-ingest/last30days.json"
97
+ --source "horizon-ingest:${vars.work_dir}/10-ingest/horizon.json"
98
+ --source-timeout-override "horizon-ingest:2700000"
99
+ --out "${vars.work_dir}/10-ingest/${vars.stamp}_02_collect_blended.json"
100
+ - kind: shell
101
+ onError: continue
102
+ options:
103
+ # R4: the shipped stage, unmodified — no new code in kk-workflow-stages.ts. Writes
104
+ # <work_dir>/20-facts/<stamp>_02_collect_core-facts.md.
105
+ command: >-
106
+ kk stage daily-collect-facts "${vars.work_dir}" "${vars.stamp}" "${vars.plugins_path}"
107
+
108
+ - id: store
109
+ description: >-
110
+ Upsert the collected Doc[] into the store (items + collect_runs) and write the labelling input (R2).
111
+ A failure routes to `release-failed`: the lock must never be left behind.
112
+ onEnter:
113
+ - kind: shell
114
+ # onError: continue — a store failure is release-and-fail, not a hard abort that would keep the
115
+ # lock. The transition guard reads the result file this step writes.
116
+ onError: continue
117
+ options:
118
+ command: >-
119
+ kk store ingest --run "${vars.__runId}"
120
+ --in "${vars.work_dir}/10-ingest/${vars.stamp}_02_collect_blended.json"
121
+ --new-ids "${vars.work_dir}/new-ids.json"
122
+ > "${vars.work_dir}/store-result.json"
123
+
124
+ - id: label
125
+ description: >-
126
+ Label the stored items through the kk decision catalog (`kk store label --run … --new-ids …`,
127
+ task 0209): heuristic-first, so the model is consulted only for items the keyword pass files
128
+ `non-ai` while seeing an AI signal, and a degraded backend records the heuristic value with
129
+ `source: default` instead of failing. stdout is the `{verdict, confidence, labeled, model,
130
+ fallback}` contract and doubles as the routing guard's positive artifact: `verdict: PASS`
131
+ carries exactly one `HIGH | MEDIUM | LOW` level (the weakest level among the pass's rows,
132
+ G31 R14), and the guard below reads that line so a missing or out-of-enum level is refused
133
+ rather than read as a pass — a command that failed before printing emits no verdict at all
134
+ and is refused the same way.
135
+ onEnter:
136
+ - kind: shell
137
+ onError: continue
138
+ options:
139
+ command: >-
140
+ kk store label --run "${vars.__runId}" --new-ids "${vars.work_dir}/new-ids.json"
141
+ > "${vars.work_dir}/label-result.txt"
142
+
143
+ - id: release
144
+ description: >-
145
+ Release the lock and record the run in the ledger (R7). The append runs with onError: continue
146
+ because a run's own trace is still `running` while a state executes, so `kk ledger append` refuses
147
+ it (`kk.ledger.run.running`, exit 1); the section is written by the next tick's `lock` →
148
+ `kk ledger sync`, which is the backfill contract of docs/design/kk-collect-and-daily-voice2.md §7.
149
+ onEnter:
150
+ - kind: shell
151
+ options:
152
+ command: >-
153
+ kk store lock release --name collect --run "${vars.__runId}"
154
+ - kind: shell
155
+ onError: continue
156
+ options:
157
+ command: >-
158
+ kk ledger append --workflow kk-collect --run "${vars.__runId}"
159
+
160
+ - id: release-failed
161
+ description: >-
162
+ Release the lock on a failed run before failing: a claim left by a failed tick would stall the next
163
+ four hours of collection even though the TTL would eventually cover it.
164
+ onEnter:
165
+ - kind: shell
166
+ onError: continue
167
+ options:
168
+ command: >-
169
+ kk store lock release --name collect --run "${vars.__runId}"
170
+
171
+ - id: done
172
+ description: "Terminal — collection stored; the daily workflow reads it back through `kk store extract`"
173
+
174
+ - id: skipped
175
+ description: "Terminal — another run held the lock inside its TTL; nothing was collected or stored"
176
+
177
+ - id: failed
178
+ description: "Terminal — collection or storage failed; the lock was released on the way out"
179
+
180
+ transitions:
181
+ - from: lock
182
+ to: skipped
183
+ description: "The lock is held by another run inside its TTL -> nothing to do (exit 0, single-flight)"
184
+ guard:
185
+ kind: shell
186
+ options:
187
+ command: 'jq -e .skipped "${vars.work_dir}/lock.json" > /dev/null'
188
+
189
+ - from: lock
190
+ to: collect
191
+ description: "Lock acquired -> collect every source"
192
+ guard:
193
+ kind: always
194
+
195
+ - from: collect
196
+ to: store
197
+ description: "Blended Doc[] written and core facts rendered -> store it"
198
+ guard:
199
+ kind: shell
200
+ options:
201
+ command: >-
202
+ test -s "${vars.work_dir}/10-ingest/${vars.stamp}_02_collect_blended.json" &&
203
+ test -s "${vars.work_dir}/20-facts/${vars.stamp}_02_collect_core-facts.md"
204
+
205
+ - from: collect
206
+ to: release-failed
207
+ description: "A source set that failed outright, or a failed core-facts render -> release the lock and fail"
208
+ guard:
209
+ kind: always
210
+
211
+ - from: store
212
+ to: label
213
+ description: "Ingest reported its counts (store-result.json) -> label the new and invalidated items"
214
+ guard:
215
+ kind: shell
216
+ options:
217
+ command: 'test -s "${vars.work_dir}/store-result.json"'
218
+
219
+ - from: store
220
+ to: release-failed
221
+ description: "The store write failed -> release the lock and fail the run"
222
+ guard:
223
+ kind: always
224
+
225
+ - from: label
226
+ to: release
227
+ description: "Labels written and the pass verdict is PASS with a HIGH|MEDIUM|LOW confidence -> release and record"
228
+ guard:
229
+ kind: shell
230
+ options:
231
+ # The pass verdict is read through the same confidence-aware jq filter the judge gates use
232
+ # (byte-identical, so the one contract cannot drift): a `verdict: PASS` whose `confidence` is
233
+ # missing or outside HIGH|MEDIUM|LOW reads as `PASS!`, which no accept guard matches. The
234
+ # refusal names the offending value on stderr before the transition falls through to
235
+ # `release-failed` — fail-closed, and never a deadlock (that state is `always`).
236
+ # Kept at 3 logical commands: spur's ADR-115 composition lint errors on a guard above 5.
237
+ command: >-
238
+ test "$(jq -r 'if (.verdict // "") == "PASS" then (.verdict + (if ((.confidence // "") | test("^(HIGH|MEDIUM|LOW)$")) then "" else "!" end)) else (.verdict // "") end' "${vars.work_dir}/label-result.txt" 2>/dev/null)" = PASS ||
239
+ { echo "kk-collect.label.confidence-refused: verdict '$(jq -r '.verdict // "<none>"' "${vars.work_dir}/label-result.txt" 2>/dev/null)' carries confidence '$(jq -r '.confidence // "<absent>"' "${vars.work_dir}/label-result.txt" 2>/dev/null)', not HIGH|MEDIUM|LOW" >&2; false; }
240
+
241
+ - from: label
242
+ to: release-failed
243
+ description: "The label pass failed, or its verdict carried no HIGH|MEDIUM|LOW confidence -> release the lock and fail the run"
244
+ guard:
245
+ kind: always
246
+
247
+ - from: release
248
+ to: done
249
+ description: "Lock released and the ledger appended -> done"
250
+ guard:
251
+ kind: always
252
+
253
+ - from: release-failed
254
+ to: failed
255
+ description: "Lock released after a failed step -> failed"
256
+ terminalReason: failed-check
257
+ guard:
258
+ kind: always
@@ -52,6 +52,8 @@ vars:
52
52
  # "true" retries down AUTO_ORDER, anything else is a comma-separated provider list. The empty
53
53
  # default keeps F614's pinned look the default; availability trades against it only on opt-in.
54
54
  cover_fallback: ""
55
+ # 0219 R3: image-gen vendor fallback for the XHS render; "" restores strict fail-fast.
56
+ xhs_fallback: "true"
55
57
  # E8: XHS image-card target. Cards are planned from the lead plan item (the same source the
56
58
  # cover uses) and rendered through the pinned xhs-card/xhs-knowledge presets, then the xhs-pub
57
59
  # headed browser saves them into the creator draft box.
@@ -471,7 +473,7 @@ states:
471
473
  onError: continue
472
474
  options:
473
475
  command: >-
474
- kk stage daily-xhs-render "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.xhs_cards}" "${vars.xhs_style}" "${vars.xhs_provider}" "${vars.xhs_model}"
476
+ kk stage daily-xhs-render "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.xhs_cards}" "${vars.xhs_style}" "${vars.xhs_provider}" "${vars.xhs_model}" "${vars.xhs_fallback}"
475
477
 
476
478
  - id: xhs-publish
477
479
  description: "Publish the packaged card series to XHS — draft, or live only when xhs_live=true (ADR-022); independent and non-blocking: its outcome never enters publish_status. xhs-pub itself enforces a headless session preflight and a 2h publish/check cap (ADR-025), so a sessionless or capped run fails soft with a named reason and the run continues"
@@ -520,8 +522,12 @@ states:
520
522
  kk stage daily-publish-partial "${vars.work_dir}" "${vars.run_date}"
521
523
 
522
524
  - id: run-report
523
- description: "Assemble the per-run report (scores+aggregates+rejected counts+step timing) from existing artifacts — news-report-gen; output-only, never throws; 0117"
525
+ description: "Assemble the per-run report (scores+aggregates+rejected counts+step timing) from existing artifacts — news-report-gen; output-only, never throws; 0117; preceded by terminal publish-completeness snapshot (0220)"
524
526
  onEnter:
527
+ - kind: shell
528
+ options:
529
+ command: >-
530
+ kk stage daily-publish-completeness "${vars.work_dir}" "${vars.run_date}"
525
531
  - kind: shell
526
532
  options:
527
533
  command: >-
@@ -723,7 +729,7 @@ transitions:
723
729
 
724
730
  - from: safety-judge
725
731
  to: review
726
- description: "pass without skip_review, or escalate (RISK / evidence gap) -> operator review; skip_review never bypasses an escalation"
732
+ description: "pass without skip_review, or escalate (a finding marked disposition:\"escalate\"; legacy reports: any RISK/escalate) -> operator review; skip_review never bypasses an escalation"
727
733
  guard:
728
734
  kind: shell
729
735
  options: