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/fuzzy-dagster-workspaces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fission-ai/openspec": patch
---

Move beta workspace view state to `.openspec-workspace/view.yaml`, stop top-level `openspec update` from routing into workspace updates, and ignore foreign root `workspace.yaml` files so Dagster projects keep updating normally.
2 changes: 1 addition & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ openspec workspace update --workspace platform --tools none

`workspace update` refreshes the generated workspace guidance block and local open surface. For agent skills, it reuses the stored workspace skill agent selection when `--tools` is omitted. Passing `--tools` replaces that stored selection. It refreshes only OpenSpec-managed workflow skill directories in the workspace root, removes deselected managed workflow skills, and leaves linked repos and folders untouched.

Running `openspec update` from inside a workspace redirects to `openspec workspace update`; run `openspec update` inside repo-local projects when you want repo-owned tool files updated.
Running `openspec update` from inside a workspace does not update workspace-local files. Use `openspec workspace update` when you want workspace-local guidance and skills refreshed, and run `openspec update` inside repo-local projects when you want repo-owned tool files updated.

### `openspec workspace open`

Expand Down
11 changes: 7 additions & 4 deletions docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ A workspace has a different shape from a repo-local project:

```text
getGlobalDataDir()/workspaces/<workspace-name>/
├── workspace.yaml # Private local view record
├── .openspec-workspace/
│ └── view.yaml # Private local view record
├── AGENTS.md # Generated runtime guidance
└── <workspace-name>.code-workspace # Generated editor workspace file
```
Expand All @@ -85,12 +86,14 @@ repo-root/
└── changes/
```

Root-level `workspace.yaml` files are not OpenSpec workspace state. Workspace state is namespaced under `.openspec-workspace/`, so other tools can keep owning root-level files with the same name.

That distinction matters. The workspace folder is a local coordination surface for opening and inspecting linked repos or folders. Each repo's `openspec/` directory remains the home for repo-owned specs, repo-local changes, and implementation planning. Users do not need to run repo-local `openspec init` inside a workspace folder.

Stable link names are how a workspace refers to repos and folders. The private workspace record keeps names such as `api`, `web`, or `checkout` and maps them to this runtime's local paths.

```yaml
# workspace.yaml
# .openspec-workspace/view.yaml
version: 1
name: platform
context: null
Expand All @@ -99,7 +102,7 @@ links:
web: /repos/web
```

When a workspace opens an initiative, `context` records the selected context-store binding and initiative id. Registry-selected stores stay portable by id; path-selected stores intentionally preserve the runtime-local path because `workspace.yaml` is private local state.
When a workspace opens an initiative, `context` records the selected context-store binding and initiative id. Registry-selected stores stay portable by id; path-selected stores intentionally preserve the runtime-local path because `.openspec-workspace/view.yaml` is private local state.

```yaml
context:
Expand Down Expand Up @@ -133,7 +136,7 @@ getGlobalDataDir()/workspaces

That means `$XDG_DATA_HOME/openspec/workspaces` when `XDG_DATA_HOME` is set, `~/.local/share/openspec/workspaces` on Unix-style fallback, and `%LOCALAPPDATA%\openspec\workspaces` on native Windows fallback. Native Windows shells, PowerShell, and WSL2 each keep the path strings for the runtime running OpenSpec. This foundation does not translate between `D:\repo`, `/mnt/d/repo`, and UNC WSL paths.

OpenSpec can still read older beta workspace roots as compatibility inputs, but managed workspaces now use the root `workspace.yaml` record above. The workspace folder remains authoritative for its own private local view.
Managed workspaces use the namespaced private view record above. The workspace folder remains authoritative for its own private local view.

Workspace visibility is not change commitment. Set up a workspace when OpenSpec should know which repos or folders are relevant; create a change later when you are ready to plan a feature, fix, project, or other piece of work.

Expand Down
2 changes: 1 addition & 1 deletion docs/workspaces-beta/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ which local repos or folders to include.

The opened editor view shows linked repos and folders first, initiative context
when attached, and a small `OpenSpec workspace` folder last with `AGENTS.md`,
`workspace.yaml`, and the generated `.code-workspace` file.
`.openspec-workspace/view.yaml`, and the generated `.code-workspace` file.

