Skip to content

Commit 67bcdab

Browse files
authored
Merge pull request #10 from Multiplier-Labs/fix/remove-local-scripts
Remove local scripts, fix workflows, add repo-level workflow definitions
2 parents 8ca5738 + 7f9c4ee commit 67bcdab

File tree

8 files changed

+162
-29
lines changed

8 files changed

+162
-29
lines changed

dist/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
77
<title>Codekin</title>
8-
<script type="module" crossorigin src="/assets/index-pBYmsgpF.js"></script>
9-
<link rel="stylesheet" crossorigin href="/assets/index-MUdfhLvr.css">
8+
<script type="module" crossorigin src="/assets/index-CfSEhsc3.js"></script>
9+
<link rel="stylesheet" crossorigin href="/assets/index-a8q-qxCN.css">
1010
</head>
1111
<body class="bg-neutral-12 text-neutral-2">
1212
<div id="root"></div>

docs/WORKFLOWS.md

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Automated Workflows
22

3-
Codekin includes an automated workflow system that runs Claude Code sessions on a schedule to produce structured reports — code reviews, security audits, coverage assessments, and more. Workflows are defined as Markdown files with YAML frontmatter.
3+
Codekin includes an automated workflow system that runs Claude Code sessions on a schedule to produce structured reports — code reviews, security audits, coverage assessments, and more. Workflows are defined as Markdown files with YAML frontmatter. Codekin ships with six built-in workflows, and you can define your own custom workflows per-repo.
44

55
---
66

@@ -77,34 +77,77 @@ All built-in workflows are loaded automatically at server start.
7777

7878
---
7979

80-
## Per-Repo Prompt Overrides
80+
## Custom Repo Workflows
8181

82-
You can override the prompt for any workflow kind on a per-repository basis without modifying the built-in definitions. Create a file at:
82+
You can define your own workflow types on a per-repository basis — no changes to Codekin itself are needed. Place `.md` workflow files at:
8383

8484
```
8585
{repoPath}/.codekin/workflows/{kind}.md
8686
```
8787

88-
For example, to override the daily code review prompt for a specific repo:
88+
These files use the exact same format as built-in definitions (YAML frontmatter + prompt body). Codekin discovers them automatically and they appear alongside built-in workflows in the UI.
89+
90+
### Creating a Custom Workflow
91+
92+
1. Create the directory `{repoPath}/.codekin/workflows/` in your repo.
93+
94+
2. Add a `.md` file with the full frontmatter and prompt:
95+
96+
```markdown
97+
---
98+
kind: api-docs.weekly
99+
name: API Documentation Check
100+
sessionPrefix: api-docs
101+
outputDir: .codekin/reports/api-docs
102+
filenameSuffix: _api-docs.md
103+
commitMessage: chore: api docs check
104+
---
105+
You are reviewing the API documentation for this project.
106+
107+
1. Find all REST endpoints and verify they have corresponding documentation
108+
2. Check for outdated examples or missing parameters
109+
3. Produce a Markdown report with a table of endpoints and their doc status
110+
111+
Important: Do NOT modify any source files.
112+
```
113+
114+
3. In the Codekin UI, select a repo and click **Add Workflow**. Custom workflows defined in that repo will appear in the workflow selector with a "repo" label.
115+
116+
### Overriding Built-in Prompts
117+
118+
If a repo workflow file uses the same `kind` as a built-in (e.g. `code-review.daily`), the repo file's **prompt** replaces the built-in prompt at run time. The metadata (outputDir, commitMessage, etc.) still comes from the built-in definition.
119+
120+
For example, to customize the daily code review for a specific repo:
89121

90122
```
91123
my-repo/.codekin/workflows/code-review.daily.md
92124
```
93125

94-
The override file uses the same MD format (frontmatter + prompt body). At run time, the loader checks for this file first; if found, its prompt replaces the global one for that run. The frontmatter in an override file is parsed but only the `prompt` body is used — the workflow metadata (outputDir, commitMessage, etc.) always comes from the built-in definition.
95-
96-
**Use cases for per-repo overrides:**
126+
**Use cases for overrides:**
97127

98128
- Focus the review on areas specific to this codebase (e.g. "pay special attention to the payment module")
99129
- Adjust the report format or section headings
100130
- Add repo-specific shell commands or file paths
101131
- Restrict scope to certain directories or file types
102132

