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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ release artifact. Current release versions are:
| Package | Version | Release role |
| --- | ---: | --- |
| `@openspec-ui/core` | 0.18.0 | Shared behavior and persistence contract |
| `openspec-ui-vscode` | 0.12.0 | VS Code delivery |
| `openspec-ui-vscode` | 0.12.1 | VS Code delivery |
| `@openspec-ui/server` | 1.7.0 | Standalone server delivery |
| `@openspec-ui/webui` | 1.8.0 | Shared browser UI |
| `@openspec-ui/cli` | 0.1.0 | CI merge-gate delivery |
Expand Down
45 changes: 45 additions & 0 deletions openspec/changes/task-done-lock/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## Why

`tasks-tree-expand` added a "Delete Task" action on active-change task
items, gated only on archived state (`openspec-ui.activeTask` vs
`openspec-ui.archivedTask`). Raised in review: a task marked done still
offers Delete today, even though deleting a completed checklist line
throws away a record of finished work with no equivalent safeguard to
the one archived changes already get. There's no legitimate reason to
delete a done task's line — if it's wrong, the fix is unchecking it, not
removing the record that it happened.

## What Changes

- A task item whose checklist line is done (`- [x]`) no longer offers
"Delete Task" in its context menu, in **active** changes — matching
the existing behavior for archived changes, which already withhold it
regardless of done-state.
- The command itself refuses to delete a done task even if invoked
another way (not just menu-gated), the same fail-closed pattern
already used for the archived-item guard.
- Undone tasks in active changes are unaffected — still deletable exactly
as before.

## Capabilities

### New Capabilities

(none)

### Modified Capabilities

- `vscode-extension`: "Delete Task" is now withheld for done tasks in
active changes, not just archived ones.

## Impact

- `packages/extension/src/tree/changes-tree.ts` (`TaskTreeItem`: new
`openspec-ui.activeTaskDone` contextValue, distinct from
`openspec-ui.activeTask`).
- `packages/extension/src/commands.ts` (`openspec-ui.deleteTask`: guard
extended to `item.done`).
- `packages/extension/package.json` (no change needed — the existing
`viewItem == openspec-ui.activeTask` menu `when`-clause already
excludes the new `openspec-ui.activeTaskDone` value).
- No change to the command/event protocol, no core or standalone impact.
16 changes: 16 additions & 0 deletions openspec/changes/task-done-lock/smoke-test-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Smoke test — task-done-lock

- Real Extension Host run (`npm run test:integration --workspace
openspec-ui-vscode`): 6/6 passing, extension activates and registers
all contributed commands including `openspec-ui.deleteTask`.
- Unit tests exercise the actual new code paths, not mocks of them:
`changes-tree.test.ts` asserts a done active task gets
`openspec-ui.activeTaskDone` (not `openspec-ui.activeTask`), which is
what the unchanged `package.json` `"when": "viewItem ==
openspec-ui.activeTask"` clause relies on to withhold the menu item.
`commands.test.ts` asserts `openspec-ui.deleteTask` no-ops for a done
item without even prompting, mirroring the existing archived-item test.
- Live fixture check (via `tsx`, against a real temp `tasks.md`, deleted
after): `readTaskChecklist` correctly reports `done: true`/`false` for
a real `- [x]`/`- [ ]` line, confirming the data the tree consumes to
pick the contextValue is correct end to end, not just in mocks.
51 changes: 51 additions & 0 deletions openspec/changes/task-done-lock/specs/vscode-extension/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
## MODIFIED Requirements

### Requirement: Changes and Archive trees expand to individual tasks, with reveal and scoped delete

Expanding a change node in either the "Changes" or "Archive" tree view
SHALL also list that change's individual `tasks.md` checklist items as
child tree items, alongside its existing artifact children. Selecting a
task item SHALL open (or reveal, if already open) `tasks.md` with the
cursor moved to that task's line, in both trees. A "Delete Task" action
SHALL be available only on task items belonging to an active (non-
archived) change that are not marked done; selecting it, after
confirmation, SHALL remove exactly that task's checklist line from the
change's `tasks.md`. Task items belonging to archived changes, and done
task items in active changes, SHALL NOT offer a delete action.

#### Scenario: Expanding an active change shows its tasks

- **WHEN** the user expands a change node in the "Changes" tree
- **THEN** its individual `tasks.md` checklist items appear as child
items alongside the existing artifact children