Use `openspec workspace open --initiative team-context/billing-launch --editor`
when you want to skip the picker. Use `--agent codex-cli`, `--agent claude`, or
Expand Down
9 changes: 8 additions & 1 deletion openspec/specs/cli-update/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ The archive slash command template SHALL support optional change ID arguments fo
- **AND** wrap it in a clear structure like `<ChangeId>\n $ARGUMENTS\n</ChangeId>` to indicate the expected argument
- **AND** include validation steps in the template body to check if the change ID is valid

### Requirement: Repo update redirects from workspace planning homes
### Requirement: Repo update stays separate from workspace planning homes
The repo-local `openspec update` command SHALL not silently treat a workspace planning home as a repo-local OpenSpec project.

#### Scenario: Running update from a workspace root
Expand All @@ -186,6 +186,13 @@ The repo-local `openspec update` command SHALL not silently treat a workspace pl
- **WHEN** the user runs `openspec update`
- **THEN** OpenSpec SHALL preserve existing repo-local update behavior

#### Scenario: Updating a repo-local project nested below a workspace folder
- **GIVEN** the target path contains repo-local OpenSpec state
- **AND** an ancestor is an OpenSpec workspace root
- **WHEN** the user runs `openspec update <path>`
- **THEN** OpenSpec SHALL preserve repo-local update behavior for the target path
- **AND** it SHALL not run workspace update behavior

## Edge Cases

### Error Handling
Expand Down
8 changes: 4 additions & 4 deletions openspec/specs/workspace-foundation/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ OpenSpec SHALL use one kebab-case workspace name across workspace identity, mana

#### Scenario: Using one workspace name
- **WHEN** OpenSpec creates or records a managed workspace
- **THEN** the workspace name SHALL be stored in `.openspec-workspace/workspace.yaml`
- **THEN** the workspace name SHALL be stored in `.openspec-workspace/view.yaml`
- **AND** the same name SHALL be used as the default managed workspace folder name
- **AND** the same name SHALL be used as the local registry name

Expand Down Expand Up @@ -78,7 +78,7 @@ OpenSpec SHALL keep shared workspace information separate from local machine pat

#### Scenario: Keeping managed workspace view state local
- **WHEN** OpenSpec creates a managed workspace
- **THEN** it SHALL write `workspace.yaml` in the workspace root as private local view state
- **THEN** it SHALL write `.openspec-workspace/view.yaml` as private local view state
- **AND** the file SHALL preserve stable link names and local path values for the current machine

### Requirement: Standard Workspace Location
Expand Down Expand Up @@ -130,7 +130,7 @@ OpenSpec SHALL keep a lightweight local registry of known workspaces on the curr

#### Scenario: Keeping workspace folders authoritative
- **WHEN** OpenSpec reads workspace details
- **THEN** each workspace folder's `.openspec-workspace/workspace.yaml` SHALL remain the source of truth for that workspace
- **THEN** each workspace folder's `.openspec-workspace/view.yaml` SHALL remain the source of truth for that workspace
- **AND** the local registry SHALL act only as an index of known workspace locations

#### Scenario: Finding workspaces from anywhere
Expand Down Expand Up @@ -210,7 +210,7 @@ OpenSpec SHALL store a workspace's preferred opener in machine-local workspace s

#### Scenario: Recording an interactive setup opener choice
- **WHEN** an interactive user chooses a preferred opener during `openspec workspace setup`
- **THEN** OpenSpec SHALL record the opener in `.openspec-workspace/local.yaml`
- **THEN** OpenSpec SHALL record the opener in `.openspec-workspace/view.yaml`
- **AND** the stored value SHALL use a structured `preferred_opener` object with `kind` and `id`

