@warnyin/agents 0.14.0 → 0.15.0
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/CHANGELOG.md +139 -133
- package/README.md +160 -160
- package/package.json +38 -38
- package/src/.claude/agents/warnyin-infra.md +13 -13
- package/src/.claude/agents/warnyin-qa.md +13 -13
- package/src/.claude/agents/warnyin-sa.md +13 -13
- package/src/.claude/agents/warnyin-security.md +13 -13
- package/src/.claude/agents/warnyin-tech-lead.md +13 -13
- package/src/.claude/commands/warnyin/build.md +31 -31
- package/src/.claude/commands/warnyin/design.md +27 -27
- package/src/.claude/commands/warnyin/discovery.md +22 -17
- package/src/.claude/commands/warnyin/explore.md +14 -14
- package/src/.claude/commands/warnyin/init.md +12 -12
- package/src/.claude/commands/warnyin/install-skill.md +14 -14
- package/src/.claude/commands/warnyin/next.md +17 -17
- package/src/.claude/commands/warnyin/ship.md +28 -28
- package/src/.claude/commands/warnyin/triage.md +14 -14
- package/src/.claude/commands/warnyin/update-codemaps.md +12 -12
- package/src/.claude/commands/warnyin/verify.md +20 -20
- package/src/.claude/skills/explore/SKILL.md +8 -8
- package/src/.claude/skills/next/SKILL.md +8 -8
- package/src/.claude/skills/update-codemaps/SKILL.md +8 -8
- package/src/.warnyin/installer/templates/CLAUDE.global.md +5 -5
- package/src/.warnyin/installer/templates/CLAUDE.md +34 -34
- package/src/.warnyin/template/docs/codemap/index.md +18 -18
- package/src/.warnyin/template/docs/features/[feature-name]/business.md +5 -5
- package/src/.warnyin/template/docs/features/[feature-name]/feature.md +5 -5
- package/src/.warnyin/template/docs/features/[feature-name]/spec.md +16 -16
- package/src/.warnyin/template/docs/infra.md +16 -16
- package/src/.warnyin/template/docs/project.md +18 -18
- package/src/.warnyin/template/docs/rule.md +7 -7
- package/src/.warnyin/template/docs/techstack/[component]/about.md +6 -6
- package/src/.warnyin/template/docs/techstack/[component]/rule.md +6 -6
- package/src/.warnyin/template/docs/techstack/[component]/standard.md +6 -6
- package/src/.warnyin/template/docs/techstack/[component]/structure.md +7 -7
- package/src/.warnyin/template/docs/techstack/[component]/test.md +7 -7
- package/src/.warnyin/template/docs/troubleshooting.md +32 -32
- package/src/.warnyin/template/stages/[topic]/build.md +58 -58
- package/src/.warnyin/template/stages/[topic]/business.md +21 -21
- package/src/.warnyin/template/stages/[topic]/design.md +63 -63
- package/src/.warnyin/template/stages/[topic]/discovery.md +69 -69
- package/src/.warnyin/template/stages/[topic]/proposal.md +43 -43
- package/src/.warnyin/template/stages/[topic]/research.md +49 -49
- package/src/.warnyin/template/stages/[topic]/ship.md +32 -32
- package/src/.warnyin/template/stages/[topic]/tasks/[task-name]/issue.md +19 -19
- package/src/.warnyin/template/stages/[topic]/tasks/[task-name]/rule.md +13 -13
- package/src/.warnyin/template/stages/[topic]/tasks/[task-name]/spec.md +36 -36
- package/src/.warnyin/template/stages/[topic]/tasks/[task-name]/standard.md +21 -21
- package/src/.warnyin/template/stages/[topic]/tasks/[task-name]/task.md +40 -40
- package/src/.warnyin/template/stages/[topic]/test.md +46 -46
- package/src/.warnyin/template/stages/[topic]/troubleshooting.md +34 -34
- package/src/.warnyin/template/stages/[topic]/verify.md +44 -44
- package/src/.warnyin/workflow/README.md +105 -102
- package/src/.warnyin/workflow/api-doc.md +93 -93
- package/src/.warnyin/workflow/codemap.md +91 -91
- package/src/.warnyin/workflow/contexts/README.md +51 -51
- package/src/.warnyin/workflow/contexts/build.md +25 -25
- package/src/.warnyin/workflow/contexts/research.md +25 -25
- package/src/.warnyin/workflow/contexts/review.md +25 -25
- package/src/.warnyin/workflow/explore.md +32 -32
- package/src/.warnyin/workflow/init.md +136 -136
- package/src/.warnyin/workflow/next.md +48 -48
- package/src/.warnyin/workflow/roles/README.md +47 -47
- package/src/.warnyin/workflow/roles/ba.md +25 -25
- package/src/.warnyin/workflow/roles/developer.md +31 -31
- package/src/.warnyin/workflow/roles/infra.md +24 -24
- package/src/.warnyin/workflow/roles/po.md +28 -28
- package/src/.warnyin/workflow/roles/qa.md +35 -35
- package/src/.warnyin/workflow/roles/sa.md +28 -28
- package/src/.warnyin/workflow/roles/security.md +39 -39
- package/src/.warnyin/workflow/roles/tech-lead.md +28 -28
- package/src/.warnyin/workflow/scripts/build-wave.mjs +145 -143
- package/src/.warnyin/workflow/scripts/validate-topic.mjs +378 -378
- package/src/.warnyin/workflow/stages/build.md +98 -98
- package/src/.warnyin/workflow/stages/design.md +154 -138
- package/src/.warnyin/workflow/stages/discovery.md +256 -78
- package/src/.warnyin/workflow/stages/ship.md +94 -94
- package/src/.warnyin/workflow/stages/verify.md +82 -82
- package/src/.warnyin/workflow/triage.md +74 -74
- package/src/AGENTS.md +54 -54
- package/src/bin/cli.mjs +310 -310
|
@@ -1,143 +1,145 @@
|
|
|
1
|
-
// build-wave — fan-out หนึ่ง sub-agent ต่อหนึ่ง task ใน "หนึ่ง wave" (task ที่ independent กัน)
|
|
2
|
-
// main loop (BUILD command) เรียก script นี้ทีละ wave ตาม dependency แล้ว integrate ระหว่าง wave
|
|
3
|
-
//
|
|
4
|
-
// args = {
|
|
5
|
-
// slug: string, // ชื่อ topic เช่น "billing-redesign"
|
|
6
|
-
// tasks: string[] | Array<{ name: string, model?: string }>,
|
|
7
|
-
// // ชื่อ task ใน wave นี้ (โฟลเดอร์ docs/stages/<slug>/tasks/<task>)
|
|
8
|
-
// // รับทั้ง string[] (เดิม, backward compat) และ {name, model?}[] — normalize ภายในเป็น {name, model}
|
|
9
|
-
// // model = pass-through string (orchestrator map tier→รุ่นจริงก่อนส่งเข้ามา); script ไม่ map/ไม่ hardcode ชื่อรุ่น
|
|
10
|
-
// isolate?: boolean, // true = worktree ต่อ task (ดีฟอลต์), false = shared tree (sequential)
|
|
11
|
-
// baseRef?: string, // ชื่อ build branch เช่น "build/my-topic"; ไม่ส่ง = ไม่ sync (backward compat)
|
|
12
|
-
// }
|
|
13
|
-
|
|
14
|
-
export const meta = {
|
|
15
|
-
name: 'build-wave',
|
|
16
|
-
description: 'BUILD: fan-out sub-agent ต่อ task ใน wave เดียว — implement + test/lint + commit แล้วรายงานผล',
|
|
17
|
-
phases: [{ title: 'Build wave', detail: 'parallel agent, หนึ่งตัวต่อหนึ่ง task (worktree isolation)' }],
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// บาง harness ส่ง args ของ Workflow เป็น string (JSON text) ไม่ใช่ object — รับทั้งสองแบบ
|
|
21
|
-
const A = typeof args === 'string' ? JSON.parse(args) : (args || {})
|
|
22
|
-
const slug = A.slug
|
|
23
|
-
const isolate = !(A.isolate === false)
|
|
24
|
-
const baseRef = A.baseRef || null // ชื่อ build branch เช่น "build/my-topic"; ไม่ส่ง = ไม่ sync (backward compat)
|
|
25
|
-
|
|
26
|
-
// normalize tasks: รับทั้ง string[] (เดิม) และ {name, model?}[] (ใหม่) → ภายในเป็น {name, model} เสมอ
|
|
27
|
-
// string element → {name, model: undefined} (backward compat); model = pass-through string ไม่ map/ไม่ hardcode
|
|
28
|
-
export function
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
`
|
|
93
|
-
` -
|
|
94
|
-
` - ${dir}/
|
|
95
|
-
` - ${dir}/
|
|
96
|
-
` -
|
|
97
|
-
` - rule
|
|
98
|
-
`
|
|
99
|
-
`
|
|
100
|
-
`
|
|
101
|
-
`
|
|
102
|
-
`
|
|
103
|
-
`
|
|
104
|
-
`
|
|
105
|
-
`
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
`
|
|
114
|
-
`
|
|
115
|
-
`
|
|
116
|
-
` -
|
|
117
|
-
` -
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
1
|
+
// build-wave — fan-out หนึ่ง sub-agent ต่อหนึ่ง task ใน "หนึ่ง wave" (task ที่ independent กัน)
|
|
2
|
+
// main loop (BUILD command) เรียก script นี้ทีละ wave ตาม dependency แล้ว integrate ระหว่าง wave
|
|
3
|
+
//
|
|
4
|
+
// args = {
|
|
5
|
+
// slug: string, // ชื่อ topic เช่น "billing-redesign"
|
|
6
|
+
// tasks: string[] | Array<{ name: string, model?: string }>,
|
|
7
|
+
// // ชื่อ task ใน wave นี้ (โฟลเดอร์ docs/stages/<slug>/tasks/<task>)
|
|
8
|
+
// // รับทั้ง string[] (เดิม, backward compat) และ {name, model?}[] — normalize ภายในเป็น {name, model}
|
|
9
|
+
// // model = pass-through string (orchestrator map tier→รุ่นจริงก่อนส่งเข้ามา); script ไม่ map/ไม่ hardcode ชื่อรุ่น
|
|
10
|
+
// isolate?: boolean, // true = worktree ต่อ task (ดีฟอลต์), false = shared tree (sequential)
|
|
11
|
+
// baseRef?: string, // ชื่อ build branch เช่น "build/my-topic"; ไม่ส่ง = ไม่ sync (backward compat)
|
|
12
|
+
// }
|
|
13
|
+
|
|
14
|
+
export const meta = {
|
|
15
|
+
name: 'build-wave',
|
|
16
|
+
description: 'BUILD: fan-out sub-agent ต่อ task ใน wave เดียว — implement + test/lint + commit แล้วรายงานผล',
|
|
17
|
+
phases: [{ title: 'Build wave', detail: 'parallel agent, หนึ่งตัวต่อหนึ่ง task (worktree isolation)' }],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// บาง harness ส่ง args ของ Workflow เป็น string (JSON text) ไม่ใช่ object — รับทั้งสองแบบ
|
|
21
|
+
const A = typeof args === 'string' ? JSON.parse(args) : (args || {})
|
|
22
|
+
const slug = A.slug
|
|
23
|
+
const isolate = !(A.isolate === false)
|
|
24
|
+
const baseRef = A.baseRef || null // ชื่อ build branch เช่น "build/my-topic"; ไม่ส่ง = ไม่ sync (backward compat)
|
|
25
|
+
|
|
26
|
+
// normalize tasks: รับทั้ง string[] (เดิม) และ {name, model?}[] (ใหม่) → ภายในเป็น {name, model} เสมอ
|
|
27
|
+
// string element → {name, model: undefined} (backward compat); model = pass-through string ไม่ map/ไม่ hardcode
|
|
28
|
+
// ★ ห้าม `export function` — Workflow runtime wrap body เป็น async fn ยอมรับเฉพาะ `export const meta`
|
|
29
|
+
// (export อื่น → SyntaxError); unit test สกัดด้วย extractFn ใน build-wave.test.mjs (ดู installer/rule.md §build orchestration)
|
|
30
|
+
function normalizeTasks(rawTasks) {
|
|
31
|
+
return (rawTasks || []).map((t) =>
|
|
32
|
+
typeof t === 'string' ? { name: t, model: undefined } : { name: t.name, model: t.model })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// สร้าง opts ของ agent() แบบ immutable — conditional spread: key หายเมื่อไม่มีค่า (ไม่ใช่ undefined)
|
|
36
|
+
// แนวเดียวกับ baseRef เดิม (optional arg, conditional เฉพาะเมื่อมีค่า)
|
|
37
|
+
function buildOpts(task, isolate) {
|
|
38
|
+
return {
|
|
39
|
+
label: `build:${task.name}`,
|
|
40
|
+
schema: RESULT_SCHEMA,
|
|
41
|
+
...(isolate && { isolation: 'worktree' }),
|
|
42
|
+
...(task.model && { model: task.model }),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const tasks = normalizeTasks(A.tasks)
|
|
47
|
+
|
|
48
|
+
if (!slug || tasks.length === 0) {
|
|
49
|
+
log('ไม่มี slug หรือ tasks — ไม่มีอะไรให้ build')
|
|
50
|
+
return { slug: slug || null, results: [], failed: [] }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
phase('Build wave')
|
|
54
|
+
log(`Build ${tasks.length} task ของ "${slug}"${isolate ? ' · worktree isolation' : ' · shared tree'}`)
|
|
55
|
+
|
|
56
|
+
const RESULT_SCHEMA = {
|
|
57
|
+
type: 'object',
|
|
58
|
+
additionalProperties: false,
|
|
59
|
+
required: ['task', 'status', 'summary'],
|
|
60
|
+
properties: {
|
|
61
|
+
task: { type: 'string', description: 'ชื่อ task' },
|
|
62
|
+
status: { enum: ['passed', 'failed'], description: 'passed ก็ต่อเมื่อ test/lint เขียวจริง' },
|
|
63
|
+
summary: { type: 'string', description: 'สรุปสั้นๆ ว่าทำอะไร' },
|
|
64
|
+
branch: { type: 'string', description: 'ชื่อ git branch ของ worktree (ถ้า isolate) ให้ main loop merge' },
|
|
65
|
+
filesChanged: { type: 'array', items: { type: 'string' } },
|
|
66
|
+
testResult: { type: 'string', description: 'ผล test-flow + build/lint' },
|
|
67
|
+
notes: { type: 'string', description: 'conflict/ข้อควรระวัง/ rule ใหม่ที่ note ไว้' },
|
|
68
|
+
troubleshooting: {
|
|
69
|
+
type: 'array',
|
|
70
|
+
description: 'ปัญหายาก/เจอซ้ำที่แก้สำเร็จ — main loop จะเขียนรวมลง topic troubleshooting.md',
|
|
71
|
+
items: {
|
|
72
|
+
type: 'object',
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
required: ['title', 'rootCause', 'solution'],
|
|
75
|
+
properties: {
|
|
76
|
+
title: { type: 'string' },
|
|
77
|
+
symptom: { type: 'string', description: 'อาการ/error message' },
|
|
78
|
+
rootCause: { type: 'string' },
|
|
79
|
+
solution: { type: 'string' },
|
|
80
|
+
prevention: { type: 'string', description: 'วิธีป้องกันไม่ให้เกิดซ้ำ' },
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function prompt(task) {
|
|
88
|
+
const dir = `docs/stages/${slug}/tasks/${task}`
|
|
89
|
+
const lines = [
|
|
90
|
+
`คุณคือ build sub-agent ของ task "${task}" (vertical slice) ทำตาม playbook .warnyin/workflow/stages/build.md`,
|
|
91
|
+
``,
|
|
92
|
+
`1. อ่านให้ครบก่อนเขียนโค้ด:`,
|
|
93
|
+
` - .warnyin/workflow/roles/developer.md (role card: lens + checklist ก่อนส่งงาน — ทำตามทุกข้อ)`,
|
|
94
|
+
` - ${dir}/task.md (เป้าหมาย + sub-tasks + dependency + acceptance)`,
|
|
95
|
+
` - ${dir}/spec.md (API/UXUI/data-flow/user-flow/persona/test-flow)`,
|
|
96
|
+
` - ${dir}/standard.md (pattern โค้ด, shared component — reuse ห้ามเขียนซ้ำ)`,
|
|
97
|
+
` - ${dir}/rule.md (กฎที่ต้อง follow)`,
|
|
98
|
+
` - ภาพรวม: docs/stages/${slug}/design.md, proposal.md`,
|
|
99
|
+
` - rule/standard กลางที่อ้างถึงใน docs/techstack/<component>/`,
|
|
100
|
+
`2. Implement ให้ครบทุก sub-task แบบ vertical slice (end-to-end) ทำตาม standard.md + rule.md เคร่งครัด`,
|
|
101
|
+
`3. รัน test-flow ใน spec.md + build/lint ของ component นั้น`,
|
|
102
|
+
`4. ถ้าเจอ error/ติดปัญหา → อ่าน docs/troubleshooting.md ก่อน เผื่อเคยแก้แล้ว`,
|
|
103
|
+
`5. รายงาน status=passed เฉพาะเมื่อ test/build เขียวจริง; ถ้าแก้ไม่ได้ → status=failed พร้อมเหตุผล`,
|
|
104
|
+
` ห้ามรายงานผ่านทั้งที่ยังแดง`,
|
|
105
|
+
`6. ห้ามแก้ไฟล์ rule/standard กลางใน docs/ (rule ใหม่ note ไว้ใน ${dir}/rule.md อยู่แล้ว รอ SHIP)`,
|
|
106
|
+
`7. อัปเดตสถานะ + acceptance ที่ผ่านใน ${dir}/task.md`,
|
|
107
|
+
`8. ปัญหาที่ "ยาก/เจอซ้ำ" และแก้สำเร็จ → ใส่ในฟิลด์ troubleshooting (main loop จะรวมลง topic troubleshooting.md)`,
|
|
108
|
+
]
|
|
109
|
+
// worktree fork จาก main (คุมไม่ได้) → ให้ agent sync build branch เข้า worktree เองก่อนทำงาน
|
|
110
|
+
// แทรกเป็น step "0." ก่อน "1. อ่านให้ครบ" — เฉพาะ isolate && baseRef (ไม่ renumber step 1-9; !baseRef = พฤติกรรมเดิม)
|
|
111
|
+
if (isolate && baseRef) {
|
|
112
|
+
lines.splice(2, 0,
|
|
113
|
+
`0. **★ Sync build branch เข้า worktree ก่อน (ทำก่อน Read ไฟล์ใดๆ):** รัน`,
|
|
114
|
+
` \`git merge ${baseRef} --no-edit || (git merge --abort; <รายงาน failed>)\``,
|
|
115
|
+
` (worktree fork จาก main — ต้อง merge build branch เพื่อให้เห็น docs/stages/${slug}/ + output ของ wave ก่อนหน้า)`,
|
|
116
|
+
` - ปกติเป็น fast-forward (main มักเป็น ancestor ของ build branch); ถ้าเป็น 3-way แล้ว conflict → **abort + รายงาน failed** (ห้ามทิ้ง worktree ค้าง MERGE state — step commit ท้ายจะพัง)`,
|
|
117
|
+
` - ถ้าล้มด้วย lock error ชั่วคราว (transient \`index.lock\`/\`packed-refs\`) → **retry 1 ครั้ง** ก่อนรายงาน failed`,
|
|
118
|
+
` - **★ hard-stop กัน improvise (panel B2):** หลัง merge ถ้าไฟล์ \`${dir}/task.md\` **ยังไม่ปรากฏ** → **STOP รายงาน failed ทันที ห้าม improvise/git reset เอง** (กันวนรอย KB#14)`,
|
|
119
|
+
` - บันทึกผล merge ลงฟิลด์ \`notes\` (เช่น "merged ${baseRef}: fast-forward to <sha>") เพื่อ main loop verify ว่า sync เกิดจริง (Infra-S5)`,
|
|
120
|
+
``,
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
if (isolate) {
|
|
124
|
+
lines.push(
|
|
125
|
+
`9. คุณอยู่ใน git worktree แยก: เมื่อเสร็จและเขียวแล้ว ให้ commit งาน (git add -A && git commit -m "build(${task}): ...")`,
|
|
126
|
+
` แล้วรายงานชื่อ branch (git rev-parse --abbrev-ref HEAD) ในฟิลด์ branch เพื่อให้ main loop merge`,
|
|
127
|
+
)
|
|
128
|
+
} else {
|
|
129
|
+
lines.push(`9. (shared tree) อย่า commit เอง — main loop จะ commit ให้หลังตรวจ`)
|
|
130
|
+
}
|
|
131
|
+
lines.push(``, `คืนผลตาม schema.`)
|
|
132
|
+
return lines.join('\n')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const results = await parallel(
|
|
136
|
+
tasks.map((task) => () => agent(prompt(task.name), buildOpts(task, isolate)))
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
const clean = results.filter(Boolean)
|
|
140
|
+
const failed = clean.filter((r) => r.status === 'failed').map((r) => r.task)
|
|
141
|
+
const skipped = tasks.filter((t) => !clean.some((r) => r.task === t.name)).map((t) => t.name)
|
|
142
|
+
|
|
143
|
+
log(`เสร็จ ${clean.length}/${tasks.length} · ผ่าน ${clean.length - failed.length} · ล้ม ${failed.length}${skipped.length ? ` · ข้าม ${skipped.length}` : ''}`)
|
|
144
|
+
|
|
145
|
+
return { slug, results: clean, failed, skipped }
|