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
5 changes: 5 additions & 0 deletions .changeset/clarify-no-tracking-task-counts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openspecui/web': patch
---

Clarify no-tracking task counts after the Owner walkthrough of the 1.13.2 rotation: the Change Detail apply badge keeps the CLI-owned 0/0 numbers while lowering visual weight and appending the "schema tracks no tasks" qualification to its accessible name and tooltip (strictly on `taskTrackingConfigured === false`, never for absent members), and Changes rows with the CLI `no-tasks` status and zero totals render a muted "No tasks" whose tooltip keeps the upstream ambiguity (unconfigured `apply.tracks` vs an empty tracked list) instead of a bare `Tasks 0/0`. Board cards intentionally stay unchanged.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: opsx-collab-pr-loop
created: 2026-09-27
35 changes: 35 additions & 0 deletions openspec/changes/clarify-no-tracking-task-counts/loop/intake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<!--
Orthogonal intents (created 2026-09-27 Asia/Shanghai):
1. Record the Owner-walkthrough P3 adjudication and its rulings (fix / reject) with rationale.
2. Keep the delivered update-openspec-cli-1132 semantics untouched (presentation-only change).

Original request (2026-09-27): "我目前看没有致命问题,剩余裁决你和 codex 讨论,完成工作收尾"
-->

# clarify-no-tracking-task-counts intake

## Adjudication record (2026-09-27)

Owner's final walkthrough of update-openspec-cli-1132: no fatal issues. Three P3 observations went
to joint adjudication. The Codex channel was hard-down (local gateway 502 on every attempt,
confirmed by direct probe), so per the standing substitution rule an independent super-thinker
reviewer executed the adjudication against the real repository code; rulings below are joint
(ZCode + that reviewer).

| P3 | Ruling | Essence |
| --- | --- | --- |
| 1 — Detail `0/0` badge beside the no-tracking note | **Fix** | Badge keeps literal CLI numbers, lowers tone, appends qualification to aria/tooltip; gated strictly on `taskTrackingConfigured === false` (never absent). Data already in hand (`change-view.tsx` holds the member); presentation only. |
| 2 — Board card lacks unavailable-tracking hint | **Reject** | Unavailable evidence is a decision fact owned by the Change Detail direct plane (delivered spec's design, not an omission); the list/Kanban payload carries no `unavailableTrackingFiles`, so a fix needs per-change apply subscriptions — cost/benefit inverted, and it would push decision facts onto a scan surface against the OPSX-first hierarchy law. Revisit only if upstream `list` ever aggregates tracking-evidence summaries. |
| 3 — Changes row `Tasks 0/0` for no-tracking schemas | **Fix** | `cliTaskSummary.status === 'no-tasks' && totalTasks === 0` renders muted `No tasks`; tooltip keeps the upstream ambiguity verbatim (list cannot distinguish unconfigured `apply.tracks` from an empty tracked list — the wording must not assert either). Board cards unchanged (lane placement already carries the phase). |

## Non-Goals

- No schema/projection/gating change: `opsx-types.ts`, CLI contracts, `createApplyInstructionProgress`
untouched; numbers stay CLI-owned (2026-08-18 law).
- Board/Kanban cards unchanged (Kanban projection law; P3-2 rejected).
- No change to the archived update-openspec-cli-1132 or its spec.

## Acceptance boundary

Focused web tests + typecheck + the repo's changed-file format/lint gates; Owner visual pass is
already covered by the walkthrough that produced these P3s (badge/list states are the same pages).
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<!--
Orthogonal intents (created 2026-09-27 Asia/Shanghai):
1. Clarify scan-level task-count presentation for schemas that track no tasks (CLI 1.13.2 facts).

Original request (2026-09-27): Owner walkthrough of update-openspec-cli-1132 recorded three P3
observations; joint adjudication (ZCode + independent reviewer, Codex channel unavailable) ruled
two as presentation fixes and one as rejected — rulings recorded in loop/intake.md.
-->

## ADDED Requirements

### Requirement: No-Tracking Task Count Scan Presentation

Scan-level task-count presentation SHALL distinguish "the CLI reports no tasks" from incomplete
work without redefining CLI-owned progress evidence. On Change Detail, when the apply projection
carries `taskTrackingConfigured === false` (strictly, never for absent members), the apply progress
badge SHALL keep the literal CLI numbers while lowering its visual weight and appending the
no-tracking qualification to its accessible name and tooltip. On the Changes list, when the CLI
task summary carries `status: "no-tasks"` with `totalTasks === 0`, the row SHALL present a muted
"No tasks" in place of the `Tasks 0/0` count, with a tooltip that keeps the upstream ambiguity
verbatim (the list cannot distinguish an unconfigured `apply.tracks` from an empty tracked list;
the wording SHALL NOT assert either). Absent members and every other status SHALL render exactly
as before. Board/Kanban cards SHALL NOT change (lane placement already carries the phase fact).

#### Scenario: Detail badge degrades, not hides, for untracked schemas

- **GIVEN** a Change Detail whose apply instructions carry `taskTrackingConfigured: false` with
`progress {0,0,0}`
- **WHEN** the header renders
- **THEN** the badge SHALL still show the literal `0/0` numbers in a muted tone
- **AND** its accessible name and tooltip SHALL append the schema-tracks-no-tasks qualification

#### Scenario: Absent tracking member keeps today's badge

- **GIVEN** apply instructions from a pre-1.13.2 CLI (no `taskTrackingConfigured`)
- **WHEN** the header renders
- **THEN** the badge SHALL render exactly as before, unqualified

#### Scenario: List row reports No tasks for the no-tasks status

- **GIVEN** a Changes row whose CLI task summary carries `status: "no-tasks"` and `totalTasks: 0`
- **WHEN** the row renders
- **THEN** it SHALL show a muted `No tasks` instead of `Tasks 0/0`
- **AND** the tooltip SHALL state the CLI reports no tasks without asserting whether tracking is
configured
- **AND** rows with any other status or nonzero totals SHALL render exactly as before
32 changes: 32 additions & 0 deletions packages/web/src/components/apply-progress-notice.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,38 @@ describe('ApplyProgressBadge', () => {
)
).toBeTruthy()
})

