@gobing-ai/knowledge-kit 0.0.5 → 0.0.7
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/dist/index.js +4 -2
- package/package.json +1 -1
- package/plugins/generations/content-gen/package.json +2 -1
- package/plugins/ingestions/web-search/src/index.ts +169 -9
- package/plugins/kk/README.md +5 -0
- package/plugins/kk/agents/{kk-judge-compliance.md → judge-compliance.md} +8 -8
- package/plugins/kk/agents/{kk-judge-tech.md → judge-tech.md} +8 -8
- package/plugins/kk/agents/{kk-judge-tone.md → judge-tone.md} +8 -8
- package/plugins/kk/commands/workflow-run.md +130 -0
- package/plugins/kk/scripts/render-md.ts +145 -0
- package/plugins/kk/skills/{kk-judge → judge}/SKILL.md +4 -4
- package/plugins/kk/skills/{kk-judge → judge}/references/rubrics.md +2 -2
- package/plugins/kk/skills/{kk-judge → judge}/references/workflow-integration.md +8 -8
- package/plugins/kk/skills/storm-research/SKILL.md +166 -0
- package/plugins/kk/skills/{kk-topic → topic}/SKILL.md +4 -4
- package/plugins/kk/workflows/kk-storm-research.yaml +184 -0
package/dist/index.js
CHANGED
|
@@ -16682,6 +16682,8 @@ function isDirectory(path2) {
|
|
|
16682
16682
|
function getByName(result, name) {
|
|
16683
16683
|
return result.plugins.find((p) => p.name === name);
|
|
16684
16684
|
}
|
|
16685
|
+
// ../../packages/kk-core/src/render-md.ts
|
|
16686
|
+
if (false) {}
|
|
16685
16687
|
// src/fanin.ts
|
|
16686
16688
|
var EXIT_USAGE = 2;
|
|
16687
16689
|
var EXIT_INPUT = 1;
|
|
@@ -17438,7 +17440,7 @@ function createProgram() {
|
|
|
17438
17440
|
}
|
|
17439
17441
|
|
|
17440
17442
|
// src/main.ts
|
|
17441
|
-
async function
|
|
17443
|
+
async function main2(argv = process.argv, options = {}) {
|
|
17442
17444
|
const program2 = createProgram();
|
|
17443
17445
|
program2.exitOverride();
|
|
17444
17446
|
program2.configureOutput({
|
|
@@ -17468,5 +17470,5 @@ async function main(argv = process.argv, options = {}) {
|
|
|
17468
17470
|
|
|
17469
17471
|
// src/index.ts
|
|
17470
17472
|
if (import.meta.main) {
|
|
17471
|
-
process.exit(await
|
|
17473
|
+
process.exit(await main2());
|
|
17472
17474
|
}
|
package/package.json
CHANGED
|
@@ -40,11 +40,161 @@ const FIXTURE_ENTRIES: FixtureEntry[] = [
|
|
|
40
40
|
},
|
|
41
41
|
];
|
|
42
42
|
|
|
43
|
-
/** Deterministic Doc id: sha256(sourceUri) hex prefix, same idea as karakeep-local (0054 Block 4).
|
|
44
|
-
|
|
43
|
+
/** Deterministic Doc id: sha256(sourceUri) hex prefix, same idea as karakeep-local (0054 Block 4).
|
|
44
|
+
* Exported so 0059's local-Doc mapping can stay consistent if a later helper imports it (R3). */
|
|
45
|
+
export function computeId(uri: string): string {
|
|
45
46
|
return createHash('sha256').update(uri).digest('hex').slice(0, 16);
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/** Fetch signature the live path accepts, so tests can inject a fake (CI never hits the network). */
|
|
50
|
+
export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
51
|
+
|
|
52
|
+
/** Live-path dependencies — fetch, env, and clock are all injectable for tests. */
|
|
53
|
+
export interface SearchDeps {
|
|
54
|
+
fetch: FetchLike;
|
|
55
|
+
env: Record<string, string | undefined>;
|
|
56
|
+
now: () => Date;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** One Firecrawl `data.web[]` item — the subset of fields 0054 Block 3/4 consume. */
|
|
60
|
+
interface FirecrawlWebItem {
|
|
61
|
+
url?: unknown;
|
|
62
|
+
title?: unknown;
|
|
63
|
+
markdown?: unknown;
|
|
64
|
+
position?: unknown;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface FirecrawlSearchResponse {
|
|
68
|
+
success?: unknown;
|
|
69
|
+
error?: unknown;
|
|
70
|
+
data?: { web?: FirecrawlWebItem[] | null } | null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Live Firecrawl search → Doc[] (0054 Blocks 2–5). Throws an Error whose message is the
|
|
75
|
+
* fail-loud diagnostic; `main` prefixes it with `web-search failed: ` and returns 1.
|
|
76
|
+
* Never writes `--out` on a thrown path (no DocListSchema.parse, no file write here).
|
|
77
|
+
*/
|
|
78
|
+
export async function searchFirecrawl(input: InInput, deps: SearchDeps): Promise<Doc[]> {
|
|
79
|
+
const key = deps.env.FIRECRAWL_API_KEY;
|
|
80
|
+
if (!key) {
|
|
81
|
+
// 0054 Block 5 row 2 — key required by this plugin's contract, not by Firecrawl.
|
|
82
|
+
throw new Error('FIRECRAWL_API_KEY is unset');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let res: Response;
|
|
86
|
+
try {
|
|
87
|
+
res = await deps.fetch('https://api.firecrawl.dev/v2/search', {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: {
|
|
90
|
+
'Content-Type': 'application/json',
|
|
91
|
+
Authorization: `Bearer ${key}`,
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
query: input.topic,
|
|
95
|
+
limit: input.maxResults,
|
|
96
|
+
sources: ['web'],
|
|
97
|
+
scrapeOptions: { formats: ['markdown'] },
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
} catch (err: unknown) {
|
|
101
|
+
// 0054 Block 5 row 3 — transport error (DNS, refused, TLS, fetch throws).
|
|
102
|
+
throw new Error(`Firecrawl network error: ${err instanceof Error ? err.message : String(err)}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let bodyText: string;
|
|
106
|
+
try {
|
|
107
|
+
bodyText = await res.text();
|
|
108
|
+
} catch {
|
|
109
|
+
throw new Error(`Firecrawl HTTP ${res.status}: empty response body`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!res.ok || res.status !== 200) {
|
|
113
|
+
// 0054 Block 5 rows 4–5 — 401/402/400/404/408/429/5xx. No silent retry in v1.
|
|
114
|
+
throw new Error(`Firecrawl HTTP ${res.status}: ${extractError(bodyText)}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let parsed: FirecrawlSearchResponse;
|
|
118
|
+
try {
|
|
119
|
+
parsed = JSON.parse(bodyText) as FirecrawlSearchResponse;
|
|
120
|
+
} catch {
|
|
121
|
+
// 200 but not JSON → malformed response (row 5).
|
|
122
|
+
throw new Error('Firecrawl malformed response: invalid JSON');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (parsed.success === false) {
|
|
126
|
+
// 0054 Block 5 row 5 — `success:false` in a 200 body.
|
|
127
|
+
throw new Error(`Firecrawl HTTP 200: ${extractError(bodyText)}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (parsed.success !== true) {
|
|
131
|
+
throw new Error('Firecrawl malformed response: success missing');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Row 5 (P4): a 200 `success:true` with `data` absent/null is malformed, not zero hits.
|
|
135
|
+
if (parsed.data === null || parsed.data === undefined) {
|
|
136
|
+
throw new Error('Firecrawl malformed response: data missing');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const web = parsed.data.web ?? [];
|
|
140
|
+
|
|
141
|
+
// Row 6 — true zero hits: write `[]`, exit 0.
|
|
142
|
+
if (web.length === 0) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const docs: Doc[] = [];
|
|
147
|
+
let skipped = 0;
|
|
148
|
+
for (const [rank, item] of web.entries()) {
|
|
149
|
+
const url = typeof item.url === 'string' ? item.url : undefined;
|
|
150
|
+
const markdown = typeof item.markdown === 'string' ? item.markdown : undefined;
|
|
151
|
+
const body = markdown?.trim() ?? '';
|
|
152
|
+
if (!url || body.length === 0) {
|
|
153
|
+
// Row 7 — partial item failure: skip + stderr warn, continue.
|
|
154
|
+
skipped += 1;
|
|
155
|
+
console.warn(`web-search: skipping result without usable markdown (url=${url ?? 'missing'})`);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
docs.push({
|
|
159
|
+
id: computeId(url),
|
|
160
|
+
body,
|
|
161
|
+
title: typeof item.title === 'string' ? item.title : undefined,
|
|
162
|
+
sourceUri: url,
|
|
163
|
+
mediaType: 'text/markdown',
|
|
164
|
+
metadata: {
|
|
165
|
+
query: input.topic,
|
|
166
|
+
rank,
|
|
167
|
+
position: typeof item.position === 'number' ? item.position : undefined,
|
|
168
|
+
scraped_at: deps.now().toISOString(),
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Row 8 — hits > 0 but every item skipped: deliberate departure from C1 (0054 Block 6b).
|
|
174
|
+
if (docs.length === 0 && skipped > 0) {
|
|
175
|
+
throw new Error('Firecrawl returned hits but no usable markdown bodies');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return docs;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Best-effort `error` message from a Firecrawl error body; empty when unavailable. */
|
|
182
|
+
function extractError(bodyText: string): string {
|
|
183
|
+
try {
|
|
184
|
+
const parsed = JSON.parse(bodyText) as { error?: unknown };
|
|
185
|
+
const error = parsed.error;
|
|
186
|
+
if (typeof error === 'string' && error.length > 0) {
|
|
187
|
+
return error;
|
|
188
|
+
}
|
|
189
|
+
if (error !== undefined && error !== null) {
|
|
190
|
+
return JSON.stringify(error);
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
// Non-JSON error body — fall through to empty.
|
|
194
|
+
}
|
|
195
|
+
return '';
|
|
196
|
+
}
|
|
197
|
+
|
|
48
198
|
/**
|
|
49
199
|
* Build the fixture `Doc[]` for a topic (0054 Block 4 mapping). Always emits
|
|
50
200
|
* canned docs — the v1 plugin has no live Firecrawl path.
|
|
@@ -116,13 +266,23 @@ export async function main(): Promise<number> {
|
|
|
116
266
|
}
|
|
117
267
|
|
|
118
268
|
if (!input.fixture) {
|
|
119
|
-
// 0054 Block 1/Block 8: `fixture` absent
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
269
|
+
// 0054 Block 1/Block 8: `fixture` absent/false = LIVE Firecrawl path.
|
|
270
|
+
try {
|
|
271
|
+
const docs = await searchFirecrawl(input, {
|
|
272
|
+
fetch: globalThis.fetch,
|
|
273
|
+
env: process.env,
|
|
274
|
+
now: () => new Date(),
|
|
275
|
+
});
|
|
276
|
+
const outDir = dirname(values.out);
|
|
277
|
+
if (outDir && outDir !== '.') {
|
|
278
|
+
await mkdir(outDir, { recursive: true });
|
|
279
|
+
}
|
|
280
|
+
await writeFile(values.out, JSON.stringify(DocListSchema.parse(docs), null, 2), 'utf-8');
|
|
281
|
+
return 0;
|
|
282
|
+
} catch (err: unknown) {
|
|
283
|
+
console.error(`web-search failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
284
|
+
return 1;
|
|
285
|
+
}
|
|
126
286
|
}
|
|
127
287
|
|
|
128
288
|
try {
|
package/plugins/kk/README.md
CHANGED
|
@@ -15,3 +15,8 @@ It is a Claude Code / Superskill plugin, **not** a kk-core product plugin.
|
|
|
15
15
|
`publish`) live under `plugins/ingestions|generations|publishings/<name>/`.
|
|
16
16
|
|
|
17
17
|
Install: `superskill install kk`.
|
|
18
|
+
|
|
19
|
+
Capability files **omit a leading `kk-`**. `superskill install` prefixes the plugin name, so
|
|
20
|
+
`skills/judge` installs as `kk:judge` (not `kk:kk-judge`). Same for `topic`, `storm-research`,
|
|
21
|
+
`/workflow-run`, and the `judge-*` subagents. Product workflow YAML (`kk-storm-research.yaml`)
|
|
22
|
+
keeps its existing name — that is a workflow stem, not an installable agent capability.
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description: This specialized judge agent should be used when evaluating generated content for regulatory, legal, or safety compliance — prohibited claims, required disclosures, jurisdictional rules, and PII/sensitive-data leakage. Triggers on "compliance check", "legal review of content", "check prohibited claims", "verify disclosures", "regulatory gate", or when a spur workflow needs a compliance evaluation gate before publishing. Delegates all evaluation logic to the
|
|
2
|
+
name: judge-compliance
|
|
3
|
+
description: This specialized judge agent should be used when evaluating generated content for regulatory, legal, or safety compliance — prohibited claims, required disclosures, jurisdictional rules, and PII/sensitive-data leakage. Triggers on "compliance check", "legal review of content", "check prohibited claims", "verify disclosures", "regulatory gate", or when a spur workflow needs a compliance evaluation gate before publishing. Delegates all evaluation logic to the judge fat skill with the compliance rubric pre-selected.
|
|
4
4
|
tools: Read, Grep, Glob
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
#
|
|
7
|
+
# judge-compliance — compliance judge persona
|
|
8
8
|
|
|
9
|
-
Thin wrapper around the **
|
|
9
|
+
Thin wrapper around the **judge** fat skill. Fixes the rubric to `compliance` and forwards
|
|
10
10
|
evaluation. Do not duplicate evaluation logic here — the skill owns it.
|
|
11
11
|
|
|
12
12
|
## Behavior
|
|
13
13
|
|
|
14
14
|
1. Receive the content path and the jurisdiction/policy config from the caller or workflow step.
|
|
15
|
-
2. Invoke the `
|
|
15
|
+
2. Invoke the `judge` skill procedure with `rubric: compliance`.
|
|
16
16
|
3. Return the verdict JSON path; the skill writes the verdict file.
|
|
17
17
|
|
|
18
18
|
## Invocation contract
|
|
@@ -20,10 +20,10 @@ evaluation. Do not duplicate evaluation logic here — the skill owns it.
|
|
|
20
20
|
When invoked by an `agent.run` workflow step:
|
|
21
21
|
|
|
22
22
|
```
|
|
23
|
-
You are the
|
|
24
|
-
Load the
|
|
23
|
+
You are the judge-compliance persona.
|
|
24
|
+
Load the judge skill from plugins/kk/skills/judge/SKILL.md.
|
|
25
25
|
Evaluate the content at {{content_path}} against the compliance rubric
|
|
26
|
-
(plugins/kk/skills/
|
|
26
|
+
(plugins/kk/skills/judge/references/rubrics.md, section "compliance").
|
|
27
27
|
Jurisdiction/policy: {{jurisdiction}} # e.g. US-SEC, EU-GDPR, CN-CAC; or "unspecified"
|
|
28
28
|
Write the verdict JSON to {{verdict_path}} per the contract in SKILL.md.
|
|
29
29
|
Print the verdict path as the last line of stdout.
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description: This specialized judge agent should be used when evaluating generated technical content for factual accuracy, citation integrity, and code correctness. Triggers on "check technical accuracy", "verify tech content", "fact-check technical", "review code samples in content", or when a spur workflow needs a technical-accuracy evaluation gate. Delegates all evaluation logic to the
|
|
2
|
+
name: judge-tech
|
|
3
|
+
description: This specialized judge agent should be used when evaluating generated technical content for factual accuracy, citation integrity, and code correctness. Triggers on "check technical accuracy", "verify tech content", "fact-check technical", "review code samples in content", or when a spur workflow needs a technical-accuracy evaluation gate. Delegates all evaluation logic to the judge fat skill with the tech-accuracy rubric pre-selected.
|
|
4
4
|
tools: Read, Grep, Glob
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
#
|
|
7
|
+
# judge-tech — technical-accuracy judge persona
|
|
8
8
|
|
|
9
|
-
Thin wrapper around the **
|
|
9
|
+
Thin wrapper around the **judge** fat skill. Fixes the rubric to `tech-accuracy` and forwards
|
|
10
10
|
evaluation. Do not duplicate evaluation logic here — the skill owns it.
|
|
11
11
|
|
|
12
12
|
## Behavior
|
|
13
13
|
|
|
14
14
|
1. Receive the content path (and optional verdict output path) from the caller or workflow step.
|
|
15
|
-
2. Invoke the `
|
|
15
|
+
2. Invoke the `judge` skill procedure with `rubric: tech-accuracy`.
|
|
16
16
|
3. Return the verdict JSON path; the skill writes the verdict file.
|
|
17
17
|
|
|
18
18
|
## Invocation contract
|
|
@@ -20,10 +20,10 @@ evaluation. Do not duplicate evaluation logic here — the skill owns it.
|
|
|
20
20
|
When invoked by an `agent.run` workflow step:
|
|
21
21
|
|
|
22
22
|
```
|
|
23
|
-
You are the
|
|
24
|
-
Load the
|
|
23
|
+
You are the judge-tech persona.
|
|
24
|
+
Load the judge skill from plugins/kk/skills/judge/SKILL.md.
|
|
25
25
|
Evaluate the content at {{content_path}} against the tech-accuracy rubric
|
|
26
|
-
(plugins/kk/skills/
|
|
26
|
+
(plugins/kk/skills/judge/references/rubrics.md, section "tech-accuracy").
|
|
27
27
|
Write the verdict JSON to {{verdict_path}} per the contract in SKILL.md.
|
|
28
28
|
Print the verdict path as the last line of stdout.
|
|
29
29
|
```
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description: This specialized judge agent should be used when evaluating generated content for brand voice and tone consistency, banned-phrase compliance, and audience fit. Triggers on "check brand tone", "review voice consistency", "tone audit", "style guide check", or when a spur workflow needs a brand-tone evaluation gate before publishing. Delegates all evaluation logic to the
|
|
2
|
+
name: judge-tone
|
|
3
|
+
description: This specialized judge agent should be used when evaluating generated content for brand voice and tone consistency, banned-phrase compliance, and audience fit. Triggers on "check brand tone", "review voice consistency", "tone audit", "style guide check", or when a spur workflow needs a brand-tone evaluation gate before publishing. Delegates all evaluation logic to the judge fat skill with the brand-tone rubric pre-selected.
|
|
4
4
|
tools: Read, Grep, Glob
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
#
|
|
7
|
+
# judge-tone — brand-tone judge persona
|
|
8
8
|
|
|
9
|
-
Thin wrapper around the **
|
|
9
|
+
Thin wrapper around the **judge** fat skill. Fixes the rubric to `brand-tone` and forwards
|
|
10
10
|
evaluation. Do not duplicate evaluation logic here — the skill owns it.
|
|
11
11
|
|
|
12
12
|
## Behavior
|
|
13
13
|
|
|
14
14
|
1. Receive the content path and the brand config (tone profile + banned-phrase list) from the
|
|
15
15
|
caller or workflow step.
|
|
16
|
-
2. Invoke the `
|
|
16
|
+
2. Invoke the `judge` skill procedure with `rubric: brand-tone`.
|
|
17
17
|
3. Return the verdict JSON path; the skill writes the verdict file.
|
|
18
18
|
|
|
19
19
|
## Invocation contract
|
|
@@ -21,10 +21,10 @@ evaluation. Do not duplicate evaluation logic here — the skill owns it.
|
|
|
21
21
|
When invoked by an `agent.run` workflow step:
|
|
22
22
|
|
|
23
23
|
```
|
|
24
|
-
You are the
|
|
25
|
-
Load the
|
|
24
|
+
You are the judge-tone persona.
|
|
25
|
+
Load the judge skill from plugins/kk/skills/judge/SKILL.md.
|
|
26
26
|
Evaluate the content at {{content_path}} against the brand-tone rubric
|
|
27
|
-
(plugins/kk/skills/
|
|
27
|
+
(plugins/kk/skills/judge/references/rubrics.md, section "brand-tone").
|
|
28
28
|
Brand config (tone profile + banned phrases): {{brand_config}}
|
|
29
29
|
Write the verdict JSON to {{verdict_path}} per the contract in SKILL.md.
|
|
30
30
|
Print the verdict path as the last line of stdout.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: workflow-run
|
|
3
|
+
description: >-
|
|
4
|
+
Run STORM daily research end to end — a topic sentence XOR a markdown file becomes a
|
|
5
|
+
grounded content.md report. Installs the workflow YAML on first run (0056 copy rule),
|
|
6
|
+
creates the config with defaults if missing, computes the topic workspace, and shells
|
|
7
|
+
spur workflow run against the installed workflow.
|
|
8
|
+
argument-hint: "[name] <topic|--in file> [--fixture] [--force]"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
Thin wrapper around the **storm-research** skill. Read
|
|
12
|
+
`plugins/kk/skills/storm-research/SKILL.md` first — it is the craft SSOT (query framing,
|
|
13
|
+
evidence verification, better-report practices, fail-loud rows). This command owns the run
|
|
14
|
+
procedure: config create, YAML install, topic-id computation, and the `spur workflow run`.
|
|
15
|
+
No new `kk` CLI noun (ADR-011) — everything here is shell + the workflow.
|
|
16
|
+
|
|
17
|
+
**Args:** `$ARGUMENTS`
|
|
18
|
+
|
|
19
|
+
## 1. Parse `$ARGUMENTS`
|
|
20
|
+
|
|
21
|
+
Parse per the frozen rule:
|
|
22
|
+
|
|
23
|
+
- First token is the workflow **name** when it matches `^[a-z0-9][a-z0-9-]*$` and is not `--in`;
|
|
24
|
+
otherwise `name=kk-storm-research` and that token is the topic.
|
|
25
|
+
- `--in <file>` → file mode. `--fixture` → force `fixture: "true"`. `--force` → overwrite an
|
|
26
|
+
existing dest YAML.
|
|
27
|
+
- **XOR:** exactly one of `<topic>` / `--in <file>`. Both set or both empty → exit 1, stderr
|
|
28
|
+
states the XOR rule, no writes.
|
|
29
|
+
|
|
30
|
+
Resolve these before step 2: `NAME`, `TOPIC`, `INPUT_FILE` (empty in sentence mode), `FIXTURE`
|
|
31
|
+
(`true`|`false`), `FORCE`.
|
|
32
|
+
|
|
33
|
+
## 2. Resolve the config
|
|
34
|
+
|
|
35
|
+
`config_path="${KK_CONFIG:-$HOME/.config/kk/config.yaml}"`.
|
|
36
|
+
|
|
37
|
+
- **Missing file → create-on-first-run** with compiled defaults (never overwrite an existing
|
|
38
|
+
file — no merge, no rewrite):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
install -d "$(dirname "$config_path")"
|
|
42
|
+
cat > "$config_path" <<'EOF'
|
|
43
|
+
works_dir: "$HOME/.config/kk/works"
|
|
44
|
+
workflows_dir: "$HOME/.config/kk/workflows"
|
|
45
|
+
defaults:
|
|
46
|
+
maxResults: 8
|
|
47
|
+
fixture: false
|
|
48
|
+
EOF
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- **Unreadable / unparseable / invalid types** (e.g. `maxResults: "eight"`) → exit 1, stderr
|
|
52
|
+
names the key and expected vs actual type; touch nothing.
|
|
53
|
+
- Read the four values with precedence **command flag > env > file > compiled default**:
|
|
54
|
+
|
|
55
|
+
| Value | Env override | Compiled default |
|
|
56
|
+
| --- | --- | --- |
|
|
57
|
+
| `works_dir` | `KK_WORKS_DIR` | `$HOME/.config/kk/works` |
|
|
58
|
+
| `workflows_dir` | `KK_WORKFLOWS_DIR` | `$HOME/.config/kk/workflows` |
|
|
59
|
+
| `defaults.maxResults` | — | `8` |
|
|
60
|
+
| `defaults.fixture` | — | `false` |
|
|
61
|
+
|
|
62
|
+
`--fixture` sets `fixture=true` regardless of the file. `FIRECRAWL_API_KEY` is **never** read
|
|
63
|
+
from config — env-only.
|
|
64
|
+
|
|
65
|
+
## 3. Install the workflow YAML
|
|
66
|
+
|
|
67
|
+
`dest="$workflows_dir/$NAME.yaml"`. Copy from the **first hit** of, in order:
|
|
68
|
+
|
|
69
|
+
1. `$KK_WORKFLOWS_SOURCE/$NAME.yaml`
|
|
70
|
+
2. `plugins/kk/workflows/$NAME.yaml`
|
|
71
|
+
3. `.spur/workflows/$NAME.yaml`
|
|
72
|
+
|
|
73
|
+
- `dest` missing → copy from the first hit. Source missing from all three → exit 1, stderr
|
|
74
|
+
lists the roots searched.
|
|
75
|
+
- `dest` exists + `--force` → overwrite from source.
|
|
76
|
+
- `dest` exists + identical bytes → no-op.
|
|
77
|
+
- `dest` exists + different bytes → **warn and leave** (run proceeds with the user's copy).
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
src=""
|
|
81
|
+
for d in "$KK_WORKFLOWS_SOURCE" "plugins/kk/workflows" ".spur/workflows"; do
|
|
82
|
+
[ -n "$d" ] && [ -f "$d/$NAME.yaml" ] && { src="$d/$NAME.yaml"; break; }
|
|
83
|
+
done
|
|
84
|
+
[ -z "$src" ] && { echo "no source workflow for $NAME (searched KK_WORKFLOWS_SOURCE, plugins/kk/workflows, .spur/workflows)" >&2; exit 1; }
|
|
85
|
+
if [ ! -f "$dest" ] || [ "$FORCE" = true ]; then
|
|
86
|
+
install -d "$(dirname "$dest")" && cp "$src" "$dest"
|
|
87
|
+
elif ! cmp -s "$src" "$dest"; then
|
|
88
|
+
echo "warning: $dest differs from install source; leaving user copy (pass --force to replace)" >&2
|
|
89
|
+
fi
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## 4. Compute the topic workspace (0056 `topicId`)
|
|
93
|
+
|
|
94
|
+
`raw` = `TOPIC` (sentence mode) or the first ATX H1 (`/^#\s+(.+)$/m`) else basename without
|
|
95
|
+
extension (`--in` mode). Then:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
normalized=$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
|
|
99
|
+
slug=$(printf '%s' "$normalized" | tr -cs 'a-z0-9' '-' | sed 's/^-*//; s/-*$//' | cut -c1-48 | sed 's/-*$//')
|
|
100
|
+
[ -z "$slug" ] && slug="topic"
|
|
101
|
+
hash=$(printf '%s' "$normalized" | shasum -a 256 | cut -c1-8)
|
|
102
|
+
topic_id="${slug}-${hash}"
|
|
103
|
+
work_dir="$works_dir/$topic_id"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- `raw` derives empty (no H1 and empty basename) → exit 1 with the derived value.
|
|
107
|
+
- Same `raw` → same workspace; **last write wins**; never fail on an existing works dir.
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
mkdir -p "$work_dir"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## 5. Run the workflow
|
|
114
|
+
|
|
115
|
+
All values are strings. Bind the vars first: `maxResults` and `fixture` from step 2;
|
|
116
|
+
`plugins_path` defaults to `./plugins` in a checkout, else the installed package `plugins/`
|
|
117
|
+
path or `KK_PLUGIN_PATH`; `render_script` stays `plugins/kk/scripts/render-md.ts`.
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
spur workflow run "$dest" --vars \
|
|
121
|
+
"{\"topic\":\"$TOPIC\",\"input_file\":\"$INPUT_FILE\",\"maxResults\":\"$maxResults\",\"fixture\":\"$FIXTURE\",\"work_dir\":\"$work_dir\",\"plugins_path\":\"$plugins_path\",\"render_script\":\"$render_script\"}"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## 6. Report
|
|
125
|
+
|
|
126
|
+
Print the resolved `work_dir`, then the two artifacts to hand the operator:
|
|
127
|
+
`$work_dir/content.md` (report sidecar) and `$work_dir/content.json` (machine contract).
|
|
128
|
+
Per the skill's better-report practice (d), skim `content.md` citations against `docs.json`
|
|
129
|
+
ids before presenting. If the run ended `failed`, show the failing state
|
|
130
|
+
(`spur workflow trace <run-id> --json`) and the skill's fail-loud recovery row.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shipped no-checkout runner for the Content→markdown research-report helper.
|
|
3
|
+
* 0059's `render` state shells: `bun <this script> --in <content.json> --out <content.md>`.
|
|
4
|
+
*
|
|
5
|
+
* Self-contained by design: end users install the published `kk` package and cannot
|
|
6
|
+
* resolve `@gobing-ai/kk-core` (private). The render logic below is a COPY of
|
|
7
|
+
* `packages/kk-core/src/render-md.ts`; keep them byte-identical for the same Content.
|
|
8
|
+
*
|
|
9
|
+
* SYNC: packages/kk-core/src/render-md.ts (renderContentMarkdown + helpers)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
13
|
+
import { parseArgs } from 'node:util';
|
|
14
|
+
|
|
15
|
+
interface Ref {
|
|
16
|
+
url?: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
cite?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ContentInput {
|
|
22
|
+
body: string;
|
|
23
|
+
title?: string;
|
|
24
|
+
outline?: string;
|
|
25
|
+
references?: Ref[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Mirror of `ContentSchema` structural requirements for the fields this render touches. */
|
|
29
|
+
function parseContent(raw: string): ContentInput {
|
|
30
|
+
const value: unknown = JSON.parse(raw);
|
|
31
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
32
|
+
throw new Error('content must be an object');
|
|
33
|
+
}
|
|
34
|
+
const c = value as Record<string, unknown>;
|
|
35
|
+
if (typeof c.body !== 'string') {
|
|
36
|
+
throw new Error('body is required and must be a string');
|
|
37
|
+
}
|
|
38
|
+
const out: ContentInput = { body: c.body };
|
|
39
|
+
if (c.title !== undefined) {
|
|
40
|
+
if (typeof c.title !== 'string') {
|
|
41
|
+
throw new Error('title must be a string');
|
|
42
|
+
}
|
|
43
|
+
out.title = c.title;
|
|
44
|
+
}
|
|
45
|
+
if (c.outline !== undefined) {
|
|
46
|
+
if (typeof c.outline !== 'string') {
|
|
47
|
+
throw new Error('outline must be a string');
|
|
48
|
+
}
|
|
49
|
+
out.outline = c.outline;
|
|
50
|
+
}
|
|
51
|
+
if (c.references !== undefined) {
|
|
52
|
+
if (!Array.isArray(c.references)) {
|
|
53
|
+
throw new Error('references must be an array');
|
|
54
|
+
}
|
|
55
|
+
out.references = c.references.map((r, i) => {
|
|
56
|
+
if (r === null || typeof r !== 'object' || Array.isArray(r)) {
|
|
57
|
+
throw new Error(`references[${i}] must be an object`);
|
|
58
|
+
}
|
|
59
|
+
const rr = r as Record<string, unknown>;
|
|
60
|
+
for (const key of ['url', 'title', 'cite'] as const) {
|
|
61
|
+
if (rr[key] !== undefined && typeof rr[key] !== 'string') {
|
|
62
|
+
throw new Error(`references[${i}].${key} must be a string`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { url: rr.url, title: rr.title, cite: rr.cite } as Ref;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function renderContentMarkdown(content: ContentInput): string {
|
|
72
|
+
const heading = content.title?.trim() || 'Untitled';
|
|
73
|
+
const startsWithHeading = content.body.startsWith(`# ${heading}`);
|
|
74
|
+
const lead = startsWithHeading ? '' : `# ${heading}\n\n`;
|
|
75
|
+
const outlineBlock =
|
|
76
|
+
content.outline && content.outline.trim().length > 0 ? `\n\n## Outline\n\n${content.outline.trim()}` : '';
|
|
77
|
+
const refsBlock = renderReferences(content.references);
|
|
78
|
+
return `${lead}${content.body}${outlineBlock}${refsBlock}\n`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function renderReferences(references: ContentInput['references']): string {
|
|
82
|
+
if (!references || references.length === 0) {
|
|
83
|
+
return '';
|
|
84
|
+
}
|
|
85
|
+
const lines: string[] = [];
|
|
86
|
+
for (const ref of references) {
|
|
87
|
+
const label = renderReferenceLabel(ref);
|
|
88
|
+
if (label === '') {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
lines.push(`- ${label}`);
|
|
92
|
+
}
|
|
93
|
+
if (lines.length === 0) {
|
|
94
|
+
return '';
|
|
95
|
+
}
|
|
96
|
+
return `\n\n## References\n\n${lines.join('\n')}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function renderReferenceLabel(ref: Ref): string {
|
|
100
|
+
const { url, title, cite } = ref;
|
|
101
|
+
const titleT = title?.trim() ?? '';
|
|
102
|
+
const urlT = url?.trim() ?? '';
|
|
103
|
+
const citeT = cite?.trim() ?? '';
|
|
104
|
+
let label: string;
|
|
105
|
+
if (titleT !== '' && urlT !== '') {
|
|
106
|
+
label = `[${titleT}](${urlT})`;
|
|
107
|
+
} else if (titleT !== '') {
|
|
108
|
+
label = titleT;
|
|
109
|
+
} else if (urlT !== '') {
|
|
110
|
+
label = urlT;
|
|
111
|
+
} else if (citeT !== '') {
|
|
112
|
+
label = `\`${citeT}\``;
|
|
113
|
+
} else {
|
|
114
|
+
return '';
|
|
115
|
+
}
|
|
116
|
+
if (citeT !== '' && label !== `\`${citeT}\``) {
|
|
117
|
+
label = `${label} (${citeT})`;
|
|
118
|
+
}
|
|
119
|
+
return label;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function main(): Promise<number> {
|
|
123
|
+
const { values } = parseArgs({
|
|
124
|
+
options: {
|
|
125
|
+
in: { type: 'string' },
|
|
126
|
+
out: { type: 'string' },
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
if (!values.in || !values.out) {
|
|
130
|
+
console.error('Error: Missing required arguments --in and --out');
|
|
131
|
+
return 1;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const content = parseContent(await readFile(values.in, 'utf-8'));
|
|
135
|
+
await writeFile(values.out, renderContentMarkdown(content), 'utf-8');
|
|
136
|
+
return 0;
|
|
137
|
+
} catch (err: unknown) {
|
|
138
|
+
console.error(`render-md failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
139
|
+
return 1;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (import.meta.main) {
|
|
144
|
+
process.exitCode = await main();
|
|
145
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description: This skill should be used when an LLM evaluation gate is needed in a knowledge-kit pipeline — "judge this content", "evaluate against rubric", "quality gate before publish", "check technical accuracy", "review brand tone", "compliance check", or when a spur workflow step of type `agent.run` targets the
|
|
2
|
+
name: judge
|
|
3
|
+
description: This skill should be used when an LLM evaluation gate is needed in a knowledge-kit pipeline — "judge this content", "evaluate against rubric", "quality gate before publish", "check technical accuracy", "review brand tone", "compliance check", or when a spur workflow step of type `agent.run` targets the judge skill to emit a PASS/FAIL/NEEDS_REVISION verdict. Centralizes evaluation rubrics, prompt templates, and structured verdict emission for generated Content.
|
|
4
4
|
version: 0.1.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
#
|
|
7
|
+
# judge — LLM-as-judge evaluation skill
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
10
10
|
|
|
@@ -107,7 +107,7 @@ Content to evaluate: {{content_path}}
|
|
|
107
107
|
Rubric: {{rubric}} # general | tech-accuracy | brand-tone | compliance
|
|
108
108
|
Output verdict to: {{verdict_path}}
|
|
109
109
|
|
|
110
|
-
Load the rubric definition from plugins/kk/skills/
|
|
110
|
+
Load the rubric definition from plugins/kk/skills/judge/references/rubrics.md (section: {{rubric}}).
|
|
111
111
|
Evaluate the content against every criterion in that rubric.
|
|
112
112
|
For each criterion, quote the specific passage and assign pass | fail | warn.
|
|
113
113
|
Decide the verdict: any must-pass fail → FAIL; else score < threshold → NEEDS_REVISION; else PASS.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Rubrics —
|
|
1
|
+
# Rubrics — judge evaluation criteria
|
|
2
2
|
|
|
3
|
-
Loaded by the
|
|
3
|
+
Loaded by the judge skill at evaluation time. Each rubric defines criteria, weights,
|
|
4
4
|
must-pass flags, and the score thresholds that map to verdicts.
|
|
5
5
|
|
|
6
6
|
## Scoring model
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Workflow integration —
|
|
1
|
+
# Workflow integration — judge in a spur workflow
|
|
2
2
|
|
|
3
3
|
Spur's workflow engine executes one node at a time (`type: parallel` is an inert schema field —
|
|
4
4
|
see `docs/03_ARCHITECTURE.md`). The judge step is therefore a single `agent.run` node followed
|
|
@@ -13,10 +13,10 @@ by a `shell` node that guards the transition to `publish`.
|
|
|
13
13
|
Evaluate the generated content against the {{rubric}} rubric.
|
|
14
14
|
Content: {{content_path}}
|
|
15
15
|
Write the verdict to {{verdict_path}}.
|
|
16
|
-
skill:
|
|
16
|
+
skill: judge
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
- `agent.run` spawns an agent that loads the `
|
|
19
|
+
- `agent.run` spawns an agent that loads the `judge` skill, performs the evaluation, and writes
|
|
20
20
|
the verdict JSON to `{{verdict_path}}` (typically `.spur/run/verdict-judge.json`).
|
|
21
21
|
- The verdict file is the only durable artifact. The agent's prose output is not the contract.
|
|
22
22
|
|
|
@@ -28,22 +28,22 @@ The guard runs as a `shell` step after `judge`. It reads the verdict and gates t
|
|
|
28
28
|
#!/usr/bin/env sh
|
|
29
29
|
verdict_path="{{verdict_path}}"
|
|
30
30
|
if [ ! -f "$verdict_path" ]; then
|
|
31
|
-
echo "
|
|
31
|
+
echo "judge: verdict file not found at $verdict_path"
|
|
32
32
|
exit 1
|
|
33
33
|
fi
|
|
34
34
|
verdict=$(jq -r '.verdict' "$verdict_path")
|
|
35
35
|
case "$verdict" in
|
|
36
36
|
PASS)
|
|
37
|
-
echo "
|
|
37
|
+
echo "judge: PASS"
|
|
38
38
|
exit 0
|
|
39
39
|
;;
|
|
40
40
|
FAIL|NEEDS_REVISION)
|
|
41
|
-
echo "
|
|
41
|
+
echo "judge: $verdict — see $verdict_path for findings"
|
|
42
42
|
jq -r '.findings[]? | select(.status=="fail") | " FAIL: \(.criterion): \(.detail)"' "$verdict_path"
|
|
43
43
|
exit 1
|
|
44
44
|
;;
|
|
45
45
|
*)
|
|
46
|
-
echo "
|
|
46
|
+
echo "judge: unrecognized verdict '$verdict' in $verdict_path"
|
|
47
47
|
exit 1
|
|
48
48
|
;;
|
|
49
49
|
esac
|
|
@@ -60,7 +60,7 @@ esac
|
|
|
60
60
|
|
|
61
61
|
For a bounded regenerate-retry loop, wire the workflow so that a `NEEDS_REVISION` verdict routes
|
|
62
62
|
back to the `generate` step with the verdict's `feedback` as additional input. Cap retries (the
|
|
63
|
-
example workflow uses 2). This is workflow-level orchestration — `
|
|
63
|
+
example workflow uses 2). This is workflow-level orchestration — `judge` itself is stateless
|
|
64
64
|
and re-runnable.
|
|
65
65
|
|
|
66
66
|
```
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: storm-research
|
|
3
|
+
description: >-
|
|
4
|
+
Use the storm-research skill to run daily STORM research — turn a research topic
|
|
5
|
+
sentence or a markdown source file into a grounded STORM Content report with a content.md
|
|
6
|
+
sidecar ("research X", "produce a research report on X", "run today's STORM research",
|
|
7
|
+
"turn this brief into a researched report"). The machine is the 0059 workflow
|
|
8
|
+
(prepare → ingest → generate → render) over the 0056 works layout. Research is separate from
|
|
9
|
+
authoring: topic owns init→outline→draft and this skill never merges into topic.
|
|
10
|
+
version: 0.1.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# storm-research — STORM daily research (topic sentence XOR markdown file → content.md)
|
|
14
|
+
|
|
15
|
+
## Purpose
|
|
16
|
+
|
|
17
|
+
Turn one research topic (a sentence) or one markdown source file into a **grounded STORM
|
|
18
|
+
`Content` report** plus a `content.md` sidecar by composing the `web-search` ingestion plugin,
|
|
19
|
+
the `content-gen` generator, and the `render-md` helper. The machine is the 0059 workflow
|
|
20
|
+
(`prepare → ingest → generate → render → done|failed`); this skill owns the **craft** — framing
|
|
21
|
+
the query, deciding when to re-run ingest, and verifying the report against the evidence.
|
|
22
|
+
|
|
23
|
+
Research is **not** authoring. `topic` owns `init → outline → draft` from an approved
|
|
24
|
+
outline; this skill owns evidence gathering. **Do not merge this into `topic`.**
|
|
25
|
+
|
|
26
|
+
## When to use / not to use
|
|
27
|
+
|
|
28
|
+
Use when:
|
|
29
|
+
|
|
30
|
+
- A user asks to research a topic: "research X", "produce a research report on X", "run today's
|
|
31
|
+
STORM research", "what does the evidence say about X".
|
|
32
|
+
- A user supplies a markdown source (brief, notes, outline) that should ground the report —
|
|
33
|
+
pass it with `--in <file>`.
|
|
34
|
+
|
|
35
|
+
Do **not** use for:
|
|
36
|
+
|
|
37
|
+
- Authoring an article from a topic with no research need — that is `topic`.
|
|
38
|
+
- Quality gating / judging generated content — that is `judge`.
|
|
39
|
+
- Publishing, platform adaptation, or illustration — out of C4 scope.
|
|
40
|
+
- Anything requiring a raw `kk executor run` chain when the workflow already composes it.
|
|
41
|
+
|
|
42
|
+
## Invocation contract
|
|
43
|
+
|
|
44
|
+
Same arguments as `/workflow-run` (the thin command delegates here for craft):
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
[workflow-name] <topic | --in <file>> [--fixture] [--force]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
| Token | Meaning | Default |
|
|
51
|
+
| --- | --- | --- |
|
|
52
|
+
| `workflow-name` | installed workflow YAML name; must match `^[a-z0-9][a-z0-9-]*$` and not be `--in` | `kk-storm-research` |
|
|
53
|
+
| `<topic>` | a **specific research question** (better-report a — prefer a question over a noun phrase) | required unless `--in` |
|
|
54
|
+
| `--in <file>` | markdown file mode (XOR with `<topic>`) | — |
|
|
55
|
+
| `--fixture` | force `fixture: "true"` (offline stub ingest) | config default (`false` for real runs) |
|
|
56
|
+
| `--force` | overwrite an existing dest workflow YAML | off |
|
|
57
|
+
|
|
58
|
+
**XOR rule:** exactly one of `<topic>` / `--in <file>` must be set. Both set or both empty →
|
|
59
|
+
fail loud, no writes. `--in` and a bare topic token are mutually exclusive.
|
|
60
|
+
|
|
61
|
+
The operator invocation in practice is `/workflow-run <topic>` or `/workflow-run --in <file>`; the
|
|
62
|
+
skill is also callable directly as `Skill(skill="storm-research", args="<topic|--in file> ...")`.
|
|
63
|
+
|
|
64
|
+
## How to run
|
|
65
|
+
|
|
66
|
+
1. **Frame the query** (better-report a): prefer a specific question over a noun phrase.
|
|
67
|
+
"What are the known failure modes of vector databases at 1B rows?" beats "vector databases".
|
|
68
|
+
A question constrains the search and the report; a noun phrase invites scope creep.
|
|
69
|
+
2. **Choose input**: topic sentence, or a markdown file via `--in` (XOR). File mode derives the
|
|
70
|
+
query from the first ATX H1 (`/^#\s+(.+)$/m`), else the basename without extension.
|
|
71
|
+
3. **`work_dir` is the resolved topic directory** `$works_dir/<topic-id>`, computed by the
|
|
72
|
+
invoker (the command or the operator) via the 0056 `topicId()` algorithm — never by the
|
|
73
|
+
workflow. `prepare` only `mkdir` + materializes files into `${vars.work_dir}`.
|
|
74
|
+
4. **Pick fixture vs live** (see below). Default is live (`fixture: "false"`); pass
|
|
75
|
+
`fixture: "true"` for offline/dry-run/CI.
|
|
76
|
+
5. **Run** the workflow and collect the outputs: `content.md` (report sidecar), `content.json`
|
|
77
|
+
(machine contract), `docs.json` (evidence). Read the report against the evidence before
|
|
78
|
+
presenting it (better-report d).
|
|
79
|
+
|
|
80
|
+
## topicId algorithm (0056, frozen)
|
|
81
|
+
|
|
82
|
+
`topicId(raw: string)` — computed by the **invoker**, never by the workflow YAML:
|
|
83
|
+
|
|
84
|
+
1. `normalized` = trim, collapse whitespace, lowercase.
|
|
85
|
+
2. `slug` = replace `[^a-z0-9]+` with `-`, squeeze dashes, trim dashes, slice to **48 chars**,
|
|
86
|
+
trim dashes again. If empty → `topic`.
|
|
87
|
+
3. `hash` = `sha256(normalized).digest('hex').slice(0, 8)`.
|
|
88
|
+
4. Return `slug + '-' + hash`.
|
|
89
|
+
|
|
90
|
+
- Sentence mode: `raw` = the topic string. File mode: `raw` = first ATX H1 else basename
|
|
91
|
+
without extension.
|
|
92
|
+
- Same `raw` → same id → same workspace. **Last write wins**; do not fail on an existing
|
|
93
|
+
works dir (re-running a topic is legal — no `--force` needed).
|
|
94
|
+
- If `raw` derives empty (no H1 and an empty basename, e.g. a file named `.md`) → fail loud.
|
|
95
|
+
|
|
96
|
+
## Works layout (0056, frozen)
|
|
97
|
+
|
|
98
|
+
Per-topic workspace at `$works_dir/<topic-id>/`:
|
|
99
|
+
|
|
100
|
+
| File | Written by | Contents |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| `topic.json` | prepare | ingest `--in` (`topic`, `maxResults`, `fixture`) |
|
|
103
|
+
| `input.md` | prepare (file mode only) | verbatim copy of the operator file |
|
|
104
|
+
| `local-doc.json` | prepare (file mode only) | one-element `Doc[]` (id = `sha256('file://'+absPath).hex.slice(0,16)`, rank `-1`, position `0`) |
|
|
105
|
+
| `docs.json` | ingest | `web-search` output `Doc[]` |
|
|
106
|
+
| `content.json` | generate | STORM `Content` (machine contract) |
|
|
107
|
+
| `content.md` | render | markdown sidecar report |
|
|
108
|
+
|
|
109
|
+
Contrast: `topic`'s authoring workspace is `./<kebab-case(topic)>` with `brief.md` /
|
|
110
|
+
`2-outline/` / `3-draft/` and explicitly excludes research. C4 works live under `$works_dir`,
|
|
111
|
+
outside any repo.
|
|
112
|
+
|
|
113
|
+
## Live vs fixture
|
|
114
|
+
|
|
115
|
+
| Mode | `fixture` | Behavior | Requires |
|
|
116
|
+
| --- | --- | --- | --- |
|
|
117
|
+
| Live | `"false"` (default) | `web-search` calls the Firecrawl search API (`POST https://api.firecrawl.dev/v2/search`, `scrapeOptions.formats: ["markdown"]`), maps `data.web[]` to `Doc[]` | `FIRECRAWL_API_KEY` env var; unset → the ingest exits 1 ("FIRECRAWL_API_KEY is unset") |
|
|
118
|
+
| Fixture | `"true"` | `web-search` emits canned `Doc[]` (stable `sha256(sourceUri)` ids); no network, no key | none — offline/dry-run/CI |
|
|
119
|
+
|
|
120
|
+
- `FIRECRAWL_API_KEY` is **env-only** — never a config key, never a workflow var, never in the
|
|
121
|
+
report.
|
|
122
|
+
- Real daily research defaults to live. **`fixture: true` is not research** (better-report b) —
|
|
123
|
+
the canned docs are for machine checks, not for evidence a reader should trust.
|
|
124
|
+
|
|
125
|
+
## Fail-loud table (0056 rows the agent must execute)
|
|
126
|
+
|
|
127
|
+
| Condition | Behavior | Recovery |
|
|
128
|
+
| --- | --- | --- |
|
|
129
|
+
| Both `<topic>` and `--in` set, or neither | exit 1, stderr states the XOR rule; no writes | pass exactly one of `topic` / `--in` |
|
|
130
|
+
| `--in` file unreadable (missing / I/O) | exit 1, stderr names the path | fix the path / permissions |
|
|
131
|
+
| Derived query / topic-id empty (no H1 and empty basename) | exit 1, stderr shows the derived value | add an H1 or give the file a real name |
|
|
132
|
+
| Dest `$workflows_dir/<name>.yaml` exists with different bytes | warning, **no overwrite**; run proceeds with the user's copy | re-run with `--force` to replace from the install source |
|
|
133
|
+
| Workflow YAML cannot be resolved from any install source | exit 1, stderr lists the roots searched | reinstall the package / set `KK_WORKFLOWS_SOURCE` |
|
|
134
|
+
| `KK_CONFIG` set but unreadable, or config unparseable / invalid types | exit 1, stderr names the key + expected vs actual type | fix the file, or delete it (recreated with defaults next run) |
|
|
135
|
+
| Missing config at `KK_CONFIG` | **not a failure** — create-on-first-run with compiled defaults; an existing file is never overwritten | — |
|
|
136
|
+
|
|
137
|
+
Every failure names the exact path and the exact next command. Never silently skip, never
|
|
138
|
+
auto-replace, never hash topic-id in the YAML.
|
|
139
|
+
|
|
140
|
+
## Better-report practices (R1)
|
|
141
|
+
|
|
142
|
+
1. **(a) Prefer a specific question over a noun phrase.** A question ("How does X behave under
|
|
143
|
+
Y?") pins the search and the report's thesis; a noun ("X") leaves scope open.
|
|
144
|
+
2. **(b) Do not treat `fixture: true` as research.** Fixture output is canned — fine for
|
|
145
|
+
pipeline checks, never a source of claims to present to a reader.
|
|
146
|
+
3. **(c) Re-run ingest only when the query or sources must change.** If the question is
|
|
147
|
+
unchanged and the sources are the same, **reuse `docs.json`** — do not re-fetch. Only a new
|
|
148
|
+
query, new sources, or stale evidence justifies a fresh ingest.
|
|
149
|
+
4. **(d) Read `content.md` citations against `docs.json` ids.** Every citation in the report
|
|
150
|
+
must resolve to a Doc id present in `docs.json`; verify the claim against that Doc's body
|
|
151
|
+
before trusting it. A citation to a missing id is a defect — fix the report, not the id list.
|
|
152
|
+
5. **Cap `maxResults` before widening.** Start at the default (`8`); only widen when the first
|
|
153
|
+
pass demonstrably misses coverage. More results is not better grounding — it is more noise.
|
|
154
|
+
6. **Prefer primary sources in the markdown input file.** When a source file is supplied via
|
|
155
|
+
`--in`, favor the primary material in it (first-party specs, owned notes, authoritative
|
|
156
|
+
extracts) as the spine of the report; web results fill gaps, not the reverse.
|
|
157
|
+
|
|
158
|
+
## Invariants
|
|
159
|
+
|
|
160
|
+
- The invoker computes `topicId` and passes `work_dir` resolved; the workflow never hashes.
|
|
161
|
+
- XOR input always — exactly one of `topic` / `input_file`.
|
|
162
|
+
- Never promise a judge gate or query expansion in this skill (C4 scope: neither exists).
|
|
163
|
+
- No new `kk` first-layer noun (ADR-011) — this skill runs through the workflow, not through a
|
|
164
|
+
`kk` CLI verb.
|
|
165
|
+
- `FIRECRAWL_API_KEY` stays env-only.
|
|
166
|
+
- Works layout file names are frozen (0056) — do not rename or add sidecars in this skill.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: topic
|
|
3
3
|
description: >-
|
|
4
|
-
Use the
|
|
4
|
+
Use the topic skill to author technical content from a topic or Markdown
|
|
5
5
|
brief — "create an article from this topic", "turn this brief into a draft",
|
|
6
6
|
"generate outline options", "initialize a topic workspace", "resume my draft".
|
|
7
7
|
Runs the topic-core authoring loop (init → outline → draft) through four
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
cohesive topic-authoring core, harvested from the WT 7-stage wrappers.
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
-
#
|
|
12
|
+
# topic — topic-core authoring (create | init | outline | draft)
|
|
13
13
|
|
|
14
14
|
## Purpose
|
|
15
15
|
|
|
@@ -159,7 +159,7 @@ silently skip, never auto-replace, never delete.
|
|
|
159
159
|
## Prompt template (Skill() / agent.run)
|
|
160
160
|
|
|
161
161
|
```
|
|
162
|
-
Skill(skill="
|
|
162
|
+
Skill(skill="topic",
|
|
163
163
|
args="<operation> <topic-or-brief> [--dir <target>] [--outline <a|b|c>] [--revise <feedback>] [--force]")
|
|
164
164
|
```
|
|
165
165
|
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
---
|
|
2
|
+
# STORM topic-research daily workflow (C4):
|
|
3
|
+
# prepare -> ingest -> generate -> render -> done|failed.
|
|
4
|
+
#
|
|
5
|
+
# State-machine extension of the C3 dry-run prototype. Adds the 0056 XOR
|
|
6
|
+
# prepare + works layout, the generate local-doc merge, and the 0058 render
|
|
7
|
+
# sidecar state. No publish, no judge — by design (C4 scope).
|
|
8
|
+
#
|
|
9
|
+
# 0056 contract (docs/tasks/0056 ... Solution blocks 1-8): `work_dir` is the
|
|
10
|
+
# RESOLVED topic dir (`$works_root/<topic-id>`), computed by the invoker
|
|
11
|
+
# (0060/operator) and passed as `--vars`. prepare only `mkdir` + materializes
|
|
12
|
+
# files into `${vars.work_dir}`; it never hashes the topic-id in YAML. XOR:
|
|
13
|
+
# exactly one of `topic`/`input_file` must be non-empty.
|
|
14
|
+
#
|
|
15
|
+
# 0058 contract (docs/tasks/0058 ... Solution): render shells
|
|
16
|
+
# bun plugins/kk/scripts/render-md.ts --in <work_dir>/content.json \
|
|
17
|
+
# --out <work_dir>/content.md
|
|
18
|
+
#
|
|
19
|
+
# Success edges use `guard: { kind: always }` (repo convention — see the C3
|
|
20
|
+
# file and task-pipeline.yaml): a real-run action failure short-circuits at
|
|
21
|
+
# the state under the default onError=fail policy; `always` lets `--dry-run`
|
|
22
|
+
# walk to `done` (actions don't execute in dry-run, so `action-ok` would
|
|
23
|
+
# always deny and fall to `failed`).
|
|
24
|
+
"$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
|
|
25
|
+
name: kk-storm-research
|
|
26
|
+
kind: state-machine
|
|
27
|
+
description: >-
|
|
28
|
+
STORM daily topic research — XOR prepare into 0056 works layout, chain
|
|
29
|
+
ingest + generate, render content.md sidecar
|
|
30
|
+
initialState: prepare
|
|
31
|
+
terminalStates:
|
|
32
|
+
- done
|
|
33
|
+
- failed
|
|
34
|
+
vars:
|
|
35
|
+
topic: "" # sentence-mode query; ignored when input_file set (XOR)
|
|
36
|
+
input_file: "" # file-mode markdown path when non-empty (XOR)
|
|
37
|
+
maxResults: "8" # written into topic.json as int (0054 maxResults)
|
|
38
|
+
fixture: "false" # "true" selects the stub; CI/dry-run must pass "true"
|
|
39
|
+
work_dir: "./work" # RESOLVED topic dir ($works_root/<topic-id>)
|
|
40
|
+
plugins_path: "./plugins" # --plugins-path for kk executor run
|
|
41
|
+
ingestion_plugin: "web-search"
|
|
42
|
+
generator_plugin: "content-gen"
|
|
43
|
+
render_script: "plugins/kk/scripts/render-md.ts" # 0058 shipped runner
|
|
44
|
+
|
|
45
|
+
states:
|
|
46
|
+
- id: prepare
|
|
47
|
+
description: >-
|
|
48
|
+
mkdir work_dir; XOR guard; sentence mode writes topic.json; file mode
|
|
49
|
+
copies input.md, writes local-doc.json and topic.json with a derived
|
|
50
|
+
query
|
|
51
|
+
onEnter:
|
|
52
|
+
- kind: shell
|
|
53
|
+
options:
|
|
54
|
+
command: >-
|
|
55
|
+
set -e;
|
|
56
|
+
mkdir -p "${vars.work_dir}";
|
|
57
|
+
if [ -n "${vars.topic}" ] && [ -n "${vars.input_file}" ]; then
|
|
58
|
+
echo "XOR violation: both topic and input_file set" >&2; exit 1;
|
|
59
|
+
fi;
|
|
60
|
+
if [ -z "${vars.topic}" ] && [ -z "${vars.input_file}" ]; then
|
|
61
|
+
echo "XOR violation: neither topic nor input_file set" >&2;
|
|
62
|
+
exit 1;
|
|
63
|
+
fi;
|
|
64
|
+
if [ -n "${vars.topic}" ]; then
|
|
65
|
+
jq -n --arg topic "${vars.topic}" --arg maxResults
|
|
66
|
+
"${vars.maxResults}" --arg fixture "${vars.fixture}"
|
|
67
|
+
'{topic: $topic, maxResults: ($maxResults | tonumber),
|
|
68
|
+
fixture: ($fixture == "true")}' > "${vars.work_dir}/topic.json";
|
|
69
|
+
exit 0;
|
|
70
|
+
fi;
|
|
71
|
+
if [ -f "${vars.input_file}" ]; then
|
|
72
|
+
cp "${vars.input_file}" "${vars.work_dir}/input.md";
|
|
73
|
+
ABS=$(cd "$(dirname "${vars.input_file}")" && pwd)/$(basename
|
|
74
|
+
"${vars.input_file}");
|
|
75
|
+
QUERY=$(grep -m1 '^# ' "${vars.input_file}" | sed 's/^# *//'
|
|
76
|
+
|| true);
|
|
77
|
+
if [ -z "$QUERY" ]; then
|
|
78
|
+
QUERY=$(basename "${vars.input_file}" | sed 's/\.[^.]*$//');
|
|
79
|
+
fi;
|
|
80
|
+
if [ -z "$QUERY" ]; then
|
|
81
|
+
echo "no query derivable (no H1 and empty basename)" >&2;
|
|
82
|
+
exit 1;
|
|
83
|
+
fi;
|
|
84
|
+
ID=$(printf 'file://%s' "$ABS" | shasum -a 256 | cut -c1-16);
|
|
85
|
+
jq -n --arg id "$ID" --arg body "$(cat "${vars.input_file}")"
|
|
86
|
+
--arg title "$QUERY" --arg sourceUri "file://$ABS" --arg query
|
|
87
|
+
"$QUERY" '{id: $id, body: $body, title: $title, sourceUri:
|
|
88
|
+
$sourceUri, mediaType: "text/markdown",
|
|
89
|
+
metadata: {query: $query, rank: -1, position: 0}}'
|
|
90
|
+
> "${vars.work_dir}/local-doc.json";
|
|
91
|
+
jq -n --arg topic "$QUERY" --arg maxResults
|
|
92
|
+
"${vars.maxResults}" --arg fixture "${vars.fixture}"
|
|
93
|
+
'{topic: $topic, maxResults: ($maxResults | tonumber),
|
|
94
|
+
fixture: ($fixture == "true")}' > "${vars.work_dir}/topic.json";
|
|
95
|
+
else
|
|
96
|
+
echo "input_file not readable: ${vars.input_file}" >&2; exit 1;
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
- id: ingest
|
|
100
|
+
description: >-
|
|
101
|
+
Run web-search ingestion plugin to produce document list (docs.json)
|
|
102
|
+
onEnter:
|
|
103
|
+
- kind: shell
|
|
104
|
+
options:
|
|
105
|
+
command: >-
|
|
106
|
+
kk executor run "${vars.ingestion_plugin}" --in
|
|
107
|
+
"${vars.work_dir}/topic.json" --out "${vars.work_dir}/docs.json"
|
|
108
|
+
--plugins-path "${vars.plugins_path}"
|
|
109
|
+
|
|
110
|
+
- id: generate
|
|
111
|
+
description: >-
|
|
112
|
+
Prepend local-doc (when present) onto docs.json, then run content
|
|
113
|
+
generator to produce content.json
|
|
114
|
+
onEnter:
|
|
115
|
+
- kind: shell
|
|
116
|
+
options:
|
|
117
|
+
command: >-
|
|
118
|
+
if [ -f "${vars.work_dir}/local-doc.json" ]; then
|
|
119
|
+
jq -s '[.[0]] + .[1]' "${vars.work_dir}/local-doc.json"
|
|
120
|
+
"${vars.work_dir}/docs.json" > "${vars.work_dir}/docs.json.tmp"
|
|
121
|
+
&& mv "${vars.work_dir}/docs.json.tmp" "${vars.work_dir}/docs.json";
|
|
122
|
+
fi
|
|
123
|
+
- kind: shell
|
|
124
|
+
options:
|
|
125
|
+
command: >-
|
|
126
|
+
kk executor run "${vars.generator_plugin}" --in
|
|
127
|
+
"${vars.work_dir}/docs.json" --out "${vars.work_dir}/content.json"
|
|
128
|
+
--plugins-path "${vars.plugins_path}"
|
|
129
|
+
|
|
130
|
+
- id: render
|
|
131
|
+
description: >-
|
|
132
|
+
Render content.json to the markdown sidecar content.md via the 0058
|
|
133
|
+
shipped script
|
|
134
|
+
onEnter:
|
|
135
|
+
- kind: shell
|
|
136
|
+
options:
|
|
137
|
+
command: >-
|
|
138
|
+
bun "${vars.render_script}" --in "${vars.work_dir}/content.json"
|
|
139
|
+
--out "${vars.work_dir}/content.md"
|
|
140
|
+
|
|
141
|
+
- id: done
|
|
142
|
+
description: >-
|
|
143
|
+
Terminal — topic materialized, ingested, generated, and rendered to
|
|
144
|
+
content.md. No publish/judge by design (C4).
|
|
145
|
+
- id: failed
|
|
146
|
+
description: Terminal — one of the steps failed
|
|
147
|
+
|
|
148
|
+
transitions:
|
|
149
|
+
- from: prepare
|
|
150
|
+
to: ingest
|
|
151
|
+
description: Works materialized — proceed to web-search ingestion
|
|
152
|
+
guard:
|
|
153
|
+
kind: always
|
|
154
|
+
- from: prepare
|
|
155
|
+
to: failed
|
|
156
|
+
description: >-
|
|
157
|
+
prepare failed (XOR violation / unreadable input) — terminate workflow
|
|
158
|
+
|
|
159
|
+
- from: ingest
|
|
160
|
+
to: generate
|
|
161
|
+
description: Ingestion succeeded — proceed to content generation
|
|
162
|
+
guard:
|
|
163
|
+
kind: always
|
|
164
|
+
- from: ingest
|
|
165
|
+
to: failed
|
|
166
|
+
description: Ingestion failed — terminate workflow
|
|
167
|
+
|
|
168
|
+
- from: generate
|
|
169
|
+
to: render
|
|
170
|
+
description: Content generated — render the markdown sidecar
|
|
171
|
+
guard:
|
|
172
|
+
kind: always
|
|
173
|
+
- from: generate
|
|
174
|
+
to: failed
|
|
175
|
+
description: Generation failed — terminate workflow
|
|
176
|
+
|
|
177
|
+
- from: render
|
|
178
|
+
to: done
|
|
179
|
+
description: content.md rendered — workflow complete
|
|
180
|
+
guard:
|
|
181
|
+
kind: always
|
|
182
|
+
- from: render
|
|
183
|
+
to: failed
|
|
184
|
+
description: Render failed — terminate workflow
|