133+
### API: Listing Available Kinds
134+
135+
The `GET /api/workflows/kinds` endpoint returns all available workflow kinds. Pass `?repoPath=/path/to/repo` to include repo-specific workflows in the response:
136+
137+
```json
138+
{
139+
"kinds": [
140+
{ "kind": "code-review.daily", "name": "Daily Code Review", "source": "builtin" },
141+
{ "kind": "api-docs.weekly", "name": "API Documentation Check", "source": "repo" }
142+
]
143+
}
144+
```
145+
103146
---
104147

105-
## Adding New Workflow Types
148+
## Adding New Built-in Workflow Types
106149

107-
To add a new built-in workflow, create a `.md` file in `server/workflows/` following the format above. It will be discovered and registered automatically at next server start — no code changes required.
150+
To add a new built-in workflow that ships with Codekin, create a `.md` file in `server/workflows/` following the format above. It will be discovered and registered automatically at next server start — no code changes required.
108151

109152
Choose a `kind` that doesn't conflict with existing workflows. The convention is `<topic>.<frequency>` where frequency is one of `daily`, `weekly`, or `monthly`.
110153

server/workflow-engine.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,10 @@ export class WorkflowEngine extends EventEmitter {
228228
console.log(`[workflow] Registered workflow: ${definition.kind} (${definition.steps.length} steps)`)
229229
}
230230

231+
hasWorkflow(kind: string): boolean {
232+
return this.workflows.has(kind)
233+
}
234+
231235
// -------------------------------------------------------------------------
232236
// Run management
233237
// -------------------------------------------------------------------------

server/workflow-routes.ts

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,29 @@ import {
1515
updateReviewRepo,
1616
type ReviewRepoConfig,
1717
} from './workflow-config.js'
18+
import { listAvailableKinds, ensureRepoWorkflowsRegistered } from './workflow-loader.js'
19+
import type { SessionManager } from './session-manager.js'
1820

1921
type VerifyFn = (token: string | undefined) => boolean
2022
type ExtractFn = (req: Request) => string | undefined
2123