it('degrades, never hides, the CLI numbers when the schema tracks no tasks', async () => {
render(
<ApplyProgressBadge
applyInstructionProgress={{ ...progress(false), complete: 0, total: 0, remaining: 0 }}
taskTrackingConfigured={false}
/>
)

// The numbers stay CLI-owned verbatim; only weight and qualification change.
const badge = screen.getByRole('note', {
name: 'Apply instructions progress 0 of 0; schema tracks no tasks',
})
expect(badge).toHaveTextContent('Apply 0/0')
expect(badge.className).toContain('opacity-75')
fireEvent.focus(badge)
expect(
await screen.findByText(/schema sets no apply\.tracks, so 0 of 0 is not incomplete work/)
).toBeTruthy()
})

it('keeps the badge unqualified when the tracking member is absent', () => {
render(
<ApplyProgressBadge
applyInstructionProgress={{ ...progress(false), complete: 0, total: 0, remaining: 0 }}
taskTrackingConfigured={undefined}
/>
)

const badge = screen.getByRole('note', { name: 'Apply instructions progress 0 of 0' })
expect(badge).not.toHaveClass('opacity-75')
})
})

describe('ApplyProgressNotice', () => {
Expand Down
21 changes: 19 additions & 2 deletions packages/web/src/components/apply-progress-notice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
* warnings area, a compact note when the schema tracks no tasks (empty tasks are not
* missing evidence), and nothing extra when both members are absent — absence is
* "unknown (pre-1.13.2 CLI)", never a fabricated 0/0 or unavailable claim.
* 9. Degrade, never hide, the subtitle badge's CLI numbers when the schema tracks no tasks:
* `taskTrackingConfigured === false` lowers the badge's visual weight and appends the
* qualification to its accessible name and tooltip; the numbers stay CLI-owned verbatim.
*
* Original request (2026-07-15): "与 tracked glob 进度分歧时各自归因展示。"
* Original request (2026-07-28): supporting 6.x evidence should use Badge + Tooltip or Accordion.
Expand All @@ -26,6 +29,8 @@
* Original request (2026-09-12): Owner walkthrough: the always-expanded warning/build-order blocks
* consumed the page; collapse them behind one summary row while keeping direct-plane discovery.
* Original request (2026-09-26): "Openspec 1.13.2 释放了…" — tracking-evidence surface (update-openspec-cli-1132 Slice 2).
* Original request (2026-09-27): Owner walkthrough P3 adjudication — badge/list no-tracking count
* presentation (clarify-no-tracking-task-counts).
*/
import { InformationBadge } from '@/components/information-disclosure'
import type { ApplyInstructionProgress } from '@openspecui/core'
Expand All @@ -40,14 +45,26 @@ import { Fragment, useState } from 'react'
*/
export function ApplyProgressBadge({
applyInstructionProgress,
taskTrackingConfigured,
}: {
applyInstructionProgress: ApplyInstructionProgress
taskTrackingConfigured?: boolean
}) {
const { complete, total, remaining } = applyInstructionProgress
// Strictly `=== false`: absent means "unknown (pre-1.13.2 CLI)" and keeps today's badge
// unqualified; the CLI-owned numbers themselves never change.
const untracked = taskTrackingConfigured === false
return (
<InformationBadge
ariaLabel={`Apply instructions progress ${complete} of ${total}`}
tooltip={`Progress reported by openspec instructions apply — ${complete} of ${total} tasks applied, ${remaining} remaining.`}
ariaLabel={`Apply instructions progress ${complete} of ${total}${
untracked ? '; schema tracks no tasks' : ''
}`}
tooltip={
untracked
? `Progress reported by openspec instructions apply — ${complete} of ${total} tasks applied, ${remaining} remaining. The schema sets no apply.tracks, so 0 of 0 is not incomplete work.`
: `Progress reported by openspec instructions apply — ${complete} of ${total} tasks applied, ${remaining} remaining.`
}
className={untracked ? 'opacity-75' : undefined}
>
Apply {complete}/{total}
</InformationBadge>
Expand Down
8 changes: 7 additions & 1 deletion packages/web/src/components/change-context-summary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* Owner correction (2026-08-03): unify Change scan Tooltips in the subtitle and keep failures below the Header.
* Original request (2026-08-15): Owner walkthrough: merge Apply progress into the subtitle badge row.
* Original request (2026-08-15): 刷新/解析中的 lifecycle 锁收敛为副标题行内的 shiny 徽章 + Tooltip。
* Original request (2026-09-27): Owner walkthrough P3 adjudication — taskTrackingConfigured passthrough to the apply badge.
*/
import { ApplyProgressBadge } from '@/components/apply-progress-notice'
import { InformationBadge } from '@/components/information-disclosure'
Expand Down Expand Up @@ -71,13 +72,15 @@ export function ChangeContextSummary({
status,
referenceEvidence,
applyInstructionProgress,
taskTrackingConfigured,
statusRefreshing = null,
rootChecking = null,
}: {
status: ChangeStatus
referenceEvidence: ChangeReferenceEvidence
/** Apply instruction progress; the CLI's own count is the implementation progress authority. */
applyInstructionProgress?: ApplyInstructionProgress | null
taskTrackingConfigured?: boolean
/** Non-current Change Status authority; actions stay read-only while refreshing. */
statusRefreshing?: LifecycleBadgeFact | null
/** Root Context still resolving/refreshing; root-dependent actions stay locked. */
Expand Down Expand Up @@ -108,7 +111,10 @@ export function ChangeContextSummary({
{doneCount}/{totalCount} artifacts
</InformationBadge>
{applyInstructionProgress ? (
<ApplyProgressBadge applyInstructionProgress={applyInstructionProgress} />
<ApplyProgressBadge
applyInstructionProgress={applyInstructionProgress}
taskTrackingConfigured={taskTrackingConfigured}
/>
) : null}
{provenance.kind === 'static' ? (
<InformationBadge
Expand Down
37 changes: 37 additions & 0 deletions packages/web/src/routes/change-list.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,43 @@ describe('ChangeList', () => {
expect(screen.queryByText('Loading workflow status…')).toBeNull()
})

it('renders No tasks for the CLI no-tasks status with zero totals, keeping the ambiguity', () => {
useChangesSubscriptionMock.mockReturnValue({
data: [
{
id: 'untracked-schema-change',
name: 'untracked-schema-change',
trackedTaskProgress: { total: 0, completed: 0, phase: 'no-tasks' },
cliTaskSummary: { completedTasks: 0, totalTasks: 0, status: 'no-tasks' },
updatedAt: Date.now() - 60_000,
},
{
id: 'tracked-empty-history',
name: 'tracked-empty-history',
trackedTaskProgress: { total: 0, completed: 0, phase: 'no-tasks' },
cliTaskSummary: { completedTasks: 0, totalTasks: 0, status: 'in-progress' },
updatedAt: Date.now() - 60_000,
},
],
isLoading: false,
})
useOpsxStatusListSubscriptionMock.mockReturnValue({
data: undefined,
isLoading: true,
error: null,
})

render(<ChangeList />)

// no-tasks + zero totals -> the CLI's typed "no task work" fact, muted, never Tasks 0/0.
const noTasks = screen.getByText('No tasks')
expect(noTasks.getAttribute('title')).toBe(
'The OpenSpec CLI reports no tasks for this Change (no apply.tracks, or an empty tracked task list).'
)
// Exactly one literal count remains — the in-progress row; the no-tasks row carries none.
expect(screen.getAllByText(/Tasks 0\/0/)).toHaveLength(1)
})

it('admits aggregate workflow Status only after the first Change row is renderable', () => {
useChangesSubscriptionMock.mockReturnValue({
data: undefined,
Expand Down
21 changes: 18 additions & 3 deletions packages/web/src/routes/change-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* Original request (2026-09-12): Owner walkthrough: unify the Change display title — generic
* "# Proposal" headings fall back to the change id on every surface.
* Original request (2026-09-17): "Openspec 1.13.1 释放了…" — change-list nested/warnings projection (update-openspec-cli-1131 Slice 2).
* Original request (2026-09-27): Owner walkthrough P3 adjudication — No tasks presentation for the CLI no-tasks status.
*/
import { ChangeRow, ChangeRowChevron } from '@/components/change-row'
import {
Expand Down Expand Up @@ -234,10 +235,24 @@ export function ChangeList() {
cliTaskSummary && cliTaskSummary.totalTasks > 0
? cliTaskSummary.completedTasks / cliTaskSummary.totalTasks
: null
// `no-tasks` with zero totals is the CLI's typed fact that this Change has no
// task work; the list cannot distinguish unconfigured apply.tracks from an empty
// tracked list, so the tooltip keeps that ambiguity instead of asserting either.
const cliNoTasks =
cliTaskSummary?.status === 'no-tasks' && cliTaskSummary.totalTasks === 0
const cliTaskEvidence = cliTaskSummary ? (
<span title="Task counts reported by the OpenSpec CLI for this Change.">
Tasks {cliTaskSummary.completedTasks}/{cliTaskSummary.totalTasks}
</span>
cliNoTasks ? (
<span
className="text-muted-foreground"
title="The OpenSpec CLI reports no tasks for this Change (no apply.tracks, or an empty tracked task list)."
>
No tasks
</span>
) : (
<span title="Task counts reported by the OpenSpec CLI for this Change.">
Tasks {cliTaskSummary.completedTasks}/{cliTaskSummary.totalTasks}
</span>
)
) : null
const sharedDescriptor = { family: 'changes', entityId: change.id } as const
return (
Expand Down
5 changes: 5 additions & 0 deletions packages/web/src/routes/change-view.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,11 @@ describe('ChangeView', () => {
expect(note).toHaveTextContent(/tracks no tasks/i)
// An empty task list under a no-tracking schema is never presented as blocked work.
expect(within(region).queryByRole('alert')).toBeNull()
// The subtitle badge degrades the same fact without hiding the CLI numbers.
const badge = screen.getByRole('note', {
name: 'Apply instructions progress 0 of 0; schema tracks no tasks',
})
expect(badge).toHaveTextContent('Apply 0/0')
})

it('routes static Change evidence into its dedicated tab', () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/web/src/routes/change-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
* — the status region gains the OpenSpec 1.13 Apply `warnings` and `missingPrerequisites`
* direct-plane evidence beside the existing divergence notice.
* Original request (2026-09-26): "Openspec 1.13.2 释放了…" — tracking-evidence direct-plane mounting (update-openspec-cli-1132 Slice 2).
* Original request (2026-09-27): Owner walkthrough P3 adjudication — badge receives taskTrackingConfigured.
*/
import { ApplyProgressNotice } from '@/components/apply-progress-notice'
import {
Expand Down Expand Up @@ -182,6 +183,7 @@ export function ChangeView() {
status={status}
referenceEvidence={referenceEvidence}
applyInstructionProgress={applyInstructions?.applyInstructionProgress ?? null}
taskTrackingConfigured={applyInstructions?.taskTrackingConfigured}
statusRefreshing={
statusCurrent
? null
Expand Down
Loading