@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/dist/index.js +32320 -15021
- 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 +14 -13
- package/plugins/generations/daily-article-gen/src/index.ts +1 -1
- package/plugins/generations/dailynews-gen/dist/index.js +10 -9
- 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 +154 -151
- 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 +21 -0
- package/plugins/kk/decisions/kk-items.yaml +113 -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 +1533 -0
- package/plugins/kk/scripts/kk-workflow-stages.ts +268 -1228
- package/plugins/kk/skills/content-judge/SKILL.md +12 -2
- package/plugins/kk/skills/content-judge/references/rubrics.md +8 -2
- package/plugins/kk/skills/content-safety/SKILL.md +60 -18
- 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 +258 -0
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +74 -11
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +655 -0
- package/plugins/kk/workflows/kk-daily-safety.yaml +170 -0
- package/plugins/publishings/emdash-pub/dist/index.js +10 -9
- package/plugins/publishings/infoq-pub/dist/index.js +8695 -211
- package/plugins/publishings/infoq-pub/package.json +3 -1
- package/plugins/publishings/infoq-pub/plugin.json +2 -1
- package/plugins/publishings/infoq-pub/src/index.ts +358 -29
- package/plugins/publishings/infoq-pub/src/profile-check.ts +21 -0
- package/plugins/publishings/juejin-pub/dist/index.js +8746 -211
- package/plugins/publishings/juejin-pub/package.json +3 -1
- package/plugins/publishings/juejin-pub/plugin.json +2 -1
- package/plugins/publishings/juejin-pub/src/index.ts +345 -29
- package/plugins/publishings/juejin-pub/src/profile-check.ts +19 -0
- 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 +8687 -211
- package/plugins/publishings/substack-pub/package.json +3 -1
- package/plugins/publishings/substack-pub/plugin.json +2 -1
- package/plugins/publishings/substack-pub/src/index.ts +302 -29
- package/plugins/publishings/substack-pub/src/profile-check.ts +76 -0
- package/plugins/publishings/surfdash-pub/dist/index.js +10 -9
- package/plugins/publishings/wechatmp-pub/dist/index.js +1315 -352
- package/plugins/publishings/wechatmp-pub/plugin.json +1 -0
- package/plugins/publishings/wechatmp-pub/src/enrich.ts +229 -0
- package/plugins/publishings/wechatmp-pub/src/flow.ts +896 -0
- package/plugins/publishings/wechatmp-pub/src/index.ts +223 -520
- package/plugins/publishings/wechatmp-pub/src/modes.ts +403 -0
- package/plugins/publishings/wechatmp-pub/src/page.ts +260 -0
- package/plugins/publishings/wechatmp-pub/src/profile-check.ts +84 -38
- package/plugins/publishings/x-pub/dist/index.js +8707 -223
- package/plugins/publishings/x-pub/package.json +3 -1
- package/plugins/publishings/x-pub/plugin.json +2 -1
- package/plugins/publishings/x-pub/src/index.ts +315 -23
- package/plugins/publishings/x-pub/src/profile-check.ts +19 -0
- package/plugins/publishings/xhs-pub/dist/index.js +897 -354
- package/plugins/publishings/xhs-pub/plugin.json +2 -1
- package/plugins/publishings/xhs-pub/src/flow.ts +572 -0
- package/plugins/publishings/xhs-pub/src/index.ts +108 -596
- package/plugins/publishings/xhs-pub/src/modes.ts +300 -0
- package/plugins/publishings/xhs-pub/src/page.ts +254 -0
- package/plugins/publishings/xhs-pub/src/profile-check.ts +29 -17
- 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
|
|
93
|
-
guess a jurisdiction and fail against a
|
|
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`,
|
|
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
|
-
`
|
|
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
|
|
86
|
-
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).
|
|
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
|
|
134
|
-
NEEDS_REVISION
|
|
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
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|