feat: add six specialized Claude agent definitions under .claude/agents for audit, devii, docs, dry, fanout, and frontend maintenance
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
export const meta = {
|
||||
name: 'job-service',
|
||||
description: 'Scaffold an async JobService (the zip/fork pattern): the JobService subclass, enqueue/status/download routes, the JobOut schema, main.py registration, Devii tools, JobPoller frontend, and docs, then verify',
|
||||
phases: [
|
||||
{ title: 'Understand', detail: 'read ZipService and ForkService as the template' },
|
||||
{ title: 'Plan', detail: 'a per-touchpoint plan for the new job kind' },
|
||||
{ title: 'Implement', detail: 'build the service and all consumers in the repo' },
|
||||
{ title: 'Verify', detail: 'completeness, security, and audit-log review' },
|
||||
{ title: 'Fix', detail: 'close gaps and write the job tests' },
|
||||
],
|
||||
}
|
||||
|
||||
const RULES = [
|
||||
'Obey DevPlace hard rules while editing:',
|
||||
'- No comments or docstrings in source except the file header and @tool docstrings. New files start with the "retoor <retoor@molodetz.nl>" header.',
|
||||
'- No em-dash characters; use a hyphen. Full Python type hints; pathlib over os; Pydantic input with max lengths.',
|
||||
'- Runtime artifacts live in config.DATA_DIR (the var/ dir), OUTSIDE the devplacepy package and NOT under /static. Heavy compression or blocking work runs in a subprocess. SQLite stays synchronous.',
|
||||
'- Enqueue endpoints own authz (require_user plus any resource guard); status and download are capability URLs scoped only by the unguessable uuid7. Soft-delete the job tracking rows; permanent artifacts are not deleted by cleanup().',
|
||||
'- Record audit events with record_system in the service. Frontend status polling uses JobPoller, never a bespoke loop.',
|
||||
'- Validate with "python -m agents.validator ." and "python -c \\"from devplacepy.main import app\\"". NEVER run the test suite. Never perform any git write.',
|
||||
].join('\n')
|
||||
|
||||
const CHECKLIST = [
|
||||
'A new async job kind must wire all of these (mirror ZipService/ForkService):',
|
||||
'1. services/jobs/{kind}_service.py - subclass JobService, set kind, implement async process(self, job) -> dict and cleanup(self, job).',
|
||||
'2. main.py - register the service via service_manager.register(...).',
|
||||
'3. routers/{area}.py - an enqueue route (guarded) calling queue.enqueue(kind=...), a GET status route returning a *JobOut, and a download/result route (FileResponse capability URL) where applicable.',
|
||||
'4. schemas.py - the *JobOut model with every key the status JSON returns.',
|
||||
'5. services/devii/actions/catalog.py - Devii tools for enqueue and status.',
|
||||
'6. docs_api.py - endpoint() entries for the enqueue, status, and download routes.',
|
||||
'7. static/js - wire JobPoller.run(statusUrl, {onDone, onFailed, onTimeout}) on the triggering element.',
|
||||
'8. CLI (optional) - a prune/clear subcommand if artifacts accumulate.',
|
||||
'9. README.md + AGENTS.md - document the new job kind.',
|
||||
].join('\n')
|
||||
|
||||
function jobBrief() {
|
||||
if (!args) return ''
|
||||
if (typeof args === 'string') return args
|
||||
if (typeof args.description === 'string') return args.description
|
||||
return JSON.stringify(args)
|
||||
}
|
||||
|
||||
const ask = jobBrief()
|
||||
if (!ask) {
|
||||
log('No job description provided. Invoke as /job-service <what heavy work to run off the request path>.')
|
||||
return { error: 'no description provided' }
|
||||
}
|
||||
|
||||
const MAP_SCHEMA = {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['summary'],
|
||||
properties: {
|
||||
summary: { type: 'string' },
|
||||
template: { type: 'string' },
|
||||
files: { type: 'array', items: { type: 'string' } },
|
||||
},
|
||||
}
|
||||
|
||||
const PLAN_SCHEMA = {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['steps'],
|
||||
properties: {
|
||||
steps: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['file', 'change'],
|
||||
properties: { file: { type: 'string' }, change: { type: 'string' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const BUILD_SCHEMA = {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['summary', 'filesChanged', 'validatorPassed'],
|
||||
properties: {
|
||||
summary: { type: 'string' },
|
||||
kind: { type: 'string' },
|
||||
filesChanged: { type: 'array', items: { type: 'string' } },
|
||||
validatorPassed: { type: 'boolean' },
|
||||
importOk: { type: 'boolean' },
|
||||
},
|
||||
}
|
||||
|
||||
const FINDINGS_SCHEMA = {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['summary', 'findings'],
|
||||
properties: {
|
||||
summary: { type: 'string' },
|
||||
findings: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['severity', 'file', 'rule', 'message'],
|
||||
properties: {
|
||||
severity: { type: 'string', enum: ['error', 'warning', 'info'] },
|
||||
file: { type: 'string' },
|
||||
line: { type: 'integer' },
|
||||
rule: { type: 'string' },
|
||||
message: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
log(`Job service: ${ask}`)
|
||||
|
||||
const map = await agent(
|
||||
`Read the DevPlace async job framework and the two existing consumers ZipService and ForkService end to end (services/jobs/, the enqueue/status/download routes, their *JobOut schemas, Devii tools, and frontend pollers) as the template for a new job kind. Do not write anything.\n\nJob request: ${ask}\n\n${CHECKLIST}`,
|
||||
{ agentType: 'Explore', label: 'understand', phase: 'Understand', schema: MAP_SCHEMA }
|
||||
)
|
||||
|
||||
const plan = await agent(
|
||||
`Produce a per-file plan to add this new job kind, mirroring ZipService/ForkService across the checklist. One step per file. Do not write code.\n\nJob request: ${ask}\n\nTemplate map:\n${JSON.stringify(map, null, 2)}\n\n${CHECKLIST}`,
|
||||
{ agentType: 'Plan', label: 'plan', phase: 'Plan', schema: PLAN_SCHEMA }
|
||||
)
|
||||
|
||||
const build = await agent(
|
||||
`Implement this new async job kind coherently, editing files directly in the repo, mirroring ZipService/ForkService and following the plan. Keep the *JobOut schema, routes, Devii tools, and docs in agreement. Then run "python -m agents.validator ." and "python -c \\"from devplacepy.main import app\\"". Do not write tests here. Do not run the suite. Do not commit.\n\nJob request: ${ask}\n\nPlan:\n${JSON.stringify(plan, null, 2)}\n\n${CHECKLIST}\n\n${RULES}\n\nReturn the job kind, files changed, and whether validator and import passed.`,
|
||||
{ label: 'implement', phase: 'Implement', schema: BUILD_SCHEMA }
|
||||
)
|
||||
|
||||
const changed = (build && build.filesChanged) || []
|
||||
const scopeNote = changed.length ? `\n\nRestrict findings to these files:\n${changed.join('\n')}` : ''
|
||||
|
||||
const audits = await parallel(
|
||||
[
|
||||
{ key: 'fanout', agent: 'fanout-maintainer' },
|
||||
{ key: 'security', agent: 'security-maintainer' },
|
||||
{ key: 'audit', agent: 'audit-maintainer' },
|
||||
{ key: 'docs', agent: 'docs-maintainer' },
|
||||
].map((a) => () =>
|
||||
agent(
|
||||
`Operate in REPORT mode (read-only). Audit the new async job kind for your single dimension.${scopeNote}\n\nJob request: ${ask}`,
|
||||
{ agentType: a.agent, label: `verify:${a.key}`, phase: 'Verify', schema: FINDINGS_SCHEMA }
|
||||
).then((r) => ({ key: a.key, findings: (r && r.findings) || [] }))
|
||||
)
|
||||
)
|
||||
|
||||
const gaps = audits
|
||||
.filter(Boolean)
|
||||
.flatMap((r) => r.findings.map((f) => ({ dimension: r.key, ...f })))
|
||||
.filter((f) => f.severity !== 'info')
|
||||
|
||||
let gapFix = 'no actionable gaps'
|
||||
if (gaps.length) {
|
||||
gapFix = await agent(
|
||||
`Close these job-service gaps with minimal root-cause fixes in the repo, then re-run "python -m agents.validator .". Do not run the suite. Do not commit.\n\nGaps:\n${JSON.stringify(gaps, null, 2)}\n\n${RULES}`,
|
||||
{ label: 'fix-gaps', phase: 'Fix' }
|
||||
)
|
||||
}
|
||||
|
||||
const tests = await agent(
|
||||
`Operate in FIX mode. Write the integration tests for the new job kind (enqueue, status, download) following the required patterns and the directory-mirrors-path layout. Validate by a clean import only. NEVER run the suite.\n\nJob request: ${ask}\nKind: ${build && build.kind}\nFiles changed:\n${changed.join('\n')}`,
|
||||
{ agentType: 'test-maintainer', label: 'tests', phase: 'Fix' }
|
||||
)
|
||||
|
||||
return { ask, map, plan, build, audit: gaps, gapFix, tests }
|
||||
Reference in New Issue
Block a user