#### Scenario: Selecting a task reveals it in the editor

- **WHEN** the user selects a task tree item (in either tree)
- **THEN** `tasks.md` opens (or is revealed, if already open) with the
cursor at that task's line

#### Scenario: Deleting a task from an active change

- **WHEN** the user confirms "Delete Task" on a task belonging to an
active change that is not marked done
- **THEN** that exact line is removed from the change's `tasks.md`

#### Scenario: Archived tasks offer no delete action

- **WHEN** the user views a task item under the "Archive" tree
- **THEN** no delete action is available for it

#### Scenario: Done tasks offer no delete action, even in active changes

- **WHEN** the user views a task item marked done (`- [x]`) under an
active change in the "Changes" tree
- **THEN** no delete action is available for it, and invoking the delete
command directly with that item makes no change to `tasks.md`

#### Scenario: The underlying file changed since the tree was last refreshed

- **WHEN** the user attempts to delete a task whose stored position no
longer matches the current content of `tasks.md`
- **THEN** the system reports that the task list has changed and makes no
filesystem change, rather than risking deletion of a different line
30 changes: 30 additions & 0 deletions openspec/changes/task-done-lock/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
## 1. Extension: withhold delete for done tasks

- [x] 1.1 `changes-tree.ts`: `TaskTreeItem` — new `openspec-ui.activeTaskDone`
contextValue for a done task in an active change (distinct from
`openspec-ui.activeTask`); tooltip explains why delete is withheld.
`openspec-ui.archivedTask` unchanged (archived always wins over
done-state).
- [x] 1.2 `commands.ts`: `openspec-ui.deleteTask` — guard extended to
`item.done`, same fail-closed pattern as the existing `item.archived`
no-op.
- [x] 1.3 `package.json`: no change needed — the existing
`"when": "viewItem == openspec-ui.activeTask"` menu entry already
excludes the new `openspec-ui.activeTaskDone` contextValue.
- [x] 1.4 `changes-tree.test.ts`: done active task gets
`openspec-ui.activeTaskDone`, not `openspec-ui.activeTask`.
- [x] 1.5 `commands.test.ts`: `deleteTask` no-ops for a done task without
prompting, same as the existing archived-task test.

## 2. Verification, versioning, and smoke test

- [x] 2.1 `npm run typecheck && npm run lint && npm run test` passes for
`packages/extension`. Re-run `npm run verify` after `git add`/commit of
all new files.
- [x] 2.2 Bump `package.json` version (patch) for `openspec-ui-vscode`.
Add a `packages/extension/CHANGELOG.md` entry and a README line.
- [x] 2.3 Manual smoke test: real Extension Host run via
`npm run test:integration` (6/6 passing), plus a real `readTaskChecklist`
run against a temp `tasks.md` confirming done-state is read correctly.
Full detail in `smoke-test-notes.md`.
- [x] 2.4 `openspec change validate --strict task-done-lock` passes.
76 changes: 76 additions & 0 deletions openspec/specs/repo-bootstrap-snippets/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# repo-bootstrap-snippets Specification

## Purpose
TBD - created by archiving change repo-bootstrap-snippets. Update Purpose after archive.
## Requirements
### Requirement: Agent instructions are generated identically into CLAUDE.md and AGENTS.md

The system SHALL let the user generate a project-type-specific
instructions block, written identically into both `CLAUDE.md` and
`AGENTS.md` at the workspace root, delimited by managed-section markers.
If either file already exists without matching markers, the system
SHALL NOT modify that file and SHALL report it as not managed.

#### Scenario: Neither file exists yet

- **WHEN** the user generates agent instructions for a project type and
neither `CLAUDE.md` nor `AGENTS.md` exists
- **THEN** both files are created with identical managed-section content

#### Scenario: A file already exists without the managed-section markers

- **WHEN** `CLAUDE.md` already exists and does not contain the managed-
section markers
- **THEN** `CLAUDE.md` is left unmodified and the system reports it as
not managed, while `AGENTS.md` (if unmanaged or absent) is still
handled independently

#### Scenario: Regenerating an already-managed file preserves content outside the markers

- **WHEN** the user regenerates agent instructions for a file that
already has the managed-section markers, with user-authored content
after the closing marker
- **THEN** only the content between the markers is replaced; content
before the start marker and after the end marker is preserved verbatim

### Requirement: Dependabot configuration accumulates ecosystems across invocations

