Two server-owned timestamps on duly_task. One of them is the most useful number in the product.
Files you own
src/hooks/task.hook.ts (new), added to dulyHooks in src/hooks/index.tstest/task-hook.test.ts (new)
Hooks are read from defineStack({ hooks })only — a *.hook.ts that is not in the barrel type-checks, reads as wired, and never runs. The barrel is already imported by the config; do not touch the config.
Behaviour
completed_at and last_update_at are both readonly: true, meaning a non-system caller's write is stripped. The hook is the one writer.
beforeInsert
beforeUpdate
- transition into
status = 'done' → stamp completed_at = now - transition out of
done → clear completed_at to null last_update_at = nowonly when something meaningful changed: status, note, skip_reason, or an attachment. See below.
The trap
last_update_at is the stagnation signal — the "Not moving" view is status in (open, in_progress) AND last_update_at < {14_days_ago}. If the hook stamps on every update, then any unrelated write — a bulk re-owner, a business-unit backfill, an import — silently resets the stagnation clock on the entire table and the signal goes quiet exactly when it matters.
Stamp on the fields a human touching the task would change. Do not stamp on system-owned or administrative field changes.
The other trap
duly_task has a validation rule completed_at_required_when_done. It is satisfied by the server: this hook stamps before validation runs, so a completion write carrying only { status: 'done' } passes. That rule exists as the assertion that the stamp happened — if this hook is ever unregistered, the write is refused loudly instead of committing a done task with no timestamp. Do not "fix" the rule; make the hook satisfy it.
Acceptance
{ status: 'done' } alone commits, with completed_at set- reopening (
done → in_progress) clears completed_at; the record then passes validation - a caller passing
completed_at explicitly has it stripped and replaced - editing
note advances last_update_at - changing only
business_unit (or any bulk administrative write) does not advance last_update_at — assert this directly, it is the whole point - re-saving with no changes does not advance it either
Gates
pnpm validate && pnpm typecheck && pnpm test && pnpm build.
Two server-owned timestamps on
duly_task. One of them is the most useful number in the product.Files you own
src/hooks/task.hook.ts(new), added todulyHooksinsrc/hooks/index.tstest/task-hook.test.ts(new)Hooks are read from
defineStack({ hooks })only — a*.hook.tsthat is not in the barrel type-checks, reads as wired, and never runs. The barrel is already imported by the config; do not touch the config.Behaviour
completed_atandlast_update_atare bothreadonly: true, meaning a non-system caller's write is stripped. The hook is the one writer.beforeInsert
last_update_at = nowbeforeUpdate
status = 'done'→ stampcompleted_at = nowdone→ clearcompleted_atto nulllast_update_at = nowonly when something meaningful changed:status,note,skip_reason, or an attachment. See below.The trap
last_update_atis the stagnation signal — the "Not moving" view isstatus in (open, in_progress) AND last_update_at < {14_days_ago}. If the hook stamps on every update, then any unrelated write — a bulk re-owner, a business-unit backfill, an import — silently resets the stagnation clock on the entire table and the signal goes quiet exactly when it matters.Stamp on the fields a human touching the task would change. Do not stamp on system-owned or administrative field changes.
The other trap
duly_taskhas a validation rulecompleted_at_required_when_done. It is satisfied by the server: this hook stamps before validation runs, so a completion write carrying only{ status: 'done' }passes. That rule exists as the assertion that the stamp happened — if this hook is ever unregistered, the write is refused loudly instead of committing a done task with no timestamp. Do not "fix" the rule; make the hook satisfy it.Acceptance
{ status: 'done' }alone commits, withcompleted_atsetdone→in_progress) clearscompleted_at; the record then passes validationcompleted_atexplicitly has it stripped and replacednoteadvanceslast_update_atbusiness_unit(or any bulk administrative write) does not advancelast_update_at— assert this directly, it is the whole pointGates
pnpm validate && pnpm typecheck && pnpm test && pnpm build.