@gobing-ai/knowledge-kit 0.0.32 → 0.0.34
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 +43047 -24724
- package/package.json +4 -4
- package/plugins/generations/content-gen/dist/index.js +10 -9
- package/plugins/generations/core-facts-gen/dist/index.js +13 -12
- package/plugins/generations/daily-article-gen/dist/index.js +16 -21
- package/plugins/generations/daily-article-gen/src/index.ts +8 -12
- package/plugins/generations/dailynews-gen/dist/index.js +23 -21
- package/plugins/generations/dailynews-gen/src/script-builder.ts +25 -26
- package/plugins/generations/episode-plan-gen/dist/index.js +58 -15
- package/plugins/generations/episode-plan-gen/src/index.ts +76 -6
- package/plugins/generations/gemini-tts/README.md +11 -0
- package/plugins/generations/gemini-tts/dist/index.js +10 -9
- package/plugins/generations/image-gen/dist/index.js +171 -153
- package/plugins/generations/image-gen/presets/formats/xhs-card.yaml +16 -14
- package/plugins/generations/image-gen/presets/styles/xhs-knowledge.yaml +2 -2
- package/plugins/generations/image-gen/src/compose/card.ts +17 -2
- package/plugins/generations/image-gen/src/compose/chromium.ts +60 -42
- package/plugins/generations/news-report-gen/dist/index.js +13 -12
- package/plugins/generations/voice-gen/dist/index.js +10 -9
- package/plugins/ingestions/aihot-ingest/dist/index.js +13 -12
- package/plugins/ingestions/horizon-ingest/dist/index.js +13 -12
- package/plugins/ingestions/karakeep-local/dist/index.js +13 -13
- package/plugins/ingestions/last30days-ingest/dist/index.js +13 -12
- package/plugins/ingestions/web-search/dist/index.js +11 -10
- package/plugins/kk/README.md +38 -0
- package/plugins/kk/decisions/extract/ai-news.yaml +27 -0
- package/plugins/kk/decisions/kk-items.yaml +179 -0
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/schedules/cron.tmpl +15 -0
- package/plugins/kk/schedules/launchd.plist.tmpl +77 -0
- package/plugins/kk/scripts/daily-stages.ts +2023 -0
- package/plugins/kk/scripts/daily-voice-style.zh.md +15 -10
- package/plugins/kk/scripts/kk-workflow-stages.ts +253 -1368
- package/plugins/kk/skills/content-safety/SKILL.md +39 -8
- package/plugins/kk/skills/content-safety/references/platforms/apple-podcasts.md +2 -2
- package/plugins/kk/skills/web-automation/SKILL.md +100 -0
- package/plugins/kk/skills/web-automation/references/browser-launch.md +86 -0
- package/plugins/kk/workflows/kk-collect.yaml +256 -0
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +9 -3
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +774 -0
- package/plugins/kk/workflows/kk-daily-safety.yaml +167 -0
- package/plugins/publishings/emdash-pub/dist/index.js +10 -9
- package/plugins/publishings/infoq-pub/dist/index.js +152 -59
- package/plugins/publishings/infoq-pub/plugin.json +1 -0
- package/plugins/publishings/infoq-pub/src/index.ts +167 -55
- package/plugins/publishings/infoq-pub/src/profile-check.ts +4 -2
- package/plugins/publishings/juejin-pub/dist/index.js +111 -30
- package/plugins/publishings/juejin-pub/plugin.json +1 -0
- package/plugins/publishings/juejin-pub/src/index.ts +49 -9
- package/plugins/publishings/medium-pub/dist/index.js +10 -9
- package/plugins/publishings/podcast-pub/dist/index.js +10 -9
- package/plugins/publishings/qiita-pub/dist/index.js +10 -9
- package/plugins/publishings/substack-pub/dist/index.js +105 -28
- package/plugins/publishings/substack-pub/plugin.json +1 -0
- package/plugins/publishings/substack-pub/src/index.ts +39 -8
- package/plugins/publishings/substack-pub/src/profile-check.ts +2 -0
- package/plugins/publishings/surfdash-pub/dist/index.js +10 -9
- package/plugins/publishings/wechatmp-pub/dist/index.js +115 -31
- package/plugins/publishings/wechatmp-pub/plugin.json +1 -0
- package/plugins/publishings/wechatmp-pub/src/index.ts +42 -9
- package/plugins/publishings/wechatmp-pub/src/profile-check.ts +84 -40
- package/plugins/publishings/x-pub/dist/index.js +114 -35
- package/plugins/publishings/x-pub/plugin.json +1 -0
- package/plugins/publishings/x-pub/src/index.ts +68 -14
- package/plugins/publishings/xhs-pub/dist/index.js +154 -59
- package/plugins/publishings/xhs-pub/plugin.json +1 -0
- package/plugins/publishings/xhs-pub/src/flow.ts +9 -3
- package/plugins/publishings/xhs-pub/src/index.ts +119 -9
- package/plugins/publishings/xhs-pub/src/page.ts +4 -17
- package/plugins/publishings/xhs-pub/src/profile-check.ts +28 -15
- package/plugins/publishings/zenn-pub/dist/index.js +10 -9
|
@@ -38,7 +38,7 @@ prefer exclusion or abort over any rewording.
|
|
|
38
38
|
| 2 | 语义 / 资质风险 | Political, diplomatic/trade-control, military, public-policy-dispute, or social-event mainline; content implying news-reporting qualifications the operator does not hold | BLOCK (exclude or abort). No rewrite that disguises the mainline |
|
|
39
39
|
| 3 | 可识别个人信息 | Names/IDs/contacts/addresses/health or financial details of private individuals | Desensitize before publish; keep the original values out of the report |
|
|
40
40
|
| 4 | 未经核实断言 / 来源保真 | Claims resting on RSS titles, aggregation-page URLs, unknown model names, self-contradictory dates; invented facts | Remove the claim or downgrade to attributed uncertainty; insufficient material → shorten or stop, never pad |
|
|
41
|
-
| 5 | 必要披露 | AI-generation disclosure, identity/experience truthfulness (only real hands-on experience claimed) | Missing disclosure → revise to add; false identity claim → BLOCK |
|
|
41
|
+
| 5 | 必要披露 | AI-generation disclosure, identity/experience truthfulness (only real hands-on experience claimed) | Missing disclosure → revise to add, unless that platform item is graded `note` (do not add it back); false identity claim → BLOCK |
|
|
42
42
|
|
|
43
43
|
## Modes
|
|
44
44
|
|
|
@@ -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`,
|
|
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
|
-
`
|
|
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
|
|
88
|
-
a silent auto-`OK`
|
|
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.
|
|
167
|
-
|
|
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
|
|
@@ -9,7 +9,7 @@ Review-item IDs here are **this skill's bookkeeping numbers**, not official plat
|
|
|
9
9
|
| Effective date | unknown(页面未标注版本生效日期) |
|
|
10
10
|
| Last verified | 2026-09-30 — §1.1、§1.11、§1.12 已读原文;其余条款(§1.2–§1.10、§2 起)仅见标题,未逐条核验 |
|
|
11
11
|
| Evidence status | §1.1 / §1.11 / §1.12 为**官方原文**(下引);其余条款未核验 → 引用时标 `evidenceGap: true` |
|
|
12
|
-
| Changelog | 2026-09-29 — file created (task 0189) · 2026-09-30 — 深链与 §1.1/§1.11/§1.12 原文核验,新增 AP-05/AP-06 |
|
|
12
|
+
| Changelog | 2026-09-29 — file created (task 0189) · 2026-09-30 — 深链与 §1.1/§1.11/§1.12 原文核验,新增 AP-05/AP-06 · 2026-10-08 — 操作者裁定:单集文末统一 AI 声明撤下,AP-05 改为 note |
|
|
13
13
|
|
|
14
14
|
## Evidence distinctions
|
|
15
15
|
|
|
@@ -28,7 +28,7 @@ Review-item IDs here are **this skill's bookkeeping numbers**, not official plat
|
|
|
28
28
|
| AP-02 | 官方 guidelines 的内容类别限制(露骨、暴力、侵权等)— 条文未核验,引用时 evidenceGap | judge |
|
|
29
29
|
| AP-03 | 元数据保真:标题/简介/分类不得与实际音频内容不符(官方 §1.1;通用层类别 4/5) | fix |
|
|
30
30
|
| AP-04 | 可识别个人信息(通用层类别 3 直映射) | desensitize |
|
|
31
|
-
| AP-05 | AI
|
|
31
|
+
| AP-05 | 本栏目的制作方式由频道/栏目设置说明。不要因为正文、口播或 show notes 缺少「文字由 AI 辅助整理,音频为 AI 合成配音」「本期音频为 AI 合成配音」或 “The audio in this episode is AI-synthesized.” 而记 fix,也不要提议把这些句子加回去(操作者裁定 2026-10-08)。假实测、假身份仍按 §1.1 / 类别 5 block。 | note |
|
|
32
32
|
| AP-06 | 不得用 AI 编造新闻或虚构真实事件(官方 §1.12;通用层类别 4)。无一手来源的「新闻」断言 → block | block |
|
|
33
33
|
|
|
34
34
|
## Open questions / escalation
|
|
@@ -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,256 @@
|
|
|
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 (R7). The run's ledger section is deliberately NOT appended here: a run's own
|
|
146
|
+
trace is still `running` while a state executes, so `kk ledger append` unconditionally refuses it
|
|
147
|
+
(`kk.ledger.run.running`, exit 1, "nothing was written"). Carrying the dead action only cost a
|
|
148
|
+
failed step — every green run reported `release | failed` in ledger.md (dogfood 2026-10-09,
|
|
149
|
+
7/7 runs), which trained the operator to read the ledger as broken. The section is written by
|
|
150
|
+
the next tick's `lock` → `kk ledger sync`, which is the backfill contract of
|
|
151
|
+
docs/design/kk-collect-and-daily-voice2.md §7; an operator may also append a finished run by hand.
|
|
152
|
+
onEnter:
|
|
153
|
+
- kind: shell
|
|
154
|
+
options:
|
|
155
|
+
command: >-
|
|
156
|
+
kk store lock release --name collect --run "${vars.__runId}"
|
|
157
|
+
|
|
158
|
+
- id: release-failed
|
|
159
|
+
description: >-
|
|
160
|
+
Release the lock on a failed run before failing: a claim left by a failed tick would stall the next
|
|
161
|
+
four hours of collection even though the TTL would eventually cover it.
|
|
162
|
+
onEnter:
|
|
163
|
+
- kind: shell
|
|
164
|
+
onError: continue
|
|
165
|
+
options:
|
|
166
|
+
command: >-
|
|
167
|
+
kk store lock release --name collect --run "${vars.__runId}"
|
|
168
|
+
|
|
169
|
+
- id: done
|
|
170
|
+
description: "Terminal — collection stored; the daily workflow reads it back through `kk store extract`"
|
|
171
|
+
|
|
172
|
+
- id: skipped
|
|
173
|
+
description: "Terminal — another run held the lock inside its TTL; nothing was collected or stored"
|
|
174
|
+
|
|
175
|
+
- id: failed
|
|
176
|
+
description: "Terminal — collection or storage failed; the lock was released on the way out"
|
|
177
|
+
|
|
178
|
+
transitions:
|
|
179
|
+
- from: lock
|
|
180
|
+
to: skipped
|
|
181
|
+
description: "The lock is held by another run inside its TTL -> nothing to do (exit 0, single-flight)"
|
|
182
|
+
guard:
|
|
183
|
+
kind: shell
|
|
184
|
+
options:
|
|
185
|
+
command: 'jq -e .skipped "${vars.work_dir}/lock.json" > /dev/null'
|
|
186
|
+
|
|
187
|
+
- from: lock
|
|
188
|
+
to: collect
|
|
189
|
+
description: "Lock acquired -> collect every source"
|
|
190
|
+
guard:
|
|
191
|
+
kind: always
|
|
192
|
+
|
|
193
|
+
- from: collect
|
|
194
|
+
to: store
|
|
195
|
+
description: "Blended Doc[] written and core facts rendered -> store it"
|
|
196
|
+
guard:
|
|
197
|
+
kind: shell
|
|
198
|
+
options:
|
|
199
|
+
command: >-
|
|
200
|
+
test -s "${vars.work_dir}/10-ingest/${vars.stamp}_02_collect_blended.json" &&
|
|
201
|
+
test -s "${vars.work_dir}/20-facts/${vars.stamp}_02_collect_core-facts.md"
|
|
202
|
+
|
|
203
|
+
- from: collect
|
|
204
|
+
to: release-failed
|
|
205
|
+
description: "A source set that failed outright, or a failed core-facts render -> release the lock and fail"
|
|
206
|
+
guard:
|
|
207
|
+
kind: always
|
|
208
|
+
|
|
209
|
+
- from: store
|
|
210
|
+
to: label
|
|
211
|
+
description: "Ingest reported its counts (store-result.json) -> label the new and invalidated items"
|
|
212
|
+
guard:
|
|
213
|
+
kind: shell
|
|
214
|
+
options:
|
|
215
|
+
command: 'test -s "${vars.work_dir}/store-result.json"'
|
|
216
|
+
|
|
217
|
+
- from: store
|
|
218
|
+
to: release-failed
|
|
219
|
+
description: "The store write failed -> release the lock and fail the run"
|
|
220
|
+
guard:
|
|
221
|
+
kind: always
|
|
222
|
+
|
|
223
|
+
- from: label
|
|
224
|
+
to: release
|
|
225
|
+
description: "Labels written and the pass verdict is PASS with a HIGH|MEDIUM|LOW confidence -> release and record"
|
|
226
|
+
guard:
|
|
227
|
+
kind: shell
|
|
228
|
+
options:
|
|
229
|
+
# The pass verdict is read through the same confidence-aware jq filter the judge gates use
|
|
230
|
+
# (byte-identical, so the one contract cannot drift): a `verdict: PASS` whose `confidence` is
|
|
231
|
+
# missing or outside HIGH|MEDIUM|LOW reads as `PASS!`, which no accept guard matches. The
|
|
232
|
+
# refusal names the offending value on stderr before the transition falls through to
|
|
233
|
+
# `release-failed` — fail-closed, and never a deadlock (that state is `always`).
|
|
234
|
+
# Kept at 3 logical commands: spur's ADR-115 composition lint errors on a guard above 5.
|
|
235
|
+
command: >-
|
|
236
|
+
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 ||
|
|
237
|
+
{ 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; }
|
|
238
|
+
|
|
239
|
+
- from: label
|
|
240
|
+
to: release-failed
|
|
241
|
+
description: "The label pass failed, or its verdict carried no HIGH|MEDIUM|LOW confidence -> release the lock and fail the run"
|
|
242
|
+
guard:
|
|
243
|
+
kind: always
|
|
244
|
+
|
|
245
|
+
- from: release
|
|
246
|
+
to: done
|
|
247
|
+
description: "Lock released and the ledger appended -> done"
|
|
248
|
+
guard:
|
|
249
|
+
kind: always
|
|
250
|
+
|
|
251
|
+
- from: release-failed
|
|
252
|
+
to: failed
|
|
253
|
+
description: "Lock released after a failed step -> failed"
|
|
254
|
+
terminalReason: failed-check
|
|
255
|
+
guard:
|
|
256
|
+
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 (
|
|
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:
|