Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
## Context

The user asked for stale-pending-task detection with a configurable
threshold, explicitly leaving the default value to this agent's
judgment. `2026-08-26-add-change-timeline-data-layer` already computes
a `finalLineNumber -> ISO date` blame map per `tasks.md`; the only gap
is that `getChangeTimeline` discards it for still-pending tasks.

## Goals / Non-Goals

**Goals:**
- Reuse the existing blame computation entirely — no new git calls.
- A configurable threshold, not a hardcoded one, in both delivery
targets.
- `isTaskStale`/`findStaleTasks` are pure and host-neutral, so both
webui and the extension use the identical logic rather than each
reimplementing "how old is too old."

**Non-Goals:**
- Not adding staleness to the multi-change comparison view — it only
plots `createdDate`/done-task `date`/`archivedDate` today, never
pending tasks at all, so there is no existing point to flag. Adding
pending-task points there is a bigger, separate UI change.
- Not a proactive notification/popup for stale tasks. Consistent with
this project's established local-first, no-noise stance (see the
archive-time Changesets reminder's own design.md for the same
reasoning) — the signal is visible in the timeline view when the user
looks, not pushed at them.
- Not adding a tree-view badge in the Changes/Archive views — that
would need a background scan (recomputing blame) independent of the
timeline view's already-paid-for fetch, a real performance question
deferred rather than answered here.

## Decisions

### 14-day default threshold

No existing convention in this codebase to anchor to, so: two weeks is
long enough that normal day-to-day pending work (a task genuinely
being actively worked on, just not finished yet) is not flagged, and
short enough that a truly forgotten task is still caught while the
context to act on it is still fresh. Configurable in both hosts, so
this default is not load-bearing.

### `lastTouchedDate` is a new field, not a repurposed `date`

`ChangeTimelineTask.date` was deliberately restricted to checked tasks
only in the prior change, specifically because a blame date for an
unchecked line reads as a misleading "completion" date. Reusing it here
for staleness would undo that fix. A second field keeps both meanings
intact: `date` = "completed on," `lastTouchedDate` = "line last edited
on," used for different purposes.

### `change-timeline-client.ts` now imports real types instead of a hand-duplicated copy

Adding `lastTouchedDate` to core's `ChangeTimelineTask` required also
adding it to webui's hand-duplicated copy in `change-timeline-client.ts`
— a maintenance burden that already caused exactly this kind of drift
once. Since `browser.ts` already exists as the browser-safe export
surface, and adding type-only exports there costs nothing at runtime,
this change re-points `change-timeline-client.ts` at the real
`@openspec-ui/core/browser` types instead, closing the drift risk for
good rather than patching this one instance of it.

## Verification note

Following the lesson from `2026-08-26-fix-timeline-webview-csp-inline-script`
(a prior smoke test that loaded the bundle in an unrestricted page
missed a real CSP bug), this change's manual verification constructed
the HTML exactly as `TimelineWebviewPanel.getHtml()` does — same CSP,
same nonce'd inline-script shape, both `__OPENSPEC_UI_TIMELINE__` and
`__OPENSPEC_UI_STALE_THRESHOLD_DAYS__` assignments — before trusting
that the real webview would render stale/fresh tasks correctly.
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
## Why