22-
/** Sync cron schedules with the current workflow config. */
23-
export function syncSchedules() {
24+
/**
25+
* Sync cron schedules with the current workflow config.
26+
* When `sessions` is provided, also registers any standalone repo workflows.
27+
*/
28+
export function syncSchedules(sessions?: SessionManager) {
2429
const engine = getWorkflowEngine()
2530
const config = loadWorkflowConfig()
2631
const existingSchedules = engine.listSchedules()
2732
const configIds = new Set(config.reviewRepos.map(r => r.id))
2833

2934
// Create or update schedules for configured repos
3035
for (const repo of config.reviewRepos) {
36+
// Register any standalone repo workflows before scheduling
37+
if (sessions) {
38+
ensureRepoWorkflowsRegistered(engine, sessions, repo.repoPath)
39+
}
40+
3141
engine.upsertSchedule({
3242
id: repo.id,
3343
kind: repo.kind ?? 'code-review.daily',
@@ -45,7 +55,7 @@ export function syncSchedules() {
4555
}
4656
}
4757

48-
export function createWorkflowRouter(verifyToken: VerifyFn, extractToken: ExtractFn): Router {
58+
export function createWorkflowRouter(verifyToken: VerifyFn, extractToken: ExtractFn, sessions?: SessionManager): Router {
4959
const router = Router()
5060

5161
/** Auth middleware for all workflow routes. */
@@ -70,6 +80,16 @@ export function createWorkflowRouter(verifyToken: VerifyFn, extractToken: Extrac
7080
}
7181
}
7282

83+
// -------------------------------------------------------------------------
84+
// Kinds
85+
// -------------------------------------------------------------------------
86+
87+
router.get('/kinds', (req, res) => {
88+
const repoPath = req.query.repoPath as string | undefined
89+
const kinds = listAvailableKinds(repoPath)
90+
res.json({ kinds })
91+
})
92+
7393
// -------------------------------------------------------------------------
7494
// Runs
7595
// -------------------------------------------------------------------------
@@ -203,6 +223,13 @@ export function createWorkflowRouter(verifyToken: VerifyFn, extractToken: Extrac
203223
return res.status(400).json({ error: 'Missing required fields: id, name, repoPath, cronExpression' })
204224
}
205225

226+
// Register any standalone repo workflows before saving config
227+
if (sessions) {
228+
try {
229+
ensureRepoWorkflowsRegistered(getWorkflowEngine(), sessions, repoPath)
230+
} catch { /* engine may not be ready */ }
231+
}
232+
206233
const config = addReviewRepo({
207234
id,
208235
name,
@@ -215,7 +242,7 @@ export function createWorkflowRouter(verifyToken: VerifyFn, extractToken: Extrac
215242

216243
// Re-sync schedules with updated config
217244
try {
218-
syncSchedules()
245+
syncSchedules(sessions)
219246
} catch {
220247
// Engine might not be ready yet
221248
}
@@ -226,7 +253,7 @@ export function createWorkflowRouter(verifyToken: VerifyFn, extractToken: Extrac
226253
router.patch('/config/repos/:id', (req, res) => {
227254
try {
228255
const config = updateReviewRepo(req.params.id, req.body)
229-
try { syncSchedules() } catch { /* engine may not be ready */ }
256+
try { syncSchedules(sessions) } catch { /* engine may not be ready */ }
230257
res.json({ config })
231258
} catch (err) {
232259
res.status(404).json({ error: err instanceof Error ? err.message : 'Repo not found' })
@@ -238,7 +265,7 @@ export function createWorkflowRouter(verifyToken: VerifyFn, extractToken: Extrac
238265

239266
// Re-sync schedules with updated config
240267
try {
241-
syncSchedules()
268+
syncSchedules(sessions)
242269
} catch {
243270
// Engine might not be ready yet
244271
}

server/ws-server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ app.use(createAuthRouter(verifyToken, extractToken, sessions, claudeAvailable, c
226226
app.use(createSessionRouter(verifyToken, extractToken, sessions))
227227
app.use(createWebhookRouter(verifyToken, extractToken, webhookHandler, stepflowHandler))
228228
app.use(createUploadRouter(verifyToken, extractToken))
229-
app.use('/api/workflows', createWorkflowRouter(verifyToken, extractToken))
229+
app.use('/api/workflows', createWorkflowRouter(verifyToken, extractToken, sessions))
230230

231231
// --- SPA fallback: serve index.html for non-API routes (client-side routing) ---
232232
if (FRONTEND_DIST && existsSync(FRONTEND_DIST)) {
@@ -469,7 +469,7 @@ server.listen(port, '0.0.0.0', () => {
469469
})
470470

471471
// Sync cron schedules with config and start scheduler
472-
syncSchedules()
472+
syncSchedules(sessions)
473473
engine.startCronScheduler()
474474
console.log('[workflow] Workflow engine ready')
475475
} catch (err) {

src/components/AddWorkflowModal.tsx

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
* Modal dialog for registering a new scheduled workflow.
33
*/
44

5-
import { useState } from 'react'
5+
import { useState, useEffect } from 'react'
66
import { IconX, IconLoader2 } from '@tabler/icons-react'
77
import { useRepos } from '../hooks/useRepos'
8-
import type { ReviewRepoConfig } from '../lib/workflowApi'
8+
import { listKinds } from '../lib/workflowApi'
9+
import type { ReviewRepoConfig, WorkflowKindInfo } from '../lib/workflowApi'
910
import {
1011
WORKFLOW_KINDS, DAY_PATTERNS,
1112
buildCron, formatHour, describeCron, slugify,
@@ -22,17 +23,18 @@ interface FormState {
2223
}
2324

2425
interface Props {
26+
token: string
2527
onClose: () => void
2628
onAdd: (repo: ReviewRepoConfig) => Promise<void>
2729
}
2830

29-
export function AddWorkflowModal({ onClose, onAdd }: Props) {
31+
export function AddWorkflowModal({ token, onClose, onAdd }: Props) {
3032
const { groups, loading: reposLoading } = useRepos()
3133
const allRepos = groups.flatMap(g => g.repos)
3234

3335
const [selectedRepoId, setSelectedRepoId] = useState<string>('')
3436
const [form, setForm] = useState<FormState>({
35-
kind: 'coverage.daily',
37+
kind: '',
3638
repoPath: '',
3739
repoName: '',
3840
cronHour: 6,
@@ -42,6 +44,32 @@ export function AddWorkflowModal({ onClose, onAdd }: Props) {
4244
const [saving, setSaving] = useState(false)
4345
const [formError, setFormError] = useState<string | null>(null)
4446

47+
// Dynamic kinds: built-in fallback + fetched from server (repo-aware)
48+
const [kinds, setKinds] = useState<WorkflowKindInfo[]>(
49+
WORKFLOW_KINDS.map(k => ({ kind: k.value, name: k.label, source: 'builtin' as const }))
50+
)
51+
const [kindsLoading, setKindsLoading] = useState(false)
52+
53+
// Fetch kinds when repo selection changes (includes repo-specific workflows)
54+
useEffect(() => {
55+
let cancelled = false
56+
setKindsLoading(true)
57+
listKinds(token, form.repoPath || undefined)
58+
.then(fetched => {
59+
if (cancelled) return
60+
if (fetched.length > 0) {
61+
setKinds(fetched)
62+
// Auto-select first kind if current selection is not in the new list
63+
if (!fetched.some(k => k.kind === form.kind)) {
64+
setForm(f => ({ ...f, kind: fetched[0].kind }))
65+
}
66+
}
67+
})
68+
.catch(() => { /* keep fallback */ })
69+
.finally(() => { if (!cancelled) setKindsLoading(false) })
70+
return () => { cancelled = true }
71+
}, [token, form.repoPath])
72+
4573
const handleRepoSelect = (repoId: string) => {
4674
setSelectedRepoId(repoId)
4775
const repo = allRepos.find(r => r.id === repoId)
@@ -53,6 +81,7 @@ export function AddWorkflowModal({ onClose, onAdd }: Props) {
5381
const handleSubmit = async (e: React.FormEvent) => {
5482
e.preventDefault()
5583
if (!form.repoPath.trim()) { setFormError('Please select a repository'); return }
84+
if (!form.kind) { setFormError('Please select a workflow'); return }
5685

5786
setSaving(true)
5887
setFormError(null)
@@ -119,21 +148,27 @@ export function AddWorkflowModal({ onClose, onAdd }: Props) {
119148
<div>
120149
<div className="flex items-center justify-between mb-1.5">
121150
<label className="block text-[13px] font-medium text-neutral-3">Workflow</label>
122-
<CategoryBadge kind={form.kind} />
151+
<div className="flex items-center gap-2">
152+
{kindsLoading && <IconLoader2 size={12} stroke={2} className="animate-spin text-neutral-5" />}
153+
{form.kind && <CategoryBadge kind={form.kind} />}
154+
</div>
123155
</div>
124156
<div className="grid grid-cols-3 gap-2">
125-
{WORKFLOW_KINDS.map(k => (
157+
{kinds.map(k => (
126158
<button
127-
key={k.value}
159+
key={k.kind}
128160
type="button"
129-
onClick={() => setForm(f => ({ ...f, kind: k.value }))}
161+
onClick={() => setForm(f => ({ ...f, kind: k.kind }))}
130162
className={`rounded-md border py-2 text-[13px] font-medium transition-colors
131-
${form.kind === k.value
163+
${form.kind === k.kind
132164
? 'border-accent-6 bg-accent-9/40 text-accent-2'
133165
: 'border-neutral-7 bg-neutral-10 text-neutral-3 hover:border-neutral-6 hover:text-neutral-2'
134166
}`}
135167
>
136-
{k.label}
168+
{k.name}
169+
{k.source === 'repo' && (
170+
<span className="block text-[10px] text-neutral-5 font-normal mt-0.5">repo</span>
171+
)}
137172
</button>
138173
))}
139174
</div>

src/components/WorkflowsView.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,7 @@ export function WorkflowsView({ token, onNavigateToSession }: Props) {
530530
{/* Add Workflow Modal */}
531531
{showAddForm && (
532532
<AddWorkflowModal
533+
token={token}
533534
onClose={() => setShowAddForm(false)}
534535
onAdd={addRepo}
535536
/>

src/lib/workflowApi.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,29 @@ export interface WorkflowConfig {
7272
reviewRepos: ReviewRepoConfig[]
7373
}
7474

75+
export interface WorkflowKindInfo {
76+
kind: string
77+
name: string
78+
source: 'builtin' | 'repo'
79+
}
80+
81+
// ---------------------------------------------------------------------------
82+
// Kinds
83+
// ---------------------------------------------------------------------------
84+
85+
export async function listKinds(
86+
token: string,
87+
repoPath?: string,
88+
): Promise<WorkflowKindInfo[]> {
89+
const params = new URLSearchParams()
90+
if (repoPath) params.set('repoPath', repoPath)
91+
const qs = params.toString()
92+
const res = await fetch(`${BASE}/kinds${qs ? `?${qs}` : ''}`, { headers: headers(token) })
93+
if (!res.ok) throw new Error(`Failed to list kinds: ${res.status}`)
94+
const data = await res.json()
95+
return data.kinds
96+
}
97+
7598
// ---------------------------------------------------------------------------
7699
// Runs
77100
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)