The system SHALL let the user generate or update `.github/dependabot.yml`
for one or more selected project types, using a first-line ownership
marker. Invoking it again for a different project type SHALL add that
ecosystem without removing ecosystems added by a prior invocation. A
`dependabot.yml` that exists without the ownership marker SHALL NOT be
modified.

#### Scenario: First invocation creates the file

- **WHEN** `.github/dependabot.yml` does not exist and the user selects
a project type
- **THEN** the file is created with that ecosystem's entry, the
ownership marker, and a `github-actions` entry

#### Scenario: A later invocation adds another ecosystem

- **WHEN** the file already exists (managed) with one ecosystem's entry
and the user selects a different project type
- **THEN** the regenerated file contains both ecosystems' entries

#### Scenario: An unmanaged dependabot.yml is left alone

- **WHEN** `.github/dependabot.yml` exists without the ownership marker
- **THEN** the system makes no change and reports the file as not
managed

### Requirement: Path-scoped Copilot instructions are generated per subtype

The system SHALL let the user generate
`.github/instructions/<subtype>.instructions.md` for a selected project
type and subtype, with `applyTo` frontmatter and managed-section content,
using the same file-ownership rule as `CLAUDE.md`/`AGENTS.md`.

#### Scenario: Generating instructions for a subtype

- **WHEN** the user selects a project type and a subtype
- **THEN** `.github/instructions/<subtype>.instructions.md` is created
with `applyTo` frontmatter and the subtype's managed content

28 changes: 28 additions & 0 deletions openspec/specs/template-catalog/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,31 @@ manifest without requiring a separate extension.
violates the schema
- **THEN** VS Code's native editor surfaces the validation error inline

### Requirement: Project-level templates can be deleted through the product

The system SHALL let the user permanently delete a project-level
template (`openspec/templates/<id>/` and its contents) through both
delivery targets, after an explicit confirmation step. Built-in
templates SHALL NOT offer a delete action anywhere in either delivery
target's UI — they have no on-disk representation in the target
workspace to delete.

#### Scenario: Deleting an existing project-level template

- **WHEN** the user confirms deletion of project-level template `X`
- **THEN** `openspec/templates/X/` no longer exists, and `X` no longer
appears in the template list

#### Scenario: Deleting an unknown project-level template id

- **WHEN** the requested id has no matching `openspec/templates/<id>/`
directory
- **THEN** the system reports a clear "not found" error and makes no
filesystem change

#### Scenario: Built-in tree/tab items never offer delete

- **WHEN** the user views a built-in template in either delivery
target's UI
- **THEN** no delete action is available for it

42 changes: 42 additions & 0 deletions openspec/specs/vscode-extension/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,45 @@ for the areas the embed does not show.
- **THEN** no embed signal is relevant, since this mode does not load the
standalone shell at all

### Requirement: Changes and Archive trees expand to individual tasks, with reveal and scoped delete

Expanding a change node in either the "Changes" or "Archive" tree view
SHALL also list that change's individual `tasks.md` checklist items as
child tree items, alongside its existing artifact children. Selecting a
task item SHALL open (or reveal, if already open) `tasks.md` with the
cursor moved to that task's line, in both trees. A "Delete Task" action
SHALL be available only on task items belonging to an active (non-
archived) change; selecting it, after confirmation, SHALL remove exactly
that task's checklist line from the change's `tasks.md`. Task items
belonging to archived changes SHALL NOT offer a delete action.

#### Scenario: Expanding an active change shows its tasks

- **WHEN** the user expands a change node in the "Changes" tree
- **THEN** its individual `tasks.md` checklist items appear as child
items alongside the existing artifact children

#### Scenario: Selecting a task reveals it in the editor

- **WHEN** the user selects a task tree item (in either tree)
- **THEN** `tasks.md` opens (or is revealed, if already open) with the
cursor at that task's line

#### Scenario: Deleting a task from an active change

- **WHEN** the user confirms "Delete Task" on a task belonging to an
active change
- **THEN** that exact line is removed from the change's `tasks.md`

#### Scenario: Archived tasks offer no delete action

- **WHEN** the user views a task item under the "Archive" tree
- **THEN** no delete action is available for it

#### Scenario: The underlying file changed since the tree was last refreshed

- **WHEN** the user attempts to delete a task whose stored position no
longer matches the current content of `tasks.md`
- **THEN** the system reports that the task list has changed and makes no
filesystem change, rather than risking deletion of a different line

