// retoor 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 and write the integration tests (enqueue, status, download) in the api tier', 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 from the review' }, { title: 'Test', detail: 'write the api-tier integration tests for enqueue, status, and download' }, ], } 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 " 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 "hawk ." 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') const TESTS = [ 'DevPlace test standard (a hard project requirement - one test file per endpoint, the directory tree mirroring the URL path):', 'A job kind is exercised over HTTP, so its tests live in tests/api/ against the live uvicorn subprocess (app_server/seeded_db, requests/httpx vs BASE_URL), one file per route path (POST /projects/{slug}/zip -> tests/api/projects/zip.py; GET /zips/{uid} -> tests/api/zips/index.py). Cover enqueue (authz + a job uid back), status (the *JobOut shape and lifecycle), and download/result (the capability URL) where applicable.', 'Because the service loop only runs in the lock owner and tests set DEVPLACE_DISABLE_SERVICES=1, assert the enqueue contract and the pending/known status shape rather than waiting on real completion; if you need a finished job, drive process() directly in a unit test under tests/unit/services/jobs/.', 'Required patterns: scoped assertions; try/finally restore of any flipped global setting; the shared fixtures; raw inserts into a soft-delete table set deleted_at/deleted_by. Validate by a clean import only. NEVER run the suite.', ].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 .') 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 "hawk ." 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 "hawk .". 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. The job kind is not complete until each of its routes has a test. Create any missing package directories the test paths need. Validate by a clean import only. NEVER run the suite.\n\n${TESTS}\n\nJob request: ${ask}\nKind: ${build && build.kind}\nFiles changed:\n${changed.join('\n')}\n\nReturn the test files written and the routes they cover.`, { agentType: 'test-maintainer', label: 'tests', phase: 'Test' } ) return { ask, map, plan, build, audit: gaps, gapFix, tests }