Following up on the 2026-08-26 product-direction discussion: a pending
task that has sat untouched for a long time in an active change is a
useful signal that it may have been forgotten. The data needed for this
already exists — `blameLineDates` (from
`2026-08-26-add-change-timeline-data-layer`) already computes a
per-line last-touched date via git blame; it just wasn't exposed for
still-pending tasks (`ChangeTimelineTask.date` is deliberately `null`
for those, since a "last touched" date would misleadingly read as a
completion date — see that change's design.md).

## What Changes

- Add `ChangeTimelineTask.lastTouchedDate`: always populated from blame
(done or not), distinct from `date` (completion-only). No new git
calls — reuses the blame map `getChangeTimeline` already computes.
- Add `packages/core/src/stale-tasks.ts`: `isTaskStale`/`findStaleTasks`,
pure date-math functions (no git/fs access) — a pending task is stale
when `lastTouchedDate` is older than a threshold (default 14 days,
`DEFAULT_STALE_TASK_THRESHOLD_DAYS`). Exported from both the Node-only
barrel and the browser-safe barrel, since it has no Node dependency
and both delivery targets need it.
- `packages/webui/src/change-timeline-client.ts` now re-exports the
`ChangeTimeline*` interfaces from `@openspec-ui/core/browser` instead
of hand-duplicating them — the hand-duplicated copy had already
drifted out of sync once (missing this exact new field) before this
module started importing the real ones.
- `ChangeTimelineView.tsx` gains a `staleThresholdDays` prop (default
14) and flags stale pending tasks distinctly (a warning marker/color,
not just plain "pending").
- Standalone app: a "Stale after (days)" number input next to the
single-change Timeline picker.
- VS Code extension: a new setting,
`openspec-ui.staleTaskThresholdDays` (default 14), read when
`openspec-ui.showChangeTimeline` fetches the timeline and passed to
the webview alongside the `ChangeTimeline` payload (its own global,
`window.__OPENSPEC_UI_STALE_THRESHOLD_DAYS__`, embedded the same
nonce'd way as the timeline data itself — see
`2026-08-26-fix-timeline-webview-csp-inline-script`).
- Not changed: the multi-change comparison view — it does not currently
plot pending tasks at all, so staleness has nothing to attach to
there yet. Out of scope for this change.

## Capabilities

### Modified Capabilities

- `execution-core`: adds a Requirement for stale-pending-task detection.
- `vscode-extension`: the Change Timeline webview now surfaces stale
tasks, configurable via a new setting.
- `standalone-app`: the Timeline tab gains a configurable staleness
threshold.

## Impact

- `packages/core/src/change-timeline.ts`, `browser.ts`, `index.ts`
- `packages/core/src/stale-tasks.ts` (new)
- `packages/webui/src/change-timeline-client.ts`
- `packages/webui/src/components/ChangeTimelineView.tsx`
- `packages/webui/src/standalone-entry.tsx`, `timeline-entry.tsx`,
`shell-ui.ts`
- `packages/extension/src/webview/timeline-panel.ts`
- `packages/extension/src/commands.ts`, `package.json`
- `.changeset/*.md` (new changeset file)
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## ADDED Requirements

### Requirement: Stale-pending-task detection

The system SHALL determine, for a still-pending task, whether it has
sat untouched (per git blame on `tasks.md`) longer than a configurable
threshold, defaulting to 14 days. A task with an undeterminable
last-touched date SHALL never be flagged, and a completed task SHALL
never be flagged regardless of age.

#### Scenario: A pending task untouched past the threshold

- **WHEN** a still-pending task's last-touched date is older than the
configured threshold
- **THEN** the system reports it as stale

#### Scenario: A pending task touched recently

- **WHEN** a still-pending task's last-touched date is within the
configured threshold
- **THEN** the system does not report it as stale

#### Scenario: A completed task, regardless of age

- **WHEN** a task is checked off, however old its last-touched date
- **THEN** the system never reports it as stale

#### Scenario: An undeterminable last-touched date

- **WHEN** a pending task's last-touched date cannot be determined
(e.g. blame unavailable)
- **THEN** the system does not report it as stale
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## ADDED Requirements

### Requirement: The Timeline tab's staleness threshold is user-configurable

The system SHALL let the user set the stale-pending-task threshold (in
days) in the standalone Timeline tab, defaulting to 14 days, and apply
it when rendering a change's timeline.

#### Scenario: User changes the threshold

- **WHEN** the user sets a different stale-after value and loads (or
reloads) a change's timeline
- **THEN** pending tasks are flagged stale according to the new value
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## ADDED Requirements

### Requirement: The Change Timeline webview flags stale pending tasks

The system SHALL flag, in the Change Timeline webview, any pending task
that stale-task detection identifies as stale, using a threshold
configurable via the `openspec-ui.staleTaskThresholdDays` setting
(default 14).

#### Scenario: A change has a stale pending task

- **WHEN** the user opens the Change Timeline for a change containing a
pending task untouched past the configured threshold
- **THEN** that task is visually distinguished from a fresh pending
task in the webview

#### Scenario: User changes the threshold setting

- **WHEN** the user sets `openspec-ui.staleTaskThresholdDays` to a
different value and reopens the Change Timeline
- **THEN** the new threshold is used to determine staleness
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
## 1. Core: data layer and staleness logic

- [x] 1.1 Add `ChangeTimelineTask.lastTouchedDate` to
`change-timeline.ts`, always populated from the existing blame map
(done or not) — distinct from `date`, which stays completion-only.
- [x] 1.2 Update `change-timeline.test.ts` for the new field.
- [x] 1.3 Add `packages/core/src/stale-tasks.ts`
(`DEFAULT_STALE_TASK_THRESHOLD_DAYS`, `isTaskStale`, `findStaleTasks`)
and `stale-tasks.test.ts`. Pure date math, no git/fs.
- [x] 1.4 Export from `index.ts`; export the pure logic plus the
`ChangeTimeline*` types (type-only) from `browser.ts`.

## 2. Webui: shared component and standalone UI

- [x] 2.1 Re-point `change-timeline-client.ts` at
`@openspec-ui/core/browser`'s real types instead of a hand-duplicated
copy (which had already drifted once).
- [x] 2.2 Add `staleThresholdDays`/`now` props to `ChangeTimelineView`;
flag stale pending tasks distinctly (marker + message), matching CSS
in `shell-ui.ts`.
- [x] 2.3 Add tests for stale/fresh rendering with a deterministic
`now`.
- [x] 2.4 Add a "Stale after (days)" number input to the standalone
Timeline tab's single-change mode, defaulting to
`DEFAULT_STALE_TASK_THRESHOLD_DAYS`.

## 3. Extension: setting and webview wiring

- [x] 3.1 Add `openspec-ui.staleTaskThresholdDays` to
`contributes.configuration` in `package.json` (default 14).
- [x] 3.2 `openspec-ui.showChangeTimeline` reads the setting and passes
it to `TimelineWebviewPanel.show(...)`.
- [x] 3.3 `TimelineWebviewPanel` embeds it as its own global
(`window.__OPENSPEC_UI_STALE_THRESHOLD_DAYS__`), same nonce'd
mechanism as the timeline data; `timeline-entry.tsx` reads it and
passes it to `ChangeTimelineView`.
- [x] 3.4 Update `timeline-panel.test.ts` and `commands.test.ts` for
the new parameter/setting (added a dedicated test asserting the
setting is actually read, not just defaulted).

## 4. Verification

- [x] 4.1 `npm run typecheck` and `npm run lint` (including
`lint:english`) pass workspace-wide.
- [x] 4.2 `npm run test` passes workspace-wide, including all updated/
new test files.
- [x] 4.3 Rebuild the VSIX (`npm run package --workspace
openspec-ui-vscode`) and confirm it packages without error.
- [x] 4.4 Manual verification against the *real* CSP shape (not a bare
unrestricted page — see design.md's Verification note): constructed
HTML matching `TimelineWebviewPanel.getHtml()` exactly, with a
synthetic genuinely-stale task and a fresh pending task; confirmed
zero console errors and correct stale/fresh visual distinction via a
real Chromium screenshot.
- [x] 4.5 Propose a changeset (`npx changeset`) for `@openspec-ui/core`,
`@openspec-ui/webui`, and `openspec-ui-vscode` (all minor: new
capability, no breaking change) instead of hand-editing `version`/
`CHANGELOG.md`; apply it via `npx changeset version`.
- [x] 4.6 Run `openspec change validate --strict add-stale-task-detection`.
31 changes: 31 additions & 0 deletions openspec/specs/execution-core/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,34 @@ than causing the read to fail.
the change's data (proposal/design/tasks/spec content, other
determinable dates) is still returned

### Requirement: Stale-pending-task detection

The system SHALL determine, for a still-pending task, whether it has
sat untouched (per git blame on `tasks.md`) longer than a configurable
threshold, defaulting to 14 days. A task with an undeterminable
last-touched date SHALL never be flagged, and a completed task SHALL
never be flagged regardless of age.

#### Scenario: A pending task untouched past the threshold

- **WHEN** a still-pending task's last-touched date is older than the
configured threshold
- **THEN** the system reports it as stale

#### Scenario: A pending task touched recently

- **WHEN** a still-pending task's last-touched date is within the
configured threshold
- **THEN** the system does not report it as stale

#### Scenario: A completed task, regardless of age

- **WHEN** a task is checked off, however old its last-touched date
- **THEN** the system never reports it as stale

#### Scenario: An undeterminable last-touched date

- **WHEN** a pending task's last-touched date cannot be determined
(e.g. blame unavailable)
- **THEN** the system does not report it as stale

12 changes: 12 additions & 0 deletions openspec/specs/standalone-app/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,15 @@ log-scaled time axis.
- **THEN** the system reports what is missing rather than loading an
empty or partial comparison

### Requirement: The Timeline tab's staleness threshold is user-configurable

The system SHALL let the user set the stale-pending-task threshold (in
days) in the standalone Timeline tab, defaulting to 14 days, and apply
it when rendering a change's timeline.

#### Scenario: User changes the threshold

- **WHEN** the user sets a different stale-after value and loads (or
reloads) a change's timeline
- **THEN** pending tasks are flagged stale according to the new value

20 changes: 20 additions & 0 deletions openspec/specs/vscode-extension/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,3 +369,23 @@ detail rather than compressing it further.
- **THEN** the extension shows an error message and does not open a
webview

### Requirement: The Change Timeline webview flags stale pending tasks

The system SHALL flag, in the Change Timeline webview, any pending task
that stale-task detection identifies as stale, using a threshold
configurable via the `openspec-ui.staleTaskThresholdDays` setting
(default 14).

#### Scenario: A change has a stale pending task

- **WHEN** the user opens the Change Timeline for a change containing a
pending task untouched past the configured threshold
- **THEN** that task is visually distinguished from a fresh pending
task in the webview

#### Scenario: User changes the threshold setting

- **WHEN** the user sets `openspec-ui.staleTaskThresholdDays` to a
different value and reopens the Change Timeline
- **THEN** the new threshold is used to determine staleness

10 changes: 10 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# @openspec-ui/core

## 0.28.0

### Minor Changes

- Add stale-pending-task detection: a pending task untouched (per git
blame) longer than a configurable threshold (default 14 days) is now
flagged in the Change Timeline view. Configurable via a number input in
the standalone Timeline tab and the new `openspec-ui.staleTaskThresholdDays`
VS Code setting.

## 0.27.0

### Minor Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@openspec-ui/core",
"private": true,
"version": "0.27.0",
"version": "0.28.0",
"type": "module",
"main": "src/index.ts",
"exports": {
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,8 @@ export type {
TemplateManifest,
TemplateVariable,
} from "./template-catalog.js";
export type { ChangeTimeline, ChangeTimelineSpec, ChangeTimelineTask } from "./change-timeline.js";
// Pure date math, no git/fs access — safe for the browser bundle (see the
// file header comment for why this differs from change-timeline.js's
// runtime exports, which stay Node-only).
export * from "./stale-tasks.js";
Loading
Loading