8 changes: 8 additions & 0 deletions packages/extension/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 0.12.1

- "Delete Task" is no longer offered for a task marked done, even in an
active (non-archived) change — matching the guard already in place for
archived changes. A completed checklist line records that the work
happened; the fix for a wrongly-checked task is unchecking it, not
deleting the record.

## 0.12.0

- Added three Command Palette commands to bootstrap repository files
Expand Down
3 changes: 2 additions & 1 deletion packages/extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ notifications while keeping lifecycle and security behavior in
- Create, validate, archive, unarchive, and delete changes from contextual
actions with confirmation for destructive operations. Expand a change to
see its individual `tasks.md` checklist items; select one to jump to it
in the editor, or delete it (active changes only) with confirmation.
in the editor, or delete it (active changes only, and only if not yet
done) with confirmation.
- See queued, running, completed, failed, cancelled, interrupted, and
rolled-back operations in the Processes view. Read-only work can run in
parallel; workspace mutations are serialized to keep checkpoints isolated.
Expand Down
2 changes: 1 addition & 1 deletion packages/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"displayName": "OpenSpec Workbench",
"description": "Native OpenSpec workflow with lifecycle controls, VS Code Chat, persistent processes, checkpoints, and conflict-safe rollback.",
"publisher": "openspec-ui",
"version": "0.12.0",
"version": "0.12.1",
"icon": "media/icon.png",
"license": "MIT",
"repository": {
Expand Down
9 changes: 9 additions & 0 deletions packages/extension/src/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,15 @@ describe("registerCommands", () => {
expect(deleteTaskLineMock).not.toHaveBeenCalled();
});

it("does nothing for a done task, even in an active change, without even prompting", async () => {
registerCommands(makeContext() as unknown as import("vscode").ExtensionContext, makeDeps());

await vscodeMock._registeredCommands.get("openspec-ui.deleteTask")?.({ ...activeItem, done: true });

expect(vscodeMock.window.showWarningMessage).not.toHaveBeenCalled();
expect(deleteTaskLineMock).not.toHaveBeenCalled();
});

it("reports a stale task list as a warning, not an error", async () => {
vscodeMock.window.showWarningMessage.mockResolvedValueOnce("Delete");
deleteTaskLineMock.mockRejectedValue(new TaskListChangedError("task list changed"));
Expand Down
2 changes: 1 addition & 1 deletion packages/extension/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ export function registerCommands(context: vscode.ExtensionContext, deps: Command
}),
vscode.commands.registerCommand("openspec-ui.deleteTask", async (item?: TaskTreeItem) => {
const workspaceRoot = deps.getWorkspaceRoot();
if (!workspaceRoot || !item || item.archived) return;
if (!workspaceRoot || !item || item.archived || item.done) return;
const answer = await vscode.window.showWarningMessage(
`Permanently delete task "${item.text}" from ${item.changeName}'s tasks.md?`,
{ modal: true },
Expand Down
3 changes: 2 additions & 1 deletion packages/extension/src/tree/changes-tree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ describe("ChangesTreeProvider", () => {
expect(children.map((item) => item.label)).toEqual(["1.1 First task", "1.2 Second task"]);
expect(children[0]?.description).toBe("done");
expect(children[1]?.description).toBeUndefined();
expect(children[0]?.contextValue).toBe("openspec-ui.activeTask");
expect(children[0]?.contextValue).toBe("openspec-ui.activeTaskDone");
expect(children[1]?.contextValue).toBe("openspec-ui.activeTask");
expect(children[0]?.command?.command).toBe("openspec-ui.revealTask");
});

Expand Down
9 changes: 8 additions & 1 deletion packages/extension/src/tree/changes-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,14 @@ export class TaskTreeItem extends vscode.TreeItem {
) {
super(text, vscode.TreeItemCollapsibleState.None);
this.description = done ? "done" : undefined;
this.contextValue = archived ? "openspec-ui.archivedTask" : "openspec-ui.activeTask";
this.contextValue = archived
? "openspec-ui.archivedTask"
: done
? "openspec-ui.activeTaskDone"
: "openspec-ui.activeTask";
if (!archived && done) {
this.tooltip = `${text}\n\nDone tasks can't be deleted.`;
}
this.iconPath = new vscode.ThemeIcon(done ? "check" : "circle-large-outline");
this.command = { command: "openspec-ui.revealTask", title: "Reveal Task", arguments: [this] };
}
Expand Down
Loading