#### Scenario: Recording a non-interactive setup opener choice
Expand Down
35 changes: 27 additions & 8 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import ora from 'ora';
import path from 'path';
import { fileURLToPath } from 'url';
import { promises as fs } from 'fs';
import { AI_TOOLS } from '../core/config.js';
import { AI_TOOLS, OPENSPEC_DIR_NAME } from '../core/config.js';
import { UpdateCommand } from '../core/update.js';
import { ListCommand } from '../core/list.js';
import { ArchiveCommand } from '../core/archive.js';
Expand All @@ -17,10 +17,7 @@ import { CompletionCommand } from '../commands/completion.js';
import { FeedbackCommand } from '../commands/feedback.js';
import { registerConfigCommand } from '../commands/config.js';
import { registerSchemaCommand } from '../commands/schema.js';
import {
registerWorkspaceCommand,
runWorkspaceUpdateForRoot,
} from '../commands/workspace.js';
import { registerWorkspaceCommand } from '../commands/workspace.js';
import { registerContextStoreCommand } from '../commands/context-store.js';
import { registerInitiativeCommand } from '../commands/initiative.js';
import { findWorkspaceRoot } from '../core/workspace/index.js';
Expand Down Expand Up @@ -100,6 +97,22 @@ program.hook('postAction', async () => {
const availableToolIds = AI_TOOLS.filter((tool) => tool.skillsDir).map((tool) => tool.value);
const toolsOptionDescription = `Configure AI tools non-interactively. Use "all", "none", or a comma-separated list of: ${availableToolIds.join(', ')}`;

async function hasRepoLocalOpenSpecProject(projectPath: string): Promise<boolean> {
try {
const stats = await fs.stat(path.join(projectPath, OPENSPEC_DIR_NAME));
return stats.isDirectory();
} catch (error) {
const code =
typeof error === 'object' && error !== null && 'code' in error
? (error as { code?: unknown }).code
: undefined;
if (code !== 'ENOENT' && code !== 'ENOTDIR') {
throw error;
}
return false;
}
}

program
.command('init [path]')
.description('Initialize OpenSpec in your project')
Expand Down Expand Up @@ -170,13 +183,19 @@ program
.action(async (targetPath = '.', options?: { force?: boolean }) => {
try {
const resolvedPath = path.resolve(targetPath);
const updateCommand = new UpdateCommand({ force: options?.force });
if (await hasRepoLocalOpenSpecProject(resolvedPath)) {
await updateCommand.execute(resolvedPath);
return;
}

const workspaceRoot = await findWorkspaceRoot(resolvedPath);
if (workspaceRoot) {
await runWorkspaceUpdateForRoot(workspaceRoot, { force: options?.force });
return;
throw new Error(
'OpenSpec workspace detected. Run `openspec workspace update` to refresh workspace-local guidance and skills.'
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const updateCommand = new UpdateCommand({ force: options?.force });
await updateCommand.execute(resolvedPath);
} catch (error) {
console.log(); // Empty line for spacing
Expand Down
22 changes: 1 addition & 21 deletions src/commands/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@ import {
updateWorkspaceLink,
validateWorkspaceNameForSetup,
} from './workspace/operations.js';
import {
selectWorkspaceForCommand,
selectWorkspaceRootForCommand,
} from './workspace/selection.js';
import { selectWorkspaceForCommand } from './workspace/selection.js';
import {
launchWorkspaceOpenCommand,
} from './workspace/open.js';
Expand Down Expand Up @@ -671,15 +668,6 @@ class WorkspaceCommand {
}
}

async updateRoot(workspaceRoot: string, options: WorkspaceUpdateOptions = {}): Promise<void> {
try {
const selected = await selectWorkspaceRootForCommand(workspaceRoot);
await this.updateSelected(selected, options);
} catch (error) {
this.handleFailure(options.json, { workspace: null, workspace_skills: null, status: [] }, error);
}
}

private async updateSelected(
selected: SelectedWorkspace,
options: WorkspaceUpdateOptions
Expand Down Expand Up @@ -796,14 +784,6 @@ export async function runWorkspaceUpdate(
await workspaceCommand.update(positionalName, options);
}

export async function runWorkspaceUpdateForRoot(
workspaceRoot: string,
options: WorkspaceUpdateOptions = {}
): Promise<void> {
const workspaceCommand = new WorkspaceCommand();
await workspaceCommand.updateRoot(workspaceRoot, options);
}

export function registerWorkspaceCommand(program: Command): void {
registerWorkspaceCommandWith(program, new WorkspaceCommand());
}
2 changes: 1 addition & 1 deletion src/commands/workspace/context-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export async function collectWorkspaceContextStatuses(
target: 'workspace.context.store',
fix: context.store.selector.kind === 'registry'
? 'openspec context-store doctor'
: `Check the path in workspace.yaml or run openspec initiative show ${initiativeId} ${selector}`,
: `Check the path in .openspec-workspace/view.yaml or run openspec initiative show ${initiativeId} ${selector}`,
}
),
];
Expand Down
2 changes: 1 addition & 1 deletion src/commands/workspace/open-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ async function resolveStoredWorkspaceInitiative(
target: 'workspace.context.store',
fix: context.store.selector.kind === 'registry'
? 'openspec context-store doctor'
: 'Check the path in workspace.yaml.',
: 'Check the path in .openspec-workspace/view.yaml.',
}
);
}
Expand Down
6 changes: 3 additions & 3 deletions src/commands/workspace/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ function localStateInvalidStatus(error: unknown): WorkspaceStatus {
`Machine-local paths could not be read: ${asErrorMessage(error)}`,
{
target: 'workspace.local_state',
fix: 'Repair workspace.yaml, then run openspec workspace relink <name> <path> for affected links.',
fix: 'Repair .openspec-workspace/view.yaml, then run openspec workspace relink <name> <path> for affected links.',
}
);
}
Expand Down Expand Up @@ -433,7 +433,7 @@ export async function loadWorkspaceForDoctor(
`Workspace state could not be read: ${asErrorMessage(error)}`,
{
target: 'workspace.root',
fix: 'Repair .openspec-workspace/workspace.yaml before using this workspace.',
fix: 'Repair .openspec-workspace/view.yaml before using this workspace.',
}
),
],
Expand Down Expand Up @@ -523,7 +523,7 @@ async function readWorkspaceViewForMutation(selected: SelectedWorkspace): Promis
'workspace_state_invalid',
{
target: 'workspace.state',
fix: 'Repair workspace.yaml before using this workspace.',
fix: 'Repair .openspec-workspace/view.yaml before using this workspace.',
}
);
}
Expand Down
20 changes: 0 additions & 20 deletions src/commands/workspace/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,26 +78,6 @@ export async function selectedWorkspaceFromRoot(
};
}

