S5 — an owner and a start date on the case, and the task board retired - #84
Merged
Conversation
…board (S5)
Option 7a: two fields on the case, plus Copy as ticket. No plan object and
no task object.
`start` stamps `owner` and `startedAt` and nothing else writes either. Both
are written inside `applyAction`, beside the checkpoints `mark_fixed`
schedules, so there is no second door into `in_progress` that could reach the
state without the stamp — and the registry allows `start` only from `todo`,
so neither field can be overwritten by a later move. Neither is editable by
hand: a field that can drift from the history entry beside it is a second
answer to "who has this", which is the defect the case object exists to
remove. `checklist` is gone; `notes` is free text with no schema.
The owner is the identity `attributionOf` gives the caller, not the caller.
F4 split the two on purpose — the class is a permission and this is the
record of who — and `start` is person-only, so the class here is known and
constant; a field typed to admit `{ kind: "system" }` is a field that
eventually holds one. Going through `attributionOf` also means the owner and
the attribution column in history are one answer to one question (rule 20),
and that the two identities F4 declines to render are the two this declines
to store: a system caller, and the migrated person nobody can name. Both
leave the field absent, and the row then says so.
The Fix queue draws itself, like Watch. To do above In progress, in the order
`queue.fix.holds` already states, impact-ordered within each group with
unmeasured findings last (rule 18). In progress rows carry the owner's
initials, name and start date; past thirty days that date renders amber and
that is the whole of it — no escalation, no email, no second threshold. The
queue says so at the top, because a reader who has been told there is no
escalation can leave a case for thirty-one days on purpose.
A legacy record that arrives already in progress never passed through
`start`, so it has no owner. The row says so rather than inventing one.
Copy as ticket is plain markdown on the clipboard and the entire integration
surface. A tracker connection would be a second home for the work's state,
and a second home is where it disagrees with the first. The saving comes from
`formatCaseImpact`, which is the call the case detail makes, so the ticket's
figure is the case's figure to the byte; an unmeasured finding says "Not
measured" and never 0. The link is `casePath` — `/issues/{id}`, no
`/issues/case/` segment to get wrong.
Deleted: the `/tasks` redirect stub (C1a made it redundant), the standalone
`api/agent-audits/verify` endpoint and its client — W1's checkpoint evaluator
replaces it — and the "Add to tasks" / "Add workaround to tasks" affordance
with the dead Tasks-board plumbing behind it.
`EFFORT_LABEL` moved to `impact-format.ts` and `enteredAt` generalised out of
`fixedAtOf`, both because S5 gave them a third reader that is not a component.
matthewpmunger
force-pushed
the
chunk-s5
branch
from
August 26, 2026 17:17
4983327 to
d543534
Compare
This was referenced Aug 26, 2026
Three test files still passed the retired actor option where TransitionOptions now takes by: Caller; each gains the PERSON constant the migrated suites already use. personActionsFor still tested the permission list against a bare word, which the guard in caller.test.ts flags — port main's actorPermits helper verbatim so both it and applyAction read the table the same way, and so the eventual merge is clean.
# Conflicts: # src/app/(app)/pages/pages-content.tsx # src/components/case-detail.tsx # src/lib/__tests__/digest-arrival.test.ts # src/lib/__tests__/webhook.test.ts # src/lib/issue-case.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Option 7a, as locked: two fields on the case plus Copy as ticket. No plan object, no task object.
Depends on F1, F2, S2 and W1. Rebased onto
728eb6cafter F4 landed — see "Adapted to F4" and "Main is red before this branch" below.Two fields, and no hand on either
ownerandstartedAtonIssueCase. Both are written insideapplyAction, beside the checkpointsmark_fixedschedules, rather than in thestartwrapper — which is what makes "no other path writes either" structural instead of a convention.startis a one-line call intoapplyAction, so there is no second door intoin_progressthat reaches the state without the stamp, and the registry allowsstartonly fromtodo, so neither field is overwritten by a later move.Neither is editable by hand. An owner a person can retype is an owner that disagrees with the history entry beside it the first time somebody corrects a spelling, and then the case has two answers to "who has this" — the same defect, one field smaller, that the four merged lifecycles were.
checklistis deleted.notesis free text: no schema, no required fields, nothing derived from it.Adapted to F4
F4 replaced
TransitionOptions.actor: stringwithby: Caller, which is the field the stamp reads. The owner is nowattributionOf(options.by)— the identity, not the caller.That is deliberate rather than mechanical. F4's point is that the class is a permission and the identity is the record of who;
startis person-only in the registry, so the class here is already known and constant, and a field typed to admit{ kind: "system" }is a field that eventually holds one. Going throughattributionOfrather than readingby.userIddirectly also means the owner field and the attribution column in history are one answer to one question (rule 20) — and that the two identities F4 declines to render are exactly the two this declines to store: a system caller, and the migrated person carryingUNKNOWN_USERwhom nobody can name. Both leaveownerabsent, and the row then says there is no owner rather than printing a class or a sentinel as though it were a person.The system arm is unreachable today. It is handled anyway because
attributionOfis what decides, not a belief here about who is permitted — wideningaction.start.actormust not silently start writing"grouping"into an owner field.The Fix queue
FixQueuedraws itself, the wayWatchQueuealready did. Decide and Show all are lists of undecided things, so they keep remediation grouping, the savings fold and the sort control; Fix and Watch hold decided things, and a fold is a triage affordance with no triage left to do.Group order is read off
queue.fix.holds, which already states["todo", "in_progress"]— so To do sits above In progress without a second list to keep in step. Impact-ordered within each group viabyWorstMeasured, unmeasured findings moved as a block rather than sorted by their zero (rule 18).In progress rows carry the owner's initials, name and start date. Past thirty days that date renders amber, and that is the whole of it: text colour only, no wash, no border, no icon, no escalation, no email, no second threshold. The queue states this at the top, because a reader who has been told there is no escalation can leave a case for thirty-one days on purpose, and one who suspects there might be manages the queue instead of the work.
Copy as ticket
Plain markdown on the clipboard, offered on the two states
queueHoldsState("fix", …)returns — and that is the entire integration surface. A tracker connection would be a second home for the work's state, which is where it disagrees with the first; markdown holds no state, so it cannot drift.The saving comes from
formatCaseImpact, which is now also the callcase-detail.tsxmakes. The ticket's figure is the case's figure to the byte because there is one string rather than two that agree. An unmeasured finding says "Not measured" and never 0. The link iscasePath—/issues/{id}, with no/issues/case/segment to spell wrong.Deleted
src/app/(app)/tasks/— C1a already made the redirect redundant.src/app/api/agent-audits/verify/and its client caller, plusrequestAgentAuditVerify. Confirmed present at the base commit withgit cat-file -e f37b156:…; last touched by80193a0. W1's checkpoint evaluator replaces it.triageActionLabel) and the dead Tasks-board plumbing behind it instore.tsx—saveTask,triageRec,advanceTask,verifyAgentIssueTask. The server-sidemutations.advanceTaskand/api/recsstay; they are the legacy store's write path, not affordances, and are still covered byatomic-store.test.ts.Two refactors S5 forced
EFFORT_LABELmoved fromissue-row.tsxtoimpact-format.ts, because Copy as ticket is a third reader that is not a component and a lib should not import a.tsxto learn thathoursis spelled "Hours".enteredAtgeneralised out offixedAtOf, because the fix queue asks the same question abouttodo. Both re-export from their old homes, so existing importers keep one name.Copy
Six new strings in
lib/fix-copy.ts. Four of the ten locked strings are deliberately not restated:To do,In progress,StartandMark fixedare alreadyWORK_STATE_LABELandISSUE_ACTION_LABEL, and a second literal would let a heading disagree with the chip on the row beneath it (rule 20). Same callwatch-copy.tsmade about the four checkpoint outcome labels.fix.no_owner— "No owner recorded" — is new and now locked. A legacy record that reachedin_progressthroughstateFromTaskStatusnever passed throughstart, and a migrated person caller carriesUNKNOWN_USER; neither can be named, and fabricating one would be the rule 18 failure applied to a name instead of a reading.Known, and deliberate
TaskStatus/Rec.taskStatusremains. It is a persisted field thatfromRecreads to derive every case, alongside the collector, seed, watcher, normalize and the markers API — deleting it is a storage migration, not a rename.issue-case.tsdocuments the exit condition: it goes when the last reader is gone, and the last reader isfromRec. No route or component named task remains, and a test enforces that. Tracked as the open item against S5.guide.tsstill says "Tasks". The registry's allowlist assigns it to S9 — "the glossary retires; its entries define the retired terms on purpose."Tests
lib/__tests__/fix-work.test.ts, 24 tests, written to rule 21 — assert the decision, not the code:queue.fix.holds, group headings againstwork_state[].label, the "cannot fire twice" guarantee againstaction.start.from, the dismiss reason againstDISMISS_REASONS, and the caller class for each transition constructed from that transition's own permission set rather than assumed.startedLongAgoturns on; the ticket's saving againstformatCaseImpact's output and a check that both readers reach it through that one function.fix-copy.tsis a mirror against a mirror.startis applied from every legal state and must leave both fields untouched; onlyissue-case.tsassignsowner; thestartedAtscan is narrowed to files importing fromissue-case, since collection jobs and projects have astartedAtof their own and a broad scan would be loud enough to get switched off.Main is red before this branch
728eb6cdoes not build or test green on its own, and this PR inherits that. F4 (#82) branched before S3/S6/S7 (#81) and was merged without a rebase, so it renamedTransitionOptions.actor→by: CallerandHistoryEntry.actor→bywithout seeing the S6/S7 tests that passactor. Git saw no conflict because the files are disjoint.Measured on
728eb6cwith nothing of mine applied:tsc --noEmit: 8 errors —digest-arrival.test.ts×4,digest.test.ts×3,webhook.test.ts×1vitest run: 4 files / 4 tests failing — the three above, plus F4's owncaller.test.ts, whose "no identity string is compared against a permission list" guard flags the pre-existingpersonActionsForinissue-case.tsOn this branch, with S5 applied: the same 4 files fail, the same 8 tsc errors appear, and nothing else. 1194 tests pass against main's 1170 — the +24 are this chunk's.
npm run lintandnpm run buildare clean here.I have not fixed any of it. It is F4's blast radius crossed with S7's, it wants its own commit, and folding it into S5 would hide a real merge-order defect inside an unrelated chunk. Happy to take it as a separate PR.
Verification
npm run lintclean,npm run buildclean,npx --yes npm@10.9.7 ci --dry-run --ignore-scripts --no-audit --no-fundperAGENTS.md— lockfile untouched, nested esbuild records intact. Typecheck and tests as measured above.Also exercised end to end against the real seed rather than only in unit assertions:
startstamps both fields, the amber flips at exactly 30 days, the ticket's impact string matches the case's, an unmeasured finding's ticket reads "Not measured", and the one legacy in-progress case rendersfix.no_owner.Build output confirms
/tasksand/api/agent-audits/verifyare gone from the route table.