Problem / Goal
A Quota Burn step today is a copied prompt plus a free-form params bag: normalizeQuotaBurnJob in server/lib/quotaBurnConfig.js keys on a jobType from the QUOTA_BURN_JOB_TYPE enum and stores prompt text, so a step has no durable link to the scheduled task it was cloned from. #6372 replaces that with a reference to an existing scheduled task plus per-invocation overrides.
This child lands the data model, normalization, and validation only — no new execution path, no UI replacement, no migration.
Scope
- Define a discriminated task reference on a burn step: either a built-in scheduled type (plus a validated target app where the type requires one) or a custom scheduled job ID (whose own app scope is authoritative). Extend
normalizeQuotaBurnJob / normalizeQuotaBurnFamily / normalizeQuotaBurnConfig in server/lib/quotaBurnConfig.js and the Zod schemas in server/lib/quotaBurnValidation.js (quotaBurnJobSchema / quotaBurnFamilySchema / quotaBurnConfigUpdateSchema, all .strict(), re-exported through server/lib/validation.js and used by PUT /api/quota-burn in server/routes/quotaBurn.js).
- Preserve the step's identity and settings: stable step
id, family order, enabled, label, runOnce state, and family-level settings all survive normalization untouched.
- Per-invocation overrides: provider / model / effort and the supported run parameters, stored explicitly on the step. Unset values inherit the source task's saved settings. Overrides never write back to the schedule. A provider pin must belong to the selected burn family; an absent pin resolves inside that family and must never fall back to another family's subscription.
- Unavailable steps: a deleted, disabled, wrong-scope, or incompatible reference normalizes to a retained-but-unavailable step carrying an actionable reason. Its settings are never dropped, and it is not dispatchable.
- Legacy compatibility: old
jobType payloads still load. Freeze QUOTA_BURN_JOB_TYPE, QUOTA_BURN_JOB_CATALOG, and server/lib/quotaBurnPresets.js as compatibility/migration inputs only — readable, never the canonical definition of new work. A legacy payload that has not yet been converted normalizes to unavailable with a migration reason rather than silently downgrading a reference or duplicating an automation.
- Preset identity is currently never persisted. Both the server (
upgradeStoredAuditPrompt, server/lib/quotaBurnPresets.js) and the client (matchedPreset in client/src/components/quotaBurn/JobRow.jsx) re-derive it by comparing prompt text, which is exactly the fragility this reference model exists to remove. A reference must record identity durably, not re-derive it.
- Config lives in
data/cos/quota-burn.json via server/services/quotaBurnStore.js and stays machine-local and unfederated (see the store's header comment and docs/STORAGE.md).
Acceptance criteria
Notes
Foundation for the invocation-path, accounting, migration, and editor children. Ship this first among the burn-side children.
Part of #6372
When this ships, tick its box in the ## Decomposed into checklist on #6372. If it is the last unchecked box, close #6372.
Problem / Goal
A Quota Burn step today is a copied prompt plus a free-form
paramsbag:normalizeQuotaBurnJobinserver/lib/quotaBurnConfig.jskeys on ajobTypefrom theQUOTA_BURN_JOB_TYPEenum and stores prompt text, so a step has no durable link to the scheduled task it was cloned from. #6372 replaces that with a reference to an existing scheduled task plus per-invocation overrides.This child lands the data model, normalization, and validation only — no new execution path, no UI replacement, no migration.
Scope
normalizeQuotaBurnJob/normalizeQuotaBurnFamily/normalizeQuotaBurnConfiginserver/lib/quotaBurnConfig.jsand the Zod schemas inserver/lib/quotaBurnValidation.js(quotaBurnJobSchema/quotaBurnFamilySchema/quotaBurnConfigUpdateSchema, all.strict(), re-exported throughserver/lib/validation.jsand used byPUT /api/quota-burninserver/routes/quotaBurn.js).id, family order,enabled,label,runOncestate, and family-level settings all survive normalization untouched.jobTypepayloads still load. FreezeQUOTA_BURN_JOB_TYPE,QUOTA_BURN_JOB_CATALOG, andserver/lib/quotaBurnPresets.jsas compatibility/migration inputs only — readable, never the canonical definition of new work. A legacy payload that has not yet been converted normalizes to unavailable with a migration reason rather than silently downgrading a reference or duplicating an automation.upgradeStoredAuditPrompt,server/lib/quotaBurnPresets.js) and the client (matchedPresetinclient/src/components/quotaBurn/JobRow.jsx) re-derive it by comparing prompt text, which is exactly the fragility this reference model exists to remove. A reference must record identity durably, not re-derive it.data/cos/quota-burn.jsonviaserver/services/quotaBurnStore.jsand stays machine-local and unfederated (see the store's header comment anddocs/STORAGE.md).Acceptance criteria
jobTypepayload normalizes to unavailable-with-migration-reason; it is never rewritten into a bogus reference and never duplicated.saveQuotaBurnConfig's shallow family merge and the client mirrorclient/src/lib/quotaBurnPatch.js#mergeQuotaBurnPatchstay line-for-line consistent (the store comment asserts this) — extend both or neither.Notes
Foundation for the invocation-path, accounting, migration, and editor children. Ship this first among the burn-side children.
Part of #6372
When this ships, tick its box in the
## Decomposed intochecklist on #6372. If it is the last unchecked box, close #6372.