export async function selectWorkspaceRootForCommand(
workspaceRoot: string
): Promise<SelectedWorkspace> {
const entries = await listKnownWorkspaceEntries();
const currentWorkspaceRoot = await findWorkspaceRoot(workspaceRoot);

if (!currentWorkspaceRoot) {
throw new WorkspaceCliError(
`No OpenSpec workspace found at '${workspaceRoot}'.`,
'workspace_not_found',
{
target: 'workspace.root',
fix: 'Pass a path inside an OpenSpec workspace.',
}
);
}

return selectedWorkspaceFromRoot(currentWorkspaceRoot, entries);
}

export async function selectWorkspaceForCommand(
options: WorkspaceSelectionOptions,
commandName: string,
Expand Down
4 changes: 2 additions & 2 deletions src/core/workspace/foundation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import { FileSystemUtils } from '../../utils/file-system.js';

export const WORKSPACE_METADATA_DIR_NAME = '.openspec-workspace';
export const WORKSPACE_VIEW_STATE_FILE_NAME = 'workspace.yaml';
export const WORKSPACE_VIEW_STATE_FILE_NAME = 'view.yaml';
export const WORKSPACE_CHANGES_DIR_NAME = 'changes';
export const WORKSPACE_CODE_WORKSPACE_EXTENSION = '.code-workspace';

Expand Down Expand Up @@ -77,7 +77,7 @@ export function getWorkspaceMetadataDir(workspaceRoot: string): string {
}

export function getWorkspaceViewStatePath(workspaceRoot: string): string {
return joinWorkspacePath(workspaceRoot, WORKSPACE_VIEW_STATE_FILE_NAME);
return joinWorkspacePath(getWorkspaceMetadataDir(workspaceRoot), WORKSPACE_VIEW_STATE_FILE_NAME);
}
Comment on lines 79 to 81

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1) Any code still reading a root-level workspace.yaml (outside .openspec-workspace/)?
rg -nP -C3 "workspace\.yaml" --type=ts -g '!**/*.test.ts'

# 2) Any migration/backfill for relocating legacy root state?
rg -nP -C4 '(migrat|backfill|legacyRoot|rootViewState|relocat)' --type=ts -g 'src/**'

# 3) How getWorkspaceViewStatePath / legacy shared path are consumed across src.
rg -nP -C3 '(getWorkspaceViewStatePath|getWorkspaceLegacySharedStatePath)' --type=ts -g 'src/**'

Repository: Fission-AI/OpenSpec

Length of output: 22057


🏁 Script executed:

#!/bin/bash
# Find any root-level filesystem probing for "workspace.yaml" (not under .openspec-workspace)
rg -nP --type=ts '(workspaceRoot\s*,\s*["'\'']workspace\.yaml["'\'']|joinWorkspacePath\([^)]*workspaceRoot[^)]*["'\'']workspace\.yaml["'\'']|joinPath\([^)]*workspaceRoot[^)]*["'\'']workspace\.yaml["'\'']|path.*workspace\.yaml)' src test

# Inspect workspace root detection/read path logic around view + legacy workspace.yaml
rg -nP --type=ts 'function readWorkspaceViewState|function isWorkspaceRoot|getWorkspaceLegacySharedStatePath|getWorkspaceViewStatePath' src/core/workspace/state-io.ts src/core/workspace/foundation.ts src/core/workspace/legacy-state.ts

# Locate the test mentioned in the original comment
rg -nP 'ignores unmarked root view state' test --type=ts
rg -nP 'unmarked root view state' test --type=ts
rg -nP 'root view state' test --type=ts

Repository: Fission-AI/OpenSpec

Length of output: 3112


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search in src for any reads/writes/migrations involving root-level workspace.yaml and view.yaml
rg -nP --type=ts 'workspace\.yaml' src/core src/commands | head -n 200

rg -nP --type=ts 'view\.yaml|WORKSPACE_VIEW_STATE_FILE_NAME|WORKSPACE_LEGACY_SHARED_STATE_FILE_NAME|getWorkspaceViewStatePath|getWorkspaceLegacySharedStatePath' src/core src/commands | head -n 200

# Look specifically for "migration" or "backfill" that mentions workspace state.
rg -nP --type=ts '(migrat|backfill|legacyRoot|root view state|relocat|orphan)' src/core/workspace src/core src/commands | head -n 200

# Inspect the relevant test file around the "ignores unmarked root view state" case.
sed -n '200,340p' test/core/workspace/foundation.test.ts

Repository: Fission-AI/OpenSpec

Length of output: 10675


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show relevant constants + functions for workspace metadata dir + view state path
sed -n '1,140p' src/core/workspace/foundation.ts
echo '---'
sed -n '1,120p' src/core/workspace/legacy-state.ts
echo '---'
sed -n '1,230p' src/core/workspace/state-io.ts

echo '---'
# Search for any code that might copy/migrate root-level workspace.yaml into .openspec-workspace
rg -nP --type=ts "workspaceRoot.*workspace\.yaml|path\.join\([^)]*workspaceRoot[^)]*workspace\.yaml|joinWorkspacePath\([^)]*workspaceRoot[^)]*workspace\.yaml|\.openspec-workspace'.workspace\.yaml|copyFile.*workspace\.yaml|rename.*workspace\.yaml|move.*workspace\.yaml|relocat|migrat" src/core src/commands
echo '---'
# Search for init/update referencing workspace state migration
rg -nP --type=ts "writeWorkspaceViewState|readWorkspaceViewState|readWorkspaceViewStateSync|workspace\.yaml|view\.yaml|relink|repair .*view\.yaml|orphan" src/core src/commands

Repository: Fission-AI/OpenSpec

Length of output: 20247


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Any non-constant usage of root-level workspace.yaml inside src (not tests)
rg -nP --type=ts "workspace\.yaml" src/core/workspace src/commands/workspace src/core/planning-home.ts src/core/registry* 2>/dev/null || true

# 2) Any direct mentions of root-level state backfill/migration in workspace area
rg -nP --type=ts "(migrat|backfill|relocat|legacy).*(workspace|view|state)" src/core/workspace src/commands/workspace || true

# 3) Any code that reads/writes legacy shared state file or workspace view state beyond state-io
rg -nP --type=ts "(getWorkspaceLegacySharedStatePath|readWorkspaceViewState|writeWorkspaceViewState|workspaceStatePartsToViewState|WORKSPACE_LEGACY_SHARED_STATE_FILE_NAME|getWorkspaceViewStatePath)" src/core/workspace src/commands/workspace || true

# 4) Inspect registry + managed workspace root detection
sed -n '1,260p' src/core/workspace/registry.ts || true
sed -n '260,520p' src/core/workspace/registry.ts || true

Repository: Fission-AI/OpenSpec

Length of output: 12045


Add/confirm migration/backfill for legacy root-level workspace.yaml so managed/registry workspaces aren’t orphaned

  • isWorkspaceRoot / readWorkspaceViewState* only probe .openspec-workspace/view.yaml and .openspec-workspace/workspace.yaml (not <root>/workspace.yaml), and managed workspace enumeration (registry.ts) relies on that same detection.
  • The test suite enforces the behavior that root-level workspace.yaml is ignored even when its contents look OpenSpec-shaped.
  • Please ensure there’s an explicit backfill/migration for known managed/registry workspaces (or document the behavior) so existing beta users don’t silently lose their workspace state.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/workspace/foundation.ts` around lines 79 - 81, Tests and registry
currently ignore legacy root-level workspace.yaml, so implement an explicit
migration that detects a root-level "workspace.yaml" and backfills it into the
managed metadata directory used by
getWorkspaceViewStatePath/getWorkspaceMetadataDir (or otherwise imports it into
the same shape read by isWorkspaceRoot and readWorkspaceViewState*), and ensure
registry enumeration in registry.ts picks up those migrated workspaces; add a
startup migration function (e.g., migrateLegacyRootWorkspaceYaml) that runs
before workspace enumeration and moves/parses root-level workspace.yaml into
.openspec-workspace/workspace.yaml (or writes the normalized view.yaml) for
known managed/registry workspaces, and update
isWorkspaceRoot/readWorkspaceViewState*/registry.ts to treat the migrated file
as authoritative.


export function getWorkspaceChangesDir(workspaceRoot: string): string {
Expand Down
3 changes: 1 addition & 2 deletions src/core/workspace/legacy-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { z } from 'zod';

import {
WORKSPACE_METADATA_DIR_NAME,
WORKSPACE_VIEW_STATE_FILE_NAME,
getWorkspaceMetadataDir,
parseWorkspaceViewState,
validateWorkspaceLinkName,
Expand All @@ -16,7 +15,7 @@ import {
} from './foundation.js';
import { FileSystemUtils } from '../../utils/file-system.js';

export const WORKSPACE_LEGACY_SHARED_STATE_FILE_NAME = WORKSPACE_VIEW_STATE_FILE_NAME;
export const WORKSPACE_LEGACY_SHARED_STATE_FILE_NAME = 'workspace.yaml';
export const WORKSPACE_LEGACY_LOCAL_STATE_FILE_NAME = 'local.yaml';
export const WORKSPACE_LEGACY_LOCAL_STATE_IGNORE_PATTERN =
`${WORKSPACE_METADATA_DIR_NAME}/${WORKSPACE_LEGACY_LOCAL_STATE_FILE_NAME}`;
Expand Down
2 changes: 1 addition & 1 deletion src/core/workspace/open-surface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ This directory is an OpenSpec workspace: a local working view over context store
- Use repo-local OpenSpec changes for implementation plans owned by a repo or team.
- Use linked repos and folders to inspect context, understand ownership, and make edits in the place that owns the work.
- Keep workspace-local files focused on local paths, opener state, agent setup, and other machine-specific view state.
- Use OpenSpec workspace commands instead of hand-editing \`workspace.yaml\`.
- Use OpenSpec workspace commands instead of hand-editing \`.openspec-workspace/view.yaml\`.
- If this workspace contains legacy or beta workspace-level planning files, treat them as compatibility context unless the user explicitly asks to use that beta flow.`;

export interface WorkspaceOpenResolvedContext {
Expand Down
9 changes: 5 additions & 4 deletions src/core/workspace/state-io.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as path from 'node:path';
import { FileSystemUtils } from '../../utils/file-system.js';
import {
getWorkspaceChangesDir,
getWorkspaceMetadataDir,
getWorkspaceViewStatePath,
parseWorkspaceViewState,
serializeWorkspaceViewState,
Expand Down Expand Up @@ -162,10 +163,10 @@ export async function writeWorkspaceViewState(
workspaceRoot: string,
state: WorkspaceViewState
): Promise<void> {
await FileSystemUtils.writeFile(
getWorkspaceViewStatePath(workspaceRoot),
serializeWorkspaceViewState(state)
);
const content = serializeWorkspaceViewState(state);

await FileSystemUtils.createDirectory(getWorkspaceMetadataDir(workspaceRoot));
await FileSystemUtils.writeFile(getWorkspaceViewStatePath(workspaceRoot), content);
}

export async function workspaceChangesDirExists(workspaceRoot: string): Promise<boolean> {
Expand Down
2 changes: 1 addition & 1 deletion test/commands/workspace-initiative-open.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ describe('workspace open initiative views', () => {
},
})
);
expect(fs.existsSync(path.join(workspaceRoot, '.openspec-workspace'))).toBe(false);
expect(fs.existsSync(path.join(workspaceRoot, '.openspec-workspace'))).toBe(true);
expect(fs.existsSync(path.join(globalDataDir, 'workspaces', 'registry.yaml'))).toBe(false);
expect(fs.readFileSync(path.join(workspaceRoot, 'AGENTS.md'), 'utf-8')).toContain(
'Initiative title: Billing Launch'
Expand Down
Loading
Loading