Vertical-slice plan with five slices across four phases, each with acceptance criteria, manual verification, and human-review checkpoints between phases. Surfaces and orders the inconsistencies between the current code and the per-task loop language. Flat todo.md mirrors the plan as a checkable task list.
17 KiB
Plan — Align code with the Per-Task Feedback Loop
This plan implements the decisions recorded in docs/adr/0001-per-task-feedback-loop.md and the language in CONTEXT.md. It closes the inconsistencies surfaced during the deep-design session.
Each slice is vertical: it touches model → store → UI → tests for one user-visible behaviour, rather than refactoring a layer at a time. Phases gate on checkpoints.
Goals (recap)
- The Failure Signal compares actual durations against the Initial Plan, defined as the Plan version active at the moment Execution begins.
- Re-planning (mid-Execution structural Plan changes) preserves every Step Record whose Step still exists by id.
- The loop closes in a dedicated Closing Ceremony that names three Failure modes: Estimation Variance, Discovered Scope, Abandoned Scope.
- Re-estimation (changing a number) and Re-planning (changing the shape) are distinct affordances.
Dependency graph
Phase 1 — Foundation
├─ Slice 1 (Initial Plan capture) ──┐
└─ Slice 2 (Preserve records on re-plan)─┴─▶ Phase 2
Phase 2 — Closing the loop
└─ Slice 3 (Closing Ceremony view) ──▶ Phase 3
Phase 3 — UX split
└─ Slice 4 (Re-estimation inline / Re-planning modal) ──▶ Phase 4
Phase 4 — Cleanup
└─ Slice 5 (Model & naming hygiene)
Slice 4 is technically parallelisable with Slice 3 once Slice 2 lands, but is sequenced after Slice 3 to keep the Closing Ceremony available as the source of truth while reshaping the during-Execution UI.
Phase 1 — Foundation
Slice 1 — Anchor the Initial Plan at Start-of-Execution
Goal. The Recordable stores the Plan that was active when its first Step Record began. The Failure Signal reads from this snapshot, not from the current Plan.
Files in scope.
src/modules/record/interfaces/recordable.ts— addinitialPlan: Stepable[] | null.src/modules/record/models/task-record.ts— initialize and round-tripinitialPlaninfromRecordable.src/modules/record/stores/useTaskRecordStore.ts:50-85— instartStepRecord, whenstepRecordsis empty, snapshottask.stepsintorecord.initialPlan(only if it isn't already set).src/modules/record/components/StepRecord.vue:66-69— replacestep.value.estimationwith the Initial Plan's estimation for the same Step id; fall back tostep.value.estimationonly when no match.src/modules/record/components/RecordResume.vue:14-20— total comparison uses sum of Initial Plan estimations, nottask.totalEstimation(which reflects current Plan).src/modules/record/services/compare-with-estimation.ts— keep as is (still pure); update its callers.- Tests: extend
task-record.test.ts,compare-with-estimation.test.ts, and add new unit tests for the snapshot behaviour.
Acceptance criteria.
- Pressing Start (first Step Record) captures
task.stepsintorecord.initialPlan. Verified in a unit test against the store. - Re-estimating a Step during Execution does not mutate
record.initialPlan. - The "off by 10%" indicator on a Step compares against the Initial Plan's estimation for that Step id.
- Existing persisted records (without
initialPlan) gracefully fall back totask.stepHistory[0]. This fallback path is covered by a unit test. pnpm testpasses (lint + types + unit).
Manual verification.
- Create a Task with Steps at 5 / 10 / 10 min.
- Start recording. Re-estimate the first Step to 30 min via the existing edit modal.
- The first row's "off" indicator continues to reference 5 min (not 30) once the Step has elapsed more than ~30 seconds.
Risks / migration.
- Persisted localStorage data lacks
initialPlan. Fallback covers display; new data will populate it. Document the fallback in a code comment. RecordResumetotal now diverges fromtask.totalEstimationif Re-planning occurred. Acceptable — see Slice 3 for full surface.
Slice 2 — Preserve Step Records when the Plan changes mid-Execution
Goal. Re-planning during Execution never destroys recorded time. Step Records keyed by id survive Plan edits; Steps removed from the Plan keep their orphan records for the Closing Ceremony to inspect.
Files in scope.
src/modules/record/stores/useTaskRecordStore.ts:193-225— replacecleanCurrentStepIdwith a non-destructive reconciliation:- Find the first Step in the new Plan that has no
endin the existing records. That becomes the newcurrentStepId. - If that Step has no existing record yet, start a new one from the latest end-of-completed-step time (or
Date.now()if none). - Do not delete any existing Step Record. Orphan records (no longer in the Plan) remain in
stepRecords.
- Find the first Step in the new Plan that has no
src/modules/record/stores/useTaskRecordStore.ts:20-39—syncTaskRecordcurrently wipes the whole record when any recorded Step id is missing from the Plan. Replace with the same non-destructive reconciliation.- Tests: add scenarios in
useTaskRecordStore.test.ts(new file if missing) for reorder, delete-in-progress, delete-completed, insert-at-start.
Acceptance criteria.
- Reorder during recording — start times of all completed Steps preserved; in-progress Step's start preserved.
- Delete a completed Step during recording — its record is preserved as an orphan in
stepRecords(still keyed by id, not in currenttask.steps). - Delete the in-progress Step — its record preserved; the new
currentStepIdis the next un-finished Step in the new Plan. - Insert a brand-new Step at the start — no existing records mutated; the new Step has no record yet.
pnpm testpasses.
Manual verification.
- During recording, open the edit modal, reorder steps. After saving, recorded times are unchanged in the table.
- Open browser devtools → Application → Local Storage. Confirm orphan records remain after deleting a Step from the Plan.
Risks / migration.
- Orphan records change
stepRecordsfrom "subset of current Steps" to "history of all Steps that ever had a record." Any consumer that iteratesstepRecordsand assumes membership intask.stepsmust be audited:RecordResume.vue→ usesuseTaskRecordMetadata(total duration). Acceptable since orphan time was spent.StepRecord.vueis rendered pertask.steps[i], so it skips orphans naturally.useTaskRecordMetadata— audit during this slice.
CHECKPOINT 1 — after Phase 1
Stop. Verify:
pnpm testgreen.- Manual smoke: full Task lifecycle (create → record → finish) with Re-estimation and Re-planning mid-flight; localStorage shows preserved
initialPlanand preserved Step Records. - No regression in existing per-Step "off" coloring for un-edited Tasks.
Reviewer (human): confirm Foundation matches CONTEXT.md semantics before proceeding to Phase 2.
Phase 2 — Closing the loop
Slice 3 — Closing Ceremony view (three Failure modes)
Goal. A dedicated end-of-Task view that surfaces the three Failure modes, hosts the Notes textarea for post-Execution reflection, and replaces the current adjacent-version diff view at /task/:id/history.
Files in scope.
- New:
src/modules/record/services/compute-failure-modes.ts— pure function:with unit tests covering each mode in isolation and combined.computeFailureModes({ initialPlan, finalPlan, stepRecords }) → { estimationVariance: Array<{ stepId, title, estimation, actual, deltaMin, isOff }>, discoveredScope: Array<{ stepId, title, estimation, actual? }>, abandonedScope: Array<{ stepId, title, estimation }>, totalActualMin, totalInitialMin, totalDeltaMin } - Rename
src/views/task/TaskHistory.vue→TaskClosingCeremony.vue. Body fully replaced. src/router/index.ts— repoint thehistory-taskroute at the new component. Keep the path/task/:id/historyfor backwards-compatibility of bookmarks (rename the route name totask-closing-ceremonyto match vocabulary).src/modules/record/components/TaskRecord.vue— whenrecord.endis set, surface a "Close the loop" CTA linking to the Closing Ceremony route.src/modules/record/components/RecordResume.vue— keep the one-line summary as a tease, but defer the rich breakdown to the new view.- Tests: unit tests on
compute-failure-modes, component test smoke onTaskClosingCeremony.
Open design questions (resolve during this slice).
- How are orphan Step Records (Steps removed from the Plan but with executed time) surfaced? Options: (a) hidden, (b) as a fourth "Discarded Work" section, (c) folded into Abandoned Scope. Default in the plan: hidden, with the time still contributing to the totals. Revisit if user feedback contradicts.
- Does completing a Task auto-navigate to the Closing Ceremony, or is it CTA-only? Default: CTA-only — auto-navigation disrupts the during-Execution view if the user is mid-thought. Reflection should be a deliberate transition.
Acceptance criteria.
computeFailureModesreturns the three buckets correctly given fixture inputs covering: no re-planning (only Estimation Variance), Step added mid-flight (Discovered Scope present), Step deleted from Initial Plan (Abandoned Scope present), all three at once.- The new view renders all three buckets, hiding empty ones.
- The Notes textarea on the new view binds to the same Pinia field as during-Execution Notes (single source — no duplication).
- The route
/task/:id/historyno longer renders an adjacent-version diff. pnpm testpasses.
Manual verification.
- Complete a Task with no Plan changes → only Estimation Variance shown.
- Complete a Task where you added a Step mid-flight → Discovered Scope section appears with that Step.
- Complete a Task where you deleted a planned Step before executing it → Abandoned Scope section appears.
- Notes typed during Execution are visible on the Closing Ceremony view, and edits there persist back.
Risks / migration.
- Removing the diff view eliminates a feature, however lightly-used. Acceptable per the deep-design session (declared "dead view").
- Bookmark stability: route path preserved.
CHECKPOINT 2 — after Phase 2
Stop. Verify:
pnpm testgreen.- End-to-end loop demoable: create → start → execute → re-plan mid-flight → finish → open Closing Ceremony → three modes correct.
Reviewer (human): confirm the Closing Ceremony surface matches intent before reshaping the during-Execution UI.
Phase 3 — UX split
Slice 4 — Re-estimation inline; Re-planning deliberate
Goal. Re-estimating a Step's number does not require opening the structural-edit modal. Re-planning becomes the modal's explicit purpose with framing that matches its weight.
Files in scope.
src/modules/record/components/StepRecord.vue— extend the existing inline-edit pattern (already present fordurationon completed Steps, see lines around theisEditingref) to coverestimationfor any Step. Pressing Enter commits; Esc cancels.- New store action in
useTask.store.ts:reEstimateStep({ taskId, stepId, newEstimationMinutes }). Implementation: push a new entry tostepHistorycontaining the current Plan with only that Step'sestimationupdated. The new entry does not disturbrecord.initialPlan(which is already snapshotted per Slice 1). src/modules/task/components/NewStepsForm.vue— rename heading from "Edit steps" to "Re-plan steps" (or similar weighty phrasing). Update the explanatory text to reflect that this is a structural change. Keep the existing flow for actually adding/removing/reordering.src/modules/record/components/RecordControls.vue:170-176— the "+" button: relabel/icon-update to "Re-plan" (not "+"); the action is unchanged.- Tests: store test for
reEstimateStep; component test forStepRecordinline editor.
Acceptance criteria.
- Tapping/clicking the estimation cell on a Step row opens an inline editor.
- Committing the edit triggers
reEstimateStep, which pushes a newstepHistoryentry. record.initialPlanis not mutated by re-estimation (covered by a unit test).- The structural-edit modal is now reached only via the explicit "Re-plan" button.
pnpm testpasses.
Manual verification.
- During recording, click a Step's estimation cell, type a new number, hit Enter. The number updates; no modal opens.
- The Failure Signal (per-Step "off" indicator) continues to reference the Initial Plan estimation.
- Clicking "Re-plan" opens the modal; the heading reflects the new framing.
Risks / migration.
- The inline editor needs to coexist with the existing
durationinline editor on completed Steps. Confirm visual layout supports both without ambiguity. - Step-Back Signal (variance vs latest re-estimate) is not yet rendered. Surface as a follow-up unless trivial here. Default: render as a secondary, subtler indicator next to the existing "off" color — TBD during implementation.
CHECKPOINT 3 — after Phase 3
Stop. Verify:
pnpm testgreen.- Re-estimation is a one-click, one-Enter interaction with no modal.
- Re-planning still opens the modal with deliberate framing.
Reviewer (human): confirm UX feels right before final cleanup.
Phase 4 — Cleanup
Slice 5 — Model and naming hygiene
Goal. Remove the inconsistencies in the Task model and surrounding code that no longer carry their weight after Phases 1-3.
Files in scope.
src/modules/task/models/task.ts:- Remove
updateSteps(line 53-56) — identical tonewSteps. Update test on line 73 (task.test.ts:73) to callnewSteps(or the renamed action — see below). - Rename
newSteps→rePlanto match the vocabulary established inCONTEXT.md. - Keep
editSteps(...steps)(append semantics) if any consumer needs it; if not, remove. Verify with grep across the codebase before deletion. - Fix the
readonly stepHistorydeclaration — either dropreadonlyfrom the constructor parameter (since the array is mutated via.push()), or migrate to immutable updates that produce a new array. Recommended: dropreadonly; document the mutation pattern in CONTEXT.md if non-obvious. - Re-examine
wasUpdatedgetter: name is misleading (it'sstepHistory.length > 0, i.e., "has any steps at all"). Rename tohasStepsor remove if unused.
- Remove
src/modules/record/stores/useTaskRecordStore.ts:- Rename
cleanCurrentStepId→reconcileWithReplannedPlan(or similar) to match its new non-destructive contract from Slice 2.
- Rename
src/modules/record/models/task-record.ts:TaskRecord.start = toISODate(new Date())at construction is misleading — the real start is set instartStepRecordwhen the first Step Record begins. Initialize asnulland tighten the type:start: ISODate | null. Update consumers.
CONTEXT.md: resolveTask.linkandTask.datesemantics. Likely additions:- Task Date: the moment the Task was created. Used for ordering the Task list. Not editable.
- Task Link: optional external reference (e.g., ticket URL).
- Tests: update all references to renamed methods; add tests for
TaskRecord.start === nulluntil first Step Record.
Acceptance criteria.
Taskmodel has no duplicate-bodied methods.- All renames propagated;
pnpm test(lint + types + unit) green. readonlylies removed.TaskRecord.startisnulluntil the first Step Record begins.CONTEXT.mdincludes Task Date and Task Link definitions.
Manual verification.
- Smoke the full Task lifecycle one more time to ensure no regressions.
Risks / migration.
- Renames touch many files. Use rename-symbol IDE refactors where possible; verify with
pnpm test:types.
CHECKPOINT 4 — after Phase 4
Stop. Verify:
pnpm testgreen.pnpm buildsucceeds.- Manual: full lifecycle smoke once more on a fresh localStorage (incognito tab) to confirm no migration footgun.
Explicitly out of scope (deferred)
These were surfaced in the deep-design session but are not addressed by this plan. They can be picked up in a follow-up plan if value emerges.
breakTimesingle-pause model. Multiple pauses are silently absorbed; the fact of pause is unrecoverable. Acceptable for now; the Closing Ceremony does not surface pauses.- Step-Back Signal rendering. Slice 4 mentions surfacing variance vs latest re-estimate as a softer indicator; default behaviour is to render only the Failure Signal until product feedback contradicts.
- Cross-Task patterns (e.g., "you underestimate refactors by 40%"). The per-task loop is the focus; cross-task analytics is a separate product surface.
Test command reference
pnpm test— full (lint + types + unit). Use at every checkpoint.pnpm test:unit— fast iteration during a slice.pnpm test:types— type-check only, useful after renames.pnpm dev— manual UI verification.pnpm vitest run path/to/file.spec.ts— single test file.