@gobing-ai/knowledge-kit 0.0.31 → 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 (79) 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 +268 -1228
  30. package/plugins/kk/skills/content-judge/SKILL.md +12 -2
  31. package/plugins/kk/skills/content-judge/references/rubrics.md +8 -2
  32. package/plugins/kk/skills/content-safety/SKILL.md +60 -18
  33. package/plugins/kk/skills/web-automation/SKILL.md +100 -0
  34. package/plugins/kk/skills/web-automation/references/browser-launch.md +86 -0
  35. package/plugins/kk/workflows/kk-collect.yaml +258 -0
  36. package/plugins/kk/workflows/kk-daily-ai-voice.yaml +74 -11
  37. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +655 -0
  38. package/plugins/kk/workflows/kk-daily-safety.yaml +170 -0
  39. package/plugins/publishings/emdash-pub/dist/index.js +10 -9
  40. package/plugins/publishings/infoq-pub/dist/index.js +8695 -211
  41. package/plugins/publishings/infoq-pub/package.json +3 -1
  42. package/plugins/publishings/infoq-pub/plugin.json +2 -1
  43. package/plugins/publishings/infoq-pub/src/index.ts +358 -29
  44. package/plugins/publishings/infoq-pub/src/profile-check.ts +21 -0
  45. package/plugins/publishings/juejin-pub/dist/index.js +8746 -211
  46. package/plugins/publishings/juejin-pub/package.json +3 -1
  47. package/plugins/publishings/juejin-pub/plugin.json +2 -1
  48. package/plugins/publishings/juejin-pub/src/index.ts +345 -29
  49. package/plugins/publishings/juejin-pub/src/profile-check.ts +19 -0
  50. package/plugins/publishings/medium-pub/dist/index.js +10 -9
  51. package/plugins/publishings/podcast-pub/dist/index.js +10 -9
  52. package/plugins/publishings/qiita-pub/dist/index.js +10 -9
  53. package/plugins/publishings/substack-pub/dist/index.js +8687 -211
  54. package/plugins/publishings/substack-pub/package.json +3 -1
  55. package/plugins/publishings/substack-pub/plugin.json +2 -1
  56. package/plugins/publishings/substack-pub/src/index.ts +302 -29
  57. package/plugins/publishings/substack-pub/src/profile-check.ts +76 -0
  58. package/plugins/publishings/surfdash-pub/dist/index.js +10 -9
  59. package/plugins/publishings/wechatmp-pub/dist/index.js +1315 -352
  60. package/plugins/publishings/wechatmp-pub/plugin.json +1 -0
  61. package/plugins/publishings/wechatmp-pub/src/enrich.ts +229 -0
  62. package/plugins/publishings/wechatmp-pub/src/flow.ts +896 -0
  63. package/plugins/publishings/wechatmp-pub/src/index.ts +223 -520
  64. package/plugins/publishings/wechatmp-pub/src/modes.ts +403 -0
  65. package/plugins/publishings/wechatmp-pub/src/page.ts +260 -0
  66. package/plugins/publishings/wechatmp-pub/src/profile-check.ts +84 -38
  67. package/plugins/publishings/x-pub/dist/index.js +8707 -223
  68. package/plugins/publishings/x-pub/package.json +3 -1
  69. package/plugins/publishings/x-pub/plugin.json +2 -1
  70. package/plugins/publishings/x-pub/src/index.ts +315 -23
  71. package/plugins/publishings/x-pub/src/profile-check.ts +19 -0
  72. package/plugins/publishings/xhs-pub/dist/index.js +897 -354
  73. package/plugins/publishings/xhs-pub/plugin.json +2 -1
  74. package/plugins/publishings/xhs-pub/src/flow.ts +572 -0
  75. package/plugins/publishings/xhs-pub/src/index.ts +108 -596
  76. package/plugins/publishings/xhs-pub/src/modes.ts +300 -0
  77. package/plugins/publishings/xhs-pub/src/page.ts +254 -0
  78. package/plugins/publishings/xhs-pub/src/profile-check.ts +29 -17
  79. package/plugins/publishings/zenn-pub/dist/index.js +10 -9
@@ -55,8 +55,12 @@ Every evaluation writes a JSON verdict file. The default path is
55
55
  | `FAIL` | Content violates a must-pass criterion (safety, accuracy, compliance) | Block; surface findings; do not publish |
56
56
  | `NEEDS_REVISION` | Content is salvageable but below bar on quality criteria | Block; feedback drives a bounded regenerate-retry loop |
57
57
 
58
- `PASS` is the only verdict that unblocks publish. The shell guard treats `FAIL` and
59
- `NEEDS_REVISION` identically (exit 1); the distinction is for the feedback loop, not the gate.
58
+ `PASS` is the only verdict that unblocks publish outright. The shell guard treats `FAIL` and
59
+ `NEEDS_REVISION` identically (exit 1); the distinction is for the feedback loop, not the gate — a
60
+ workflow may spend a bounded revision budget on either and then classify the residue (20261005:
61
+ `kk-daily-ai-voice` routes non-PASS through `safety-revise` and publishes only the targets whose
62
+ platform did not BLOCK). A `fail` means a rule was *violated*, not merely suspected: see
63
+ "Evidence discipline".
60
64
 
61
65
  ### Confidence level
62
66
 
@@ -113,6 +117,12 @@ and `escalate` flag; treat `LOW` as a signal to demand human review before publi
113
117
  contradicted by source Y" is.
114
118
  - Never fabricate sources. If a claim cannot be verified against the provided docs/brief, mark it
115
119
  `warn` (unverified), not `fail`, unless the rubric classifies unverified claims as must-pass.
120
+ - **A rule you cannot verify is not a rule you may `fail` on.** When a finding rests on rule text
121
+ that is unverified, out of date, or an editorial stance rather than platform/governing text — the
122
+ upstream report marks these `evidenceGap` — grade the criterion `warn` and escalate. `fail` is
123
+ reserved for a violation of rule text you actually verified. Downgrading here is what keeps a
124
+ gate a gate: an unverifiable must-pass `fail` is unanswerable, so it can neither be remediated nor
125
+ honestly cleared.
116
126
 
117
127
  ## Rubrics
118
128
 
@@ -89,8 +89,14 @@ Thresholds: `pass_threshold = 0.85`
89
89
  - `no-prohibited-claims` is strict: a single prohibited claim is an immediate `FAIL`, regardless
90
90
  of overall score. Examples: unverified health/financial advice, guarantees of outcome,
91
91
  - `jurisdictional-rules`: the wrapper supplies the jurisdiction (e.g., `US-SEC`, `EU-GDPR`,
92
- `CN-CAC`). If none is supplied, mark `warn` and note "no jurisdiction specified" — do not
93
- guess a jurisdiction and fail against a guessed rule.
92
+ `CN-CAC`, or the platform reference files a distribution gate names). If none is supplied, mark
93
+ `warn` and note "no jurisdiction specified" — do not guess a jurisdiction and fail against a
94
+ guessed rule.
95
+ Grade `fail` only against rule text verified for that jurisdiction. A rule the wrapper or its
96
+ process marks `evidenceGap` (unverified text, a stale `Last verified` date, or an editorial
97
+ stance rather than platform/governing text) is `warn` + escalate: unverifiable rule text cannot
98
+ be a must-pass failure, because no revision can clear it (20261005 — the daily gate blocked
99
+ every run on the evidence-gapped CN 资质口径 stance until this was fixed).
94
100
  - `data-handling`: scan for PII that may have been carried through from input Docs (emails,
95
101
  phone numbers, national IDs). Leakage is a `fail`.
96
102
 
@@ -45,7 +45,9 @@ prefer exclusion or abort over any rewording.
45
45
  - **detect-only** (default): emit the report; touch nothing.
46
46
  - **detect+revise**: additionally emit a proposed revision for findings in categories 3–5 where
47
47
  the fix is deletion/desensitization/disclosure. Category-2 findings get exclusion (drop the
48
- item) or abort — prefer exclusion over any rewrite.
48
+ item) or abort — prefer exclusion over any rewrite. The `kk-daily-ai-voice` `safety-revise`
49
+ state calls this mode: the revision proposals are what the agent applies to the drafts before the
50
+ re-review (the platform level is what gets excluded when a category-2 finding survives).
49
51
 
50
52
  A revision proposal is a **proposal**. The operator/agent that owns the artifact applies it —
51
53
  keep originals intact and write the revision to a new artifact — and the revised artifact must
@@ -65,13 +67,14 @@ it as a category-4 finding (quoted as evidence) and apply the decision rules exa
65
67
 
66
68
  A platform file is a snapshot. When its `Last verified` date is older than the content's run
67
69
  date by more than 90 days, or a takedown/feedback event post-dates it, treat its rules as
68
- `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.
69
71
 
70
72
  ## Decision per platform
71
73
 
72
74
  For each requested platform, load `references/platforms/<platform>.md` and decide. A requested
73
75
  platform with **no reference file** is unsupported: emit `BLOCK` with `evidenceGap: true` and
74
- `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.
75
78
 
76
79
  - `OK` — no finding reaches a block rule.
77
80
  - `RISK` — findings exist but are warn-grade (e.g. evidence gap on this platform); submission is
@@ -82,8 +85,9 @@ platform with **no reference file** is unsupported: emit `BLOCK` with `evidenceG
82
85
  never offsets a high-risk finding.**
83
86
 
84
87
  If the platform file's evidence status is `unknown`/`unverified` for the rule a finding rests on,
85
- the finding is reported with `evidenceGap: true` and the decision escalates to manual review —
86
- 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).
87
91
 
88
92
  ## Output contract
89
93
 
@@ -94,6 +98,7 @@ One JSON report per invocation, written to the path the caller provides (default
94
98
  {
95
99
  "content_ref": "<path or id of reviewed Content>",
96
100
  "mode": "detect-only | detect+revise",
101
+ "confidence": "HIGH | MEDIUM | LOW",
97
102
  "platforms": [
98
103
  {
99
104
  "platform": "<reference file name>",
@@ -104,7 +109,8 @@ One JSON report per invocation, written to the path the caller provides (default
104
109
  "item": "<platform review-item id, or generic>",
105
110
  "evidence": "<quoted passage / location>",
106
111
  "detail": "<why this is a finding>",
107
- "evidenceGap": false
112
+ "evidenceGap": false,
113
+ "disposition": "note | fix | gap | escalate"
108
114
  }
109
115
  ],
110
116
  "revision": {
@@ -124,14 +130,39 @@ One JSON report per invocation, written to the path the caller provides (default
124
130
  `escalate: true` + printed report path is required whenever evidence is missing for a finding's
125
131
  rule. Never emit `OK` with `evidenceGap` findings; that combination is a contract violation.
126
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
+
127
157
  ## Relation to content-judge
128
158
 
129
159
  - content-judge scores **quality**; content-safety decides **admission risk**. Different questions.
130
160
  - After a detect+revise revision is applied, the revised Content is re-evaluated by content-judge
131
161
  (rubric `compliance`) — a revision **never auto-PASSes**; the previous verdict is stale and the
132
162
  gate re-runs.
133
- - A content-judge `FAIL` on safety is terminal for that artifact in the pipeline: FAIL /
134
- NEEDS_REVISION / missing-verdict states must not auto-publish.
163
+ - A content-judge verdict never publishes on its own: the workflow's gate decides. `kk-daily-ai-voice`
164
+ spends a bounded revision budget on `FAIL`/`NEEDS_REVISION`, blocks only findings that travel with
165
+ the draft, and excludes the admission-blocked platforms instead of halting the run (20261005).
135
166
 
136
167
  ## Inputs
137
168
 
@@ -149,15 +180,26 @@ credentials, and no platform word lists are embedded in any caller or in TypeScr
149
180
 
150
181
  ## Workflow integration
151
182
 
152
- The `kk-daily-ai-voice` workflow runs this skill in `detect-only` mode on every publishing run
153
- (`safety-review` state, task 0189 R10): it reads the review request for the target `platforms`
154
- and writes the report JSON; `kk:content-judge` (`compliance` rubric) then re-checks the same
155
- drafts independently, and the `daily-safety-gate` stage consumes both. The program reads only
156
- `platforms[].decision`, `escalate`, and the judge's `verdict` — a `BLOCK`, a non-`PASS` verdict,
157
- or a missing/unreadable file stops the run; a `RISK` or `escalate: true` forces the human review
158
- even when the run is otherwise unattended. The report is bound to the exact draft bytes: editing a
159
- draft after the verdict invalidates it. The earlier deterministic gate is the topic-boundary QC
160
- stage (`plugins/kk/scripts/topic-boundary.json` via `daily-qc-content`); this skill covers the
161
- judgment-shaped residue that gate cannot express. Step shape:
183
+ The `kk-daily-ai-voice` workflow runs this skill on every publishing run (task 0189 R10; revise
184
+ loop 20261005): `safety-review` reads the request for the target `platforms` and writes the report
185
+ (`detect-only`); `kk:content-judge` (`compliance` rubric) re-checks the same drafts independently;
186
+ then `daily-safety-gate` consumes both. The gate **alerts and adjusts rather than stopping the run**:
187
+ a non-PASS verdict spends a bounded budget in the `safety-revise` state, where this skill runs in
188
+ `detect+revise` mode and the agent applies the proposals to the article and the VoiceScript before
189
+ `safety-review` re-opens on the edited bytes. What survives the budget is classified — findings that
190
+ travel with the draft still block (a PII finding on a blocked platform, or a judge must-pass `fail`
191
+ outside the admission criteria: prohibited claims / disclosure / data-handling), while
192
+ **admission-only findings** (the CN 资质口径 / 播报形态 rules) drop the platform
193
+ they name from that run and let every other target publish; the drop is recorded in the verdict
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.
199
+
200
+ The report is bound to the exact draft bytes: editing a draft after the verdict invalidates it, and
201
+ the remediation loop re-requests a fresh review for that reason. The earlier deterministic gate is
202
+ the topic-boundary QC stage (`plugins/kk/scripts/topic-boundary.json` via `daily-qc-content`); this
203
+ skill covers the judgment-shaped residue that gate cannot express. Step shape:
162
204
  `plugins/kk/skills/content-judge/references/workflow-integration.md`. Outside the workflow the
163
205
  skill stays usable ad hoc on any draft.
@@ -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