From 7fa463b851e16a10fe39bcac956272b7597e2ed9 Mon Sep 17 00:00:00 2001 From: TabishB Date: Wed, 13 May 2026 23:21:49 +1000 Subject: [PATCH 01/14] Propose workspace change planning --- .../candidate-requirements.md | 178 ++ .../deep-dive-requirements.html | 1660 +++++++++++++++++ .../workspace-change-planning/design.md | 130 ++ .../exploration-summary.md | 337 ++++ .../changes/workspace-change-planning/prd.md | 275 +++ .../workspace-change-planning/proposal.md | 31 +- .../review-candidate-requirements.html | 868 +++++++++ .../specs/artifact-graph/spec.md | 24 + .../specs/change-creation/spec.md | 35 + .../specs/cli-artifact-workflow/spec.md | 91 + .../specs/openspec-conventions/spec.md | 32 + .../specs/schema-resolution/spec.md | 25 + .../specs/workspace-change-planning/spec.md | 59 + .../specs/workspace-links/spec.md | 81 + .../workspace-change-planning/tasks.md | 53 + 15 files changed, 3870 insertions(+), 9 deletions(-) create mode 100644 openspec/changes/workspace-change-planning/candidate-requirements.md create mode 100644 openspec/changes/workspace-change-planning/deep-dive-requirements.html create mode 100644 openspec/changes/workspace-change-planning/design.md create mode 100644 openspec/changes/workspace-change-planning/exploration-summary.md create mode 100644 openspec/changes/workspace-change-planning/prd.md create mode 100644 openspec/changes/workspace-change-planning/review-candidate-requirements.html create mode 100644 openspec/changes/workspace-change-planning/specs/artifact-graph/spec.md create mode 100644 openspec/changes/workspace-change-planning/specs/change-creation/spec.md create mode 100644 openspec/changes/workspace-change-planning/specs/cli-artifact-workflow/spec.md create mode 100644 openspec/changes/workspace-change-planning/specs/openspec-conventions/spec.md create mode 100644 openspec/changes/workspace-change-planning/specs/schema-resolution/spec.md create mode 100644 openspec/changes/workspace-change-planning/specs/workspace-change-planning/spec.md create mode 100644 openspec/changes/workspace-change-planning/specs/workspace-links/spec.md create mode 100644 openspec/changes/workspace-change-planning/tasks.md diff --git a/openspec/changes/workspace-change-planning/candidate-requirements.md b/openspec/changes/workspace-change-planning/candidate-requirements.md new file mode 100644 index 000000000..d1a3b62af --- /dev/null +++ b/openspec/changes/workspace-change-planning/candidate-requirements.md @@ -0,0 +1,178 @@ +# Candidate Requirements + +Date: 2026-05-11 + +Status: exploratory requirements notes. These capture requirements that emerged from discussion, but they are not yet finalized OpenSpec spec deltas. + +## Requirement: Clear Workspace Planning Framing + +Workspace change planning should be framed around helping users and agents plan work across linked repos or folders. + +The user should understand: + +- where to stand +- what the workspace can see +- where the plan will be captured +- when implementation begins + +The feature should be described by the planning experience it enables, not primarily by the POC behavior it avoids. + +## Requirement: Workspace As Planning Home + +A workspace change should capture the shared plan for a cross-area effort. + +The workspace should provide a planning home for: + +- goals +- decisions +- coordination tasks +- affected areas +- cross-area risks and dependencies + +Repo-local execution should begin only when the user or agent moves into an implementation workflow for a selected area. + +## Requirement: Preserve The Familiar OpenSpec Workflow + +Workspace mode should preserve the familiar OpenSpec workflow vocabulary. + +Users should still be able to think in terms of: + +- explore +- propose +- apply +- verify +- archive + +Workspace context should change paths, scope, and allowed actions, not require a separate user-facing workflow family. + +## Requirement: Avoid Workspace-Specific Skill Duplication + +OpenSpec should not require separate workspace-specific versions of every workflow skill. + +OpenSpec should maintain one conceptual workflow skill per workflow, such as: + +- `openspec-propose` +- `openspec-explore` +- `openspec-apply-change` +- `openspec-verify-change` +- `openspec-archive-change` + +Each workflow skill should discover whether it is operating in repo-local or workspace mode instead of being duplicated by mode. + +Workspace-specific behavior should come from CLI-reported context, not from a parallel set of workspace-only skills. + +## Requirement: Separate Agent Affordances From Workflow Semantics + +OpenSpec workflow instructions should describe the OpenSpec product workflow. + +Agent-specific instructions should describe how the current coding agent performs common actions, such as: + +- asking the user questions +- tracking todos +- delegating to subagents +- attaching directories +- handling agent-specific session constraints + +Workflow skills should reference or use an agent affordance layer instead of repeating agent-specific instructions in every workflow. + +## Requirement: Status JSON Provides Agent Context + +`openspec status --change --json` should provide enough context for an agent to understand the current change and act safely. + +Status JSON should tell the agent: + +- whether the change is repo-local or workspace-scoped +- where the change artifacts live +- which artifact paths are relevant +- what should happen next +- which areas are affected or unresolved +- which edit roots are allowed for implementation +- whether the next action needs an area selection + +Agents should not need to assume that changes always live under `openspec/changes//`. + +## Requirement: Use Plain Action Language + +OpenSpec should use simple terms for agent-facing action context. + +Preferred terms: + +- `nextSteps`: what should happen next +- `actionContext`: paths, areas, and constraints needed to act + +Avoid exposing implementation-oriented terms such as "workflow affordances" in user-facing docs or JSON fields. + +## Requirement: Apply Means Implement + +`/apply` should start or continue implementation work for an already planned change. + +In repo-local mode, `/apply` should: + +- read the repo-local change artifacts +- identify pending implementation tasks +- implement the pending tasks in that repo +- update task progress as implementation work is completed + +In workspace mode, `/apply` should: + +- select or confirm one affected area when needed +- read the workspace planning context for that area +- identify the implementation root for that area +- implement only inside the allowed repo or folder +- update the relevant task progress as work is completed + +If a workspace change has multiple affected areas and the user did not specify one, `/apply` should ask which area to implement. + +## Requirement: Slice Means Delivery Increment + +`slice` should refer to a delivery increment inside a larger change. + +A slice should help users split a large effort into smaller planned parts that can be: + +- designed +- implemented +- reviewed +- verified +- shipped + +Repo or folder ownership should use a different term so delivery breakdown and ownership boundaries remain distinct. + +## Requirement: Area Means Ownership Or Execution Boundary + +`area` should describe where ownership or implementation happens. + +Examples of areas: + +- repo +- package +- folder +- service +- app +- docs site + +A small workspace change may have several areas and one implicit slice. + +A large workspace change may have several slices, and each slice may affect one or more areas. + +OpenSpec should be able to report affected areas without forcing users to think of those areas as delivery slices. + +## Requirement: Keep Unsettled Direction In Exploratory Notes + +Open questions and unsettled direction should live in exploratory notes until they are ready to become proposal, design, or spec content. + +`proposal.md` should be updated when the scope and intended behavior are clear enough to propose. + +`design.md` should be created or updated when technical decisions are selected, not while the team is still comparing models. + +## Requirement: Preserve Exploration Context + +The workspace change planning exploration should preserve: + +- key findings +- candidate requirements +- open questions +- candidate terminology +- POC lessons +- rejected or risky directions + +Future work on `workspace-change-planning` should be able to use these notes without mistaking them for final requirements. diff --git a/openspec/changes/workspace-change-planning/deep-dive-requirements.html b/openspec/changes/workspace-change-planning/deep-dive-requirements.html new file mode 100644 index 000000000..0f1ea53ba --- /dev/null +++ b/openspec/changes/workspace-change-planning/deep-dive-requirements.html @@ -0,0 +1,1660 @@ + + + + +Deep Dive: Workspace Change Planning Requirements + + + + + + +
+
+

Workspace Change Planning — Deep Dive

+
openspec/changes/workspace-change-planning/candidate-requirements.md · 12 requirements
+
+ +
+ +
+ + +
+
+
+
Requirement 1 of 12
+

+
+
+ +
+ + + + + + + +
+ +
+
+
+
+
+
+
+
+
+
+
    +
    +
    +
      +
      +
      +
      +
      +
      +
      +
      Acceptance Signal
      +
      +
      +
      +
      +
      +
      + +
      +
      + Your read +
      + + + + +
      + + + ← → nav · 1-7 tabs · esc to clear note +
      +
      + +
      Copied!
      + + + + diff --git a/openspec/changes/workspace-change-planning/design.md b/openspec/changes/workspace-change-planning/design.md new file mode 100644 index 000000000..0b651cde0 --- /dev/null +++ b/openspec/changes/workspace-change-planning/design.md @@ -0,0 +1,130 @@ +## Context + +Workspace setup already creates a planning home, records linked repos or folders, stores a preferred opener, and maintains the root open surface. For workspace change planning to work in practice, the opened agent also needs OpenSpec workflow skills available from that workspace root. + +Repo-local `openspec init` and `openspec update` already provide the user model for choosing agent surfaces and generating skills. Workspace setup should feel similar, but the installation target is the workspace root rather than any linked repo or folder. + +The existing artifact workflow assumes a change lives under a repo-local `openspec/changes/` path. Workspace planning needs the same workflow vocabulary, but the planning home may be a workspace root and the implementation homes may be linked repos or folders. + +## Goals / Non-Goals + +**Goals:** +- Install OpenSpec agent skills into the workspace root during workspace setup. +- Let users choose which agents receive skills with familiar `--tools` semantics. +- Let users refresh, add, or remove workspace-local skills later through `workspace update`. +- Add a built-in workspace planning schema for workspace-scoped changes. +- Create workspace changes under the workspace planning path. +- Represent affected areas without forcing implementation artifacts into linked repos. +- Give agents machine-readable planning context through status/instructions output. +- Preserve the workspace boundary: linked repos and folders remain untouched during setup/update. + +**Non-Goals:** +- Generating slash commands as part of workspace setup. +- Installing skills into linked repos or folders. +- Solving workspace-scoped artifact path discovery in the first setup-skill step. +- Adding a separate artifact-context CLI command in the first version. +- Implementing workspace apply, verify, or archive semantics end to end. +- Changing repo-local `openspec init` or `openspec update` behavior. + +## Decisions + +### Use agent-skill language in workspace UX + +Workspace setup should ask, "Which agents should get OpenSpec skills in this workspace?" rather than using the broader "AI tools" wording. The user-visible action is installing skills for coding agents, and the target is the workspace planning home. + +Alternative considered: reuse the exact `init` wording. That would be familiar, but it hides the important distinction between opening a workspace and installing skills into it. + +### Reuse the existing tool id model + +The CLI should use the existing `--tools all|none|` grammar for non-interactive setup and update. Reusing the existing tool IDs avoids inventing a second naming system for the same configured agents. + +Alternative considered: add `--agents`. That reads better in isolation, but it creates unnecessary parallel vocabulary next to `openspec init --tools`. + +### Preselect the preferred opener when possible + +Interactive setup should preselect the preferred opener when that opener maps to a skill-capable agent. The user can accept the default, add more agents, or deselect it. + +Alternative considered: install skills only for the preferred opener. That is simpler, but opener choice means "how should I open this workspace" while skill selection means "which agents should understand OpenSpec here." + +### Generate workspace-local skills only + +Workspace setup/update should generate skills under the workspace root, such as `.codex/skills/` or `.claude/skills/`. It should not generate slash commands in this slice because some command adapters resolve to global locations, and workspace setup should remain local and predictable. + +Alternative considered: mirror `init` exactly and generate both skills and commands. That risks surprising global writes and makes the setup boundary harder to explain. + +### Add `workspace update` for skill refresh + +`openspec workspace update` should refresh, add, or remove workspace-local OpenSpec skills after setup. It should resolve the current workspace when run from inside a workspace, and also support named and non-interactive forms. + +Alternative considered: reuse `openspec update` from inside the workspace. That command currently means repo/project update, while workspace update needs workspace selection, workspace JSON/status behavior, and linked-repo safety rules. + +### Resolve a planning home before acting + +Workflow commands should resolve whether the current change belongs to a repo-local planning home or a workspace planning home before computing paths. The resolver should identify the planning root, change root, linked areas when present, and whether implementation edits are allowed. + +Alternative considered: add workspace-specific command branches wherever paths are used. That would make the workspace model leak into every workflow and make generated skills more fragile. + +### Store workspace changes in the workspace planning path + +Workspace changes should live under the workspace planning path, initially `changes/` at the workspace root. Creating the workspace change should capture shared intent once and may record affected areas, but it should not create repo-local `openspec/changes/` directories in linked repos. + +Alternative considered: materialize a repo-local change in every affected repo during workspace change creation. That was easy to reason about in the POC, but it commits too early and makes exploration look like implementation. + +### Add a workspace planning schema + +Workspace-scoped changes should use a built-in `workspace-planning` schema by default. This keeps the workflow verbs familiar while letting workspace changes have a structure that fits cross-area planning. + +Initial artifact shape: + +```text +changes// + .openspec.yaml # schema: workspace-planning + proposal.md # shared goal and scope + areas.md # known, suspected, and unresolved affected areas + design.md # cross-area decisions + tasks.md # coordination and planning tasks +``` + +The first schema should stay intentionally small. Area-specific folders can come later once scoped artifact paths and apply semantics are stable. + +Alternative considered: reuse `spec-driven` and make all workspace differences implicit in status output. That hides the fact that workspace planning needs an affected-area artifact and different instructions from repo-local spec work. + +Alternative considered: create separate workspace workflow skills instead of a schema. That would duplicate workflow guidance and make workspace mode feel like a different product. + +### Use affected areas, not targets or repo slices + +The planning model should call ownership or implementation boundaries "affected areas." Affected areas can start with registered workspace link names, but the language should leave room for folders, packages, services, apps, or docs sites. Delivery breakdown remains a separate concept and should not be called an area. + +Alternative considered: keep "targets" because it maps to the old POC flag. That term is implementation-first and encourages users to choose repos before the plan is clear. + +### Make status JSON the agent context contract + +`openspec status --change --json` should become the primary source of machine-readable action context. It should include the planning home, change root, concrete artifact paths, affected areas, next steps, and constraints such as allowed edit roots when implementation is later in scope. + +Alternative considered: create a separate context command immediately. Status is already used by generated workflow skills, so enriching it first gives agents a single place to look. + +### Keep generated skills path-agnostic + +Generated workflow skills should ask OpenSpec where artifacts live instead of embedding repo-local paths such as `openspec/changes/`. The standard skill pattern should be: + +```text +1. Run `openspec status --change "" --json`. +2. Use the returned planning home, artifacts, next steps, and action context. +3. Run `openspec instructions --change "" --json` before writing an artifact. +4. Write to the resolved path returned by the CLI. +``` + +This keeps the same skill usable in repo-local and workspace-scoped changes. If status/instructions output later becomes too crowded, a separate context command can be introduced in a future change without changing the high-level skill rule. + +Alternative considered: add a new `openspec context` command now. That may become useful, but it adds a new surface before we have proven that enriched status/instructions are insufficient. + +## Risks / Trade-offs + +- Skill generation logic may drift from `init/update` → share the same template generation and tool validation helpers where practical. +- Removing unselected skills could remove user-modified files → remove only known OpenSpec-managed workflow skill directories by explicit workflow list. +- `--tools` is less precise than `--agents` in workspace UX → keep `--tools` for CLI consistency, but use "agents" in prompts and human output. +- Existing generated skills still contain repo-local path assumptions → handle that as a later artifact-context step after workspace-local skills can be installed. +- Status JSON may become too broad → keep fields plain and action-oriented, such as `planningHome`, `artifacts`, `affectedAreas`, `nextSteps`, and `actionContext`. +- Affected area discovery may be ambiguous → start with explicit registered workspace links and allow later refinement instead of parsing free-form Markdown headings as the only source of truth. +- A new schema can drift from repo-local workflow expectations → keep artifact IDs plain and make status/instructions carry the schema-specific paths. +- Skill instructions may lag behind CLI behavior → audit source workflow templates for hardcoded repo-local paths and replace them with the path-agnostic status/instructions pattern. diff --git a/openspec/changes/workspace-change-planning/exploration-summary.md b/openspec/changes/workspace-change-planning/exploration-summary.md new file mode 100644 index 000000000..96d117578 --- /dev/null +++ b/openspec/changes/workspace-change-planning/exploration-summary.md @@ -0,0 +1,337 @@ +# Workspace Change Planning Exploration Summary + +Date: 2026-05-11 + +Status: exploratory notes. This file records open thinking from the session. It does not represent a final product direction. + +## Why This Discussion Happened + +The active `workspace-change-planning` proposal still contains framing from the workspace POC era, especially the phrase "without immediately materializing repo-local artifacts." That framing is confusing in the current reimplementation because workspace setup/open already separates repo visibility from implementation. + +The real problem space is broader: + +- users need to plan changes that may span linked repos or folders +- agents need to explore across the workspace before committing to a plan shape +- OpenSpec should avoid making workspace mode feel like a second product with duplicated workflow skills +- large changes may need to be broken down into smaller parts without becoming many unrelated changes + +## Workspace POC Findings + +The workspace POC branch in PR #1006 changed `openspec new change` to behave differently inside a workspace. + +Outside a workspace, the command behaved like the normal repo-local flow: + +```bash +openspec new change add-auth +``` + +created: + +```text +openspec/changes/add-auth/ + .openspec.yaml +``` + +Inside a workspace, the POC required explicit targets: + +```bash +openspec new change add-3ds --targets api,web +``` + +created: + +```text +workspace/ + changes/add-3ds/ + .openspec.yaml # schema, created, targets + proposal.md + design.md + tasks/ + coordination.md + targets/ + api/ + tasks.md + specs/ + web/ + tasks.md + specs/ +``` + +Important POC behavior: + +- workspace changes lived at top-level `changes//`, not repo-local `openspec/changes//` +- `--targets` was required inside a workspace +- `--targets` was rejected outside a workspace +- target aliases had to be registered workspace repos +- no files were created inside registered repos during change creation +- `openspec apply --change --repo ` later materialized one target's planning artifacts into a repo-local change +- normal OpenSpec skills were not updated to understand this new structure + +The POC behavior was documented on the POC branch in: + +- `WORKSPACE_POC_PRD.md` +- `WORKSPACE_POC_DECISION_RECORD.md` +- `WORKSPACE_POC_FOLLOWUP_NOTES.md` +- `docs/workspace.md` +- `docs/workspace-demo.md` +- `notes/workspace-poc/phase-05-targeted-change-create/SUMMARY.md` +- `notes/workspace-poc/phase-10-materialization-contract-research/DECISION.md` + +The most important follow-up note was that the POC proved central workspace planning can work, but it also pushed target selection and materialization too early. + +## Skill Complexity Concern + +A major concern is avoiding a combinatorial explosion in generated skills. + +Bad direction: + +```text +workflow x agent x workspace-mode +``` + +For example, if OpenSpec has several workflow skills, several coding-agent-specific instruction variants, and then adds workspace-specific variants on top, the number of skill files grows quickly. + +The preferred architectural pressure is: + +```text +workflow skill + agent affordance profile + OpenSpec-provided context +``` + +Meaning: + +- keep one conceptual `openspec-propose`, `openspec-apply-change`, `openspec-verify-change`, etc. +- keep agent-specific affordances separate, such as how to ask user questions, use todos, use subagents, or attach directories +- have the OpenSpec CLI tell the skill whether it is in repo-local mode or workspace mode, where artifacts live, what areas are relevant, and what action is safe next + +Current skills that would likely need updates if workspace planning proceeds: + +- `openspec-propose` +- `openspec-explore` +- `openspec-apply-change` +- `openspec-verify-change` +- `openspec-archive-change` +- `openspec-bulk-archive-change` later, or keep it repo-local until workspace archive semantics are stable + +The key instruction these skills may need: + +```text +Run `openspec status --change --json`. +Use the returned paths, areas, next steps, and action context. +Do not assume changes live under `openspec/changes/`. +``` + +## `openspec status --json` As Agent Context + +One idea that seemed promising was to use `openspec status --change --json` as the existing CLI surface that tells agents how to act. + +Instead of introducing a separate context command immediately, status could report: + +- whether the current change is repo-local or workspace-scoped +- where the change artifacts live +- what the next steps are +- which areas are affected or unresolved +- which task/spec/design files should be read or updated +- what edit roots are allowed for implementation +- whether apply needs an area selection + +Use simpler language: + +- `nextSteps`: what should happen next +- `actionContext`: paths, areas, and constraints needed to act + +Avoid terms such as "workflow affordances" in user-facing docs or JSON fields. + +## Open Question: Should Targets Exist? + +The session questioned whether users need to explicitly set targets at all. + +Reason to have targets: + +- focused `workspace open --change ` +- per-area status roll-up +- apply or implementation of one area at a time +- unresolved path reporting +- avoiding accidental edits outside the intended area +- future verify/archive behavior + +Reason to avoid explicit target-setting: + +- target selection can be premature before exploration +- it adds bookkeeping for agents +- the plan itself should reveal the affected areas + +Potential direction under discussion: + +```text +derive affected areas from planning artifacts instead of requiring a separate target-setting step +``` + +Possible derivation sources: + +- structured area folders +- structured slice/area folders +- explicit sections in tasks or design +- optional metadata as a cache or confirmation marker, validated against artifacts + +Important caution: Markdown task headings alone are probably too ambiguous to be the only source of truth. + +## Terminology Under Discussion + +The session identified a naming conflict around "slice." + +There are two different concepts: + +```text +delivery breakdown = a smaller planned part of a large change +ownership/execution boundary = a repo, folder, package, or system area affected by the change +``` + +Calling both of these "slices" would make the model confusing. + +Working vocabulary under discussion: + +```text +change = the overall user-visible planning boundary +slice = a delivery increment inside a large change +area = an ownership or execution boundary, such as a repo or folder +unit = one area inside one slice, when implementation needs that precision +artifact = a proposal, design, task list, spec delta, or note attached to a scope +``` + +Examples: + +```text +Small repo-local work: + change only + implicit area = current repo + implicit slice = main + +Small workspace work: + change = add-3ds + areas = contracts, billing, web + slice = main + +Large workspace work: + change = workspace-reimplementation + slices = setup, open, change-planning, apply, verify-archive + areas = cli, agent-skills, docs + units = setup/cli, open/agent-skills, etc. +``` + +This vocabulary is not decided. It is a candidate model for avoiding confusion. + +## Artifact Scoping Problem + +A fixed tree where every change, slice, and area gets every artifact would be too heavy. + +Artifacts should attach to the scope where they are useful. + +Proposal: + +- usually belongs at the change level +- summarizes why the work exists and what outcome is intended +- most areas or slices do not need their own proposal +- a slice-level proposal might exist only if that slice needs independent approval or has a materially distinct "why" + +Design: + +- should live at the broadest scope where the decision applies +- change-level design for cross-area architecture, sequencing, contracts, and tradeoffs +- slice-level design for one independently complex delivery increment +- area-level design for local implementation choices +- unit-level design only when one area within one slice is complex enough + +Tasks: + +- can exist at multiple scopes +- change-level tasks for coordination, decisions, rollout, and review gates +- slice-level tasks for a delivery increment +- area or unit tasks for implementation work an agent can actually complete + +Specs: + +- should live closest to ownership +- area or unit specs for normal behavior owned by a repo or folder +- change-level draft specs only when ownership is unresolved + +This matters because OpenSpec allows users to select which artifacts a change contains. Workspace/slice/area support should not assume every artifact type exists at every scope. + +## Possible File Shapes + +No file shape was decided. + +A future-friendly, fully structured shape might look like: + +```text +changes// + proposal.md + design.md + tasks.md + slices/ + / + tasks.md + design.md + areas/ + / + tasks.md + specs/ +``` + +This may be too much structure for a first implementation. + +A simpler intermediate shape for large changes could be: + +```text +changes// + proposal.md + design.md + tasks.md + slices/ + workspace-open.md + workspace-change-planning.md +``` + +If a slice grows, it could become a folder later. + +For smaller cross-area changes, an area-oriented shape might be enough: + +```text +changes// + proposal.md + design.md + tasks.md + areas/ + api/ + tasks.md + specs/ + web/ + tasks.md + specs/ +``` + +Again, this is exploratory. The point is that artifact scope should be explicit enough for tools and agents, without forcing unnecessary files. + +## Important UX Principles + +The discussion kept returning to these UX principles: + +- workspace mode should preserve the familiar OpenSpec mental model where possible +- users should not need to understand target metadata before they can start planning +- repo visibility is not change commitment +- change creation should not be a transport mechanism just to attach repos +- workspace-specific behavior should not require duplicate skill families +- `/apply` should mean implement, not materialize planning files +- status should help agents know what to do next without hardcoding paths +- large changes should support breakdown into slices, but simple changes should stay simple + +## Open Questions + +- Should `openspec new change ` inside a workspace create only `.openspec.yaml`, or also seed minimal planning artifacts? +- Should `--targets` exist as an optional fast path, compatibility flag, or not at all? +- What structure should represent affected areas without overfitting to repo aliases? +- Should affected areas be derived from folders, metadata, artifact contents, or a combination? +- What is the smallest useful representation of a delivery slice? +- How should artifact schemas express which scopes an artifact type can attach to? +- How should `status --json` expose scoped artifact paths without becoming too large or unstable? +- Which workspace archive semantics are needed before archive skills become workspace-aware? +- How should `apply` select one area and expose allowed edit roots if `/apply` means implementation? diff --git a/openspec/changes/workspace-change-planning/prd.md b/openspec/changes/workspace-change-planning/prd.md new file mode 100644 index 000000000..44c6ad47e --- /dev/null +++ b/openspec/changes/workspace-change-planning/prd.md @@ -0,0 +1,275 @@ +# Workspace Change Planning PRD + +## Summary + +OpenSpec should help people plan one change across one or more repos or folders without making workspace mode feel like a second product. + +The product should center on a simple idea: a change has one shared plan, and that plan can point to the places where work will happen. + +For a normal repo, the plan and the work usually live in the same repo. For a workspace, the plan lives in the workspace and the work happens in one or more linked repos or folders. + +## Problem + +OpenSpec currently treats a change as something that lives under one repo-local `openspec/changes/` folder. + +That works well for a single repo, but it becomes awkward when the user is planning across several repos or folders. The user needs one place to capture the goal, decisions, affected places, risks, and coordination tasks. The user should not have to create repo-local change folders before the plan is clear. + +The current workspace notes also use terms like targets, slices, scopes, and materialization. Those terms describe implementation details more than the user experience. + +## Product Direction + +OpenSpec should make the familiar change workflow work in more places. + +A user should still think in terms of creating a change, exploring it, writing a proposal, applying the work, verifying it, and archiving it. + +The difference is where the plan lives and where implementation is allowed to happen. + +In a repo, the planning home is the repo. In a workspace, the planning home is the workspace. In both cases, the user is still working with a change. + +## What Already Exists + +OpenSpec already has several pieces needed for this direction. + +Workspaces already have a real root folder and a planning path at `changes/`. + +Workspaces can already link repos or folders, and those links already have stable names. + +Workspace state already separates shared workspace information from local machine paths. + +Workspace open already makes linked repos and folders visible to agents before a change exists. + +Workflow schemas already define plan file types, templates, order, and apply rules. + +`openspec status --change --json` already exists. + +`openspec instructions --json` already exists. + +Generated skills already call `status` and `instructions` in important places. + +This means the first step is mostly connection work, not a full new product. + +## What Is Missing + +OpenSpec does not yet have one shared way to answer this question: given this command, which planning home and change should it use? + +That missing layer is the main gap. + +Workspace links are close to affected areas, but changes do not record or report affected areas yet. + +Plan file paths exist in instructions, but only for repo-local change folders. + +Status output exists, but it does not yet explain the planning home, affected areas, allowed edit roots, or next steps clearly enough for agents. + +Apply exists, but it does not yet pick a work focus before editing. + +Workspace verify and archive are not ready yet. + +## Product Language + +| Product term | Meaning | Avoid using | +| --- | --- | --- | +| Change | The overall goal and plan for a feature, fix, project, or other piece of work. | Change Plan | +| Planning home | The place where the shared plan lives. This can be a repo or a workspace. | Planning Surface | +| Affected area | A repo, folder, package, app, service, or docs site touched by the change. | Target, repo slice | +| Phase | A delivery step inside a larger change. Most changes do not need phases. | Slice | +| Work focus | The one affected area, and optional phase, currently being implemented or verified. | Work Unit | +| Plan file | A proposal, design, task list, spec draft, or note attached to the right part of the change. | Scoped Artifact | +| Next step guidance | The CLI output that tells an agent what to read, where to write, and what not to edit. | Action Context | + +## User Experience + +Maya opens a workspace that links `api`, `web`, and `billing`. + +She creates a change called `add-3ds`. + +OpenSpec creates one shared plan in the workspace. The plan describes the goal, the affected areas, the risks, the decisions, and the coordination tasks. + +No files are written inside `api`, `web`, or `billing` just because the shared plan exists. + +Later, Maya says she wants to apply the change to `api`. + +OpenSpec reads the shared plan, confirms the work focus is `api`, and tells the agent that implementation edits are allowed only inside the `api` checkout. + +When `api` is done, Maya can move the work focus to `web` or `billing`. + +When all affected areas are done, OpenSpec can verify and archive the whole change. + +## Requirements + +### A Change Has One Shared Plan + +A workspace change should capture the shared goal once. + +The shared plan should hold the goal, decisions, affected areas, risks, and coordination tasks. + +Creating the shared plan should not create files inside linked repos or folders. + +### Repo Changes Stay Simple + +A repo-local change should keep working as it does today. + +A repo-local change should be treated as the simplest version of the same model, with one planning home and one affected area. + +Existing `openspec/changes/` folders should keep working. + +### Workspaces Add Reach, Not A New Workflow + +Workspace mode should not create separate workflow commands or separate workflow skills. + +Users should still use the same workflow words: explore, propose, apply, verify, and archive. + +The CLI should provide enough next step guidance that agent skills do not need to hardcode where change files live. + +### Affected Areas Come From The Plan + +OpenSpec should support affected areas as first-class plan information. + +An affected area may be a linked repo, a linked folder, a package, an app, a service, or a docs site. + +Workspace links are a strong starting point for affected areas, because they already provide stable names and local paths. + +The product should not force users to pick all affected areas before they have explored the change. + +### Phases Are Optional + +Most changes should not need phases. + +Large changes may use phases when the work needs to be split into delivery steps. + +Phases should describe delivery order, not repo ownership. + +### Plan Files Attach Where They Make Sense + +A proposal usually belongs to the whole change. + +A design should live at the broadest level where the decision applies. + +Tasks may belong to the whole change, one phase, one affected area, or one work focus. + +Specs should live as close as possible to the place that owns the behavior. + +OpenSpec should not force every affected area or phase to have every kind of plan file. + +### Apply Means Start Work + +Applying a change should mean starting or continuing implementation work. + +In a repo-local change, apply should work through the repo-local tasks. + +In a workspace change, apply should pick or confirm a work focus first. + +After the work focus is selected, OpenSpec should tell the agent which plan files to read and which repo or folder may be edited. + +### Status Should Guide Agents + +`openspec status --change --json` should become the main machine-readable guide for agents. + +It should say where the plan lives, which affected areas exist, which plan files matter, what should happen next, and which edit roots are allowed. + +The JSON should use plain names such as `nextSteps` and `actionContext`. + +Agent skills should use this output instead of assuming that every change lives under `openspec/changes/`. + +## Configuration And Extension + +OpenSpec already has a useful extension point through workflow schemas. + +That should remain the main extension path. + +Schemas should grow carefully so they can describe where plan files may live, what kinds of plan files exist, and how apply should find its tasks. + +Project config should continue to provide shared context and rules. + +Custom validators, custom plan file types, and custom scope kinds should come later, after the first end-to-end workspace flow is working. + +OpenSpec should not add a broad workspace-specific config layer first. + +## Implementation Shape + +The first implementation should keep the current repo-local layout working. + +The first workspace implementation should create changes under the workspace `changes/` folder. + +The system should introduce a small shared planning-home resolver. + +The resolver should answer where the plan lives, where the change lives, which files matter, which affected areas are known, and which edit roots are allowed. + +The system should introduce a richer change model that can record affected areas, optional phases, plan file locations, and the current work focus. + +The first version of this model should stay small. It should not add custom validators, custom scope kinds, deep folder trees, or workspace archive behavior. + +The system should enrich status and instruction output before changing archive behavior. + +Archive and spec sync should come later because they are the highest-risk parts of the current system. + +## Rollout + +### Step 1: Connect Existing Pieces + +Add the small planning-home resolver. + +Keep repo-local behavior unchanged. + +Use the existing workspace root, workspace links, workflow schema, status command, and instructions command. + +The goal of this step is to stop spreading workspace checks through individual commands. + +### Step 2: Product Model And Status + +Update the change model and status JSON while keeping current repo-local storage. + +The goal of this step is to let agents stop guessing paths. + +Status should return the planning home, change path, plan file paths, affected areas when known, next steps, and allowed edit roots when relevant. + +### Step 3: Workspace Change Creation + +Allow a workspace to create one shared change under `changes/`. + +This step should not write into linked repos or folders. + +### Step 4: Affected Areas + +Use workspace links as the first source of affected areas. + +Let the plan confirm or refine which linked repos or folders are affected. + +### Step 5: Work Focus Apply + +Teach apply to select one affected area before implementation starts. + +The CLI should return the allowed edit root for that selected work focus. + +### Step 6: Scoped Plan Files + +Allow selected plan files to live at the level where they make sense. + +Start with simple paths before introducing a deep folder tree. + +### Step 7: Verify And Archive + +Add per-area verification and final whole-change archive once planning and apply are stable. + +## Risks + +The biggest risk is changing archive too early. + +Archive currently assumes one repo-local change and one repo-local specs folder. Workspace archive needs a clearer model for partial completion, per-area progress, and final completion. + +The second biggest risk is exposing too many new terms to users. + +The product should keep the visible workflow simple and push richer structure into CLI JSON for agents. + +The third biggest risk is overbuilding the file tree. + +OpenSpec should avoid creating a folder for every possible phase and affected area unless there is a real plan file to put there. + +## Decision + +The change should be reframed from workspace-specific commands to shared change planning. + +Workspace support should become one use case of a more general model where a change has a planning home, affected areas, optional phases, plan files, and one current work focus. + +The user-facing workflow should stay familiar. + +The internal model should become richer enough for agents to act safely without hardcoded paths. diff --git a/openspec/changes/workspace-change-planning/proposal.md b/openspec/changes/workspace-change-planning/proposal.md index aeab2b2d1..dde0b0bb1 100644 --- a/openspec/changes/workspace-change-planning/proposal.md +++ b/openspec/changes/workspace-change-planning/proposal.md @@ -1,13 +1,13 @@ ## Why -Once repos are visible and the agent has workspace context, the user should be able to plan a cross-repo change without immediately materializing repo-local artifacts. +Once repos are visible and the agent has workspace context, the user should be able to plan a cross-repo change without creating repo-local artifacts before implementation starts. The user goal is: ```text Explore the product goal across repos. Decide the scope. -Create one workspace-level proposal that identifies the repo slices. +Create one workspace-level proposal that identifies the affected areas. ``` Planning should be the commitment point. Repo visibility alone should remain lightweight. @@ -16,13 +16,20 @@ Planning should be the commitment point. Repo visibility alone should remain lig Add workspace-level change planning: +- install and refresh OpenSpec agent skills from the workspace root so agents can operate from the planning home +- add a workspace-specific planning schema for workspace changes - create a workspace change from the coordination root - capture the product goal once -- identify target repos by registered alias -- let the agent explore before committing to implementation slices +- identify affected areas by registered workspace link name where applicable +- let the agent explore before committing to affected areas or delivery slices - keep the workspace as the planning source of truth +- update workflow skill instructions to use CLI-reported artifact paths instead of hardcoded repo-local paths -This slice should avoid rebuilding the POC's materialization-first behavior. Repo-local artifacts should not be created merely because a workspace change exists. +This slice should avoid creating repo-local artifacts as a side effect of planning. Repo-local artifacts should not be created merely because a workspace change exists. + +Workspace setup and update may write agent skill files into the workspace root, such as `.codex/skills/` or `.claude/skills/`, because those files make the workspace planning home usable by agents. That setup work must not write OpenSpec artifacts or agent skill files into linked repos or folders. + +Interactive setup should ask which agents should get OpenSpec skills in the workspace, preselecting the preferred opener when that opener supports skills. Workspace update should let users refresh or change those installed agent skills later, including when run from inside the workspace. Planning dependency: @@ -36,12 +43,18 @@ Planning dependency: ### Modified Capabilities -- `change-creation`: Adds workspace-aware change creation semantics and target repo selection. +- `workspace-links`: Adds workspace setup/update behavior for workspace-local agent skill installation. +- `change-creation`: Adds workspace-aware change creation semantics and affected area selection. +- `cli-artifact-workflow`: Enriches workflow status and instructions so agents can discover planning context and artifact paths without hardcoded repo-local assumptions. +- `artifact-graph`: Adds a built-in workspace planning schema for workspace-scoped changes. +- `schema-resolution`: Ensures workspace-scoped change creation and workflow commands can resolve the workspace planning schema. - `openspec-conventions`: Defines the relationship between workspace-level planning and repo-local implementation work. ## Impact - Workspace change creation. -- Target repo metadata and validation. -- Agent instructions for proposing cross-repo changes. -- Tests that registered repos are visible before change creation and that creating a change does not imply repo-local materialization. +- Workspace-specific planning schema and templates. +- Affected area metadata and validation. +- Workspace setup and update behavior for installing or refreshing agent skills in the workspace root. +- Agent instructions for proposing cross-repo changes without hardcoded change paths. +- Tests that registered repos are visible before change creation and that creating a change does not imply repo-local artifact creation. diff --git a/openspec/changes/workspace-change-planning/review-candidate-requirements.html b/openspec/changes/workspace-change-planning/review-candidate-requirements.html new file mode 100644 index 000000000..67417d726 --- /dev/null +++ b/openspec/changes/workspace-change-planning/review-candidate-requirements.html @@ -0,0 +1,868 @@ + + + + +Review: candidate-requirements.md + + + +
      +
      +

      Review: candidate-requirements.md

      +
      openspec/changes/workspace-change-planning/candidate-requirements.md
      +
      +
      + 0 total + 0 pending + 0 approved + 0 rejected +
      +
      + +
      +
      +
      +
      + + + + +
      +
      +
      +
      + +
      +
      +

      Prompt output

      + +
      +
      Approve suggestions or add comments to build a prompt.
      +
      + + + + diff --git a/openspec/changes/workspace-change-planning/specs/artifact-graph/spec.md b/openspec/changes/workspace-change-planning/specs/artifact-graph/spec.md new file mode 100644 index 000000000..d7a7234bf --- /dev/null +++ b/openspec/changes/workspace-change-planning/specs/artifact-graph/spec.md @@ -0,0 +1,24 @@ +## ADDED Requirements + +### Requirement: Workspace planning schema +The artifact graph SHALL provide a built-in workspace planning schema for workspace-scoped changes. + +#### Scenario: Built-in workspace planning schema is available +- **WHEN** schemas are resolved from package built-ins +- **THEN** a schema named `workspace-planning` SHALL be available +- **AND** it SHALL describe the artifact structure for workspace-scoped planning + +#### Scenario: Workspace planning schema artifacts +- **WHEN** the `workspace-planning` schema is loaded +- **THEN** it SHALL include artifacts for a shared proposal, affected areas, cross-area design, and coordination tasks +- **AND** the affected areas artifact SHALL be distinct from delivery slices or phases + +#### Scenario: Workspace planning schema templates +- **WHEN** artifact instructions are requested for the `workspace-planning` schema +- **THEN** the schema SHALL provide templates that guide agents to write workspace-level planning content +- **AND** those templates SHALL avoid instructing agents to create repo-local implementation artifacts + +#### Scenario: Workspace planning apply readiness +- **WHEN** the `workspace-planning` schema defines apply readiness +- **THEN** it SHALL require coordination tasks before implementation begins +- **AND** the apply guidance SHALL direct agents to select an affected area before making implementation edits diff --git a/openspec/changes/workspace-change-planning/specs/change-creation/spec.md b/openspec/changes/workspace-change-planning/specs/change-creation/spec.md new file mode 100644 index 000000000..0e2c5f29e --- /dev/null +++ b/openspec/changes/workspace-change-planning/specs/change-creation/spec.md @@ -0,0 +1,35 @@ +## ADDED Requirements + +### Requirement: Workspace-aware change creation +Change creation SHALL support both repo-local and workspace planning homes. + +#### Scenario: Creating a change from a workspace root +- **GIVEN** the command runs from an OpenSpec workspace root +- **WHEN** the user creates a new change +- **THEN** OpenSpec SHALL create the change under the workspace planning path +- **AND** it SHALL not create the change under a linked repo's `openspec/changes/` directory +- **AND** it SHALL use the `workspace-planning` schema when no explicit schema is provided + +#### Scenario: Creating a change from inside a workspace +- **GIVEN** the command runs from a subdirectory of an OpenSpec workspace +- **WHEN** the user creates a new change +- **THEN** OpenSpec SHALL resolve the current workspace as the planning home +- **AND** it SHALL create the change under that workspace's planning path +- **AND** it SHALL use the `workspace-planning` schema when no explicit schema is provided + +#### Scenario: Preserving repo-local change creation +- **GIVEN** the command runs outside an OpenSpec workspace +- **WHEN** the user creates a new change in a repo-local OpenSpec project +- **THEN** OpenSpec SHALL continue to create the change under `openspec/changes/` + +#### Scenario: Rejecting invalid workspace affected areas +- **GIVEN** a workspace change creation request includes affected area names +- **WHEN** one or more names are not registered workspace links +- **THEN** OpenSpec SHALL reject those invalid affected areas +- **AND** it SHALL list the valid workspace link names + +#### Scenario: Creating without affected areas +- **GIVEN** the user is still exploring scope +- **WHEN** the user creates a workspace change without affected areas +- **THEN** OpenSpec SHALL create the workspace change +- **AND** it SHALL allow affected areas to be identified later diff --git a/openspec/changes/workspace-change-planning/specs/cli-artifact-workflow/spec.md b/openspec/changes/workspace-change-planning/specs/cli-artifact-workflow/spec.md new file mode 100644 index 000000000..09a5feb82 --- /dev/null +++ b/openspec/changes/workspace-change-planning/specs/cli-artifact-workflow/spec.md @@ -0,0 +1,91 @@ +## ADDED Requirements + +### Requirement: Status JSON provides planning context +The status command SHALL provide machine-readable planning context for repo-local and workspace changes. + +#### Scenario: Reporting planning home +- **WHEN** a user runs `openspec status --change --json` +- **THEN** the output SHALL identify whether the change is repo-local or workspace-scoped +- **AND** it SHALL include the planning home root and change root + +#### Scenario: Reporting concrete artifact paths +- **WHEN** a user runs `openspec status --change --json` +- **THEN** the output SHALL include concrete paths for existing artifacts +- **AND** agents SHALL be able to read those paths without assuming `openspec/changes//` + +#### Scenario: Reporting workspace affected areas +- **GIVEN** the change is workspace-scoped +- **WHEN** a user runs `openspec status --change --json` +- **THEN** the output SHALL include known affected areas +- **AND** it SHALL indicate when affected areas are unresolved + +#### Scenario: Reporting next steps +- **WHEN** a user runs `openspec status --change --json` +- **THEN** the output SHALL include next step guidance for agents +- **AND** the guidance SHALL use plain action language + +### Requirement: Status JSON action context +The status command SHALL expose action context that lets agents act without hardcoded filesystem assumptions. + +#### Scenario: Planning action context +- **WHEN** a workspace change is still in planning +- **THEN** status JSON SHALL identify the planning artifacts agents may read or update +- **AND** it SHALL indicate that linked repos and folders are context for exploration + +#### Scenario: Implementation action context +- **WHEN** a workspace change has a selected affected area for implementation +- **THEN** status JSON SHALL include the allowed edit root for that area +- **AND** it SHALL avoid authorizing edits outside that selected area + +#### Scenario: Repo-local action context +- **GIVEN** the change is repo-local +- **WHEN** a user runs `openspec status --change --json` +- **THEN** status JSON SHALL preserve existing artifact status behavior +- **AND** it SHALL report a repo-local planning home for agents that use action context + +### Requirement: Instructions use resolved planning paths +Artifact and apply instructions SHALL use resolved planning paths rather than hardcoded repo-local change paths. + +#### Scenario: Workspace artifact instructions +- **GIVEN** the change is workspace-scoped +- **WHEN** a user runs `openspec instructions --change --json` +- **THEN** instruction output SHALL point to the artifact path under the workspace change root +- **AND** it SHALL not instruct the agent to write under a linked repo unless an explicit implementation context allows it + +#### Scenario: Repo-local artifact instructions +- **GIVEN** the change is repo-local +- **WHEN** a user runs `openspec instructions --change --json` +- **THEN** instruction output SHALL preserve existing repo-local paths + +### Requirement: Workflow skills use CLI artifact context +Generated workflow skills SHALL use OpenSpec CLI output as the source of truth for artifact locations. + +#### Scenario: Skills inspect status before artifact work +- **WHEN** a generated workflow skill needs to inspect or create artifacts for a change +- **THEN** it SHALL instruct the agent to run `openspec status --change --json` +- **AND** it SHALL use returned planning context and artifact paths rather than assuming a repo-local change path + +#### Scenario: Skills use instructions before writing artifacts +- **WHEN** a generated workflow skill is about to create or update an artifact +- **THEN** it SHALL instruct the agent to run `openspec instructions --change --json` +- **AND** it SHALL write to the resolved artifact path returned by the command + +#### Scenario: Skills avoid hardcoded repo-local paths +- **WHEN** generated workflow skills describe artifact locations +- **THEN** they SHALL avoid hardcoded examples that require changes to live under `openspec/changes//` +- **AND** any examples SHALL defer to CLI-reported paths for repo-local and workspace-scoped changes + +### Requirement: Workspace schema instructions +Workflow commands SHALL use the workspace planning schema instructions for workspace-scoped changes that use that schema. + +#### Scenario: Workspace planning artifact order +- **GIVEN** a workspace-scoped change uses schema `workspace-planning` +- **WHEN** a user runs `openspec status --change --json` +- **THEN** the artifact list SHALL reflect the workspace planning schema +- **AND** it SHALL include the affected areas artifact + +#### Scenario: Workspace areas instructions +- **GIVEN** a workspace-scoped change uses schema `workspace-planning` +- **WHEN** a user requests instructions for the affected areas artifact +- **THEN** instruction output SHALL guide the agent to capture known, suspected, and unresolved affected areas +- **AND** it SHALL not require all affected areas to be finalized before planning can continue diff --git a/openspec/changes/workspace-change-planning/specs/openspec-conventions/spec.md b/openspec/changes/workspace-change-planning/specs/openspec-conventions/spec.md new file mode 100644 index 000000000..a18fc9ea5 --- /dev/null +++ b/openspec/changes/workspace-change-planning/specs/openspec-conventions/spec.md @@ -0,0 +1,32 @@ +## ADDED Requirements + +### Requirement: Workspace planning vocabulary +OpenSpec conventions SHALL distinguish workspace planning concepts using user-facing product language. + +#### Scenario: Naming affected areas +- **WHEN** documentation or generated guidance refers to repos, folders, packages, services, apps, or docs sites touched by a workspace change +- **THEN** it SHALL call them affected areas +- **AND** it SHALL avoid using "target repo" or "repo slice" as the primary user-facing term + +#### Scenario: Naming delivery slices +- **WHEN** documentation or generated guidance refers to delivery increments inside a larger change +- **THEN** it SHALL call them slices or phases only when delivery sequencing is the subject +- **AND** it SHALL not use slice as a synonym for repo, folder, or affected area + +### Requirement: Workspace planning and implementation boundary +OpenSpec conventions SHALL distinguish workspace-level planning from repo-local implementation ownership. + +#### Scenario: Workspace as shared planning home +- **WHEN** a change spans linked repos or folders +- **THEN** conventions SHALL describe the workspace as the shared planning home +- **AND** repo-local implementation homes SHALL retain ownership of their code and canonical behavior + +#### Scenario: Avoiding materialization-first language +- **WHEN** documentation explains workspace change creation +- **THEN** it SHALL describe the user outcome in terms of shared planning and affected areas +- **AND** it SHALL avoid making users understand implementation terms such as materialization before they can plan + +#### Scenario: Preserving familiar workflow verbs +- **WHEN** workspace guidance describes OpenSpec workflows +- **THEN** it SHALL keep the familiar verbs explore, propose, apply, verify, and archive +- **AND** it SHALL explain that workspace context changes paths, scope, and allowed edit roots rather than creating a separate workflow family diff --git a/openspec/changes/workspace-change-planning/specs/schema-resolution/spec.md b/openspec/changes/workspace-change-planning/specs/schema-resolution/spec.md new file mode 100644 index 000000000..434d1d07f --- /dev/null +++ b/openspec/changes/workspace-change-planning/specs/schema-resolution/spec.md @@ -0,0 +1,25 @@ +## ADDED Requirements + +### Requirement: Workspace planning schema resolution +Schema resolution SHALL support the built-in workspace planning schema. + +#### Scenario: Listing workspace planning schema +- **WHEN** a user runs `openspec schemas` +- **THEN** the output SHALL include `workspace-planning` +- **AND** it SHALL identify it as a package-provided schema unless overridden by a higher-precedence schema + +#### Scenario: Resolving workspace planning schema by name +- **WHEN** a workflow command requests schema `workspace-planning` +- **THEN** schema resolution SHALL resolve it using the normal project, user, then package precedence order + +#### Scenario: Workspace default schema for new changes +- **GIVEN** the command creates a change in a workspace planning home +- **AND** the user did not pass an explicit `--schema` +- **WHEN** OpenSpec resolves the schema for the new change +- **THEN** it SHALL use `workspace-planning` as the default schema + +#### Scenario: Explicit schema override for workspace change +- **GIVEN** the command creates a change in a workspace planning home +- **WHEN** the user passes an explicit `--schema ` +- **THEN** OpenSpec SHALL use the explicitly requested schema +- **AND** it SHALL validate that schema using normal schema resolution diff --git a/openspec/changes/workspace-change-planning/specs/workspace-change-planning/spec.md b/openspec/changes/workspace-change-planning/specs/workspace-change-planning/spec.md new file mode 100644 index 000000000..ef97aa3a5 --- /dev/null +++ b/openspec/changes/workspace-change-planning/specs/workspace-change-planning/spec.md @@ -0,0 +1,59 @@ +## ADDED Requirements + +### Requirement: Workspace change planning home +OpenSpec SHALL support workspace-level changes whose shared plan lives in the workspace planning home. + +#### Scenario: Creating a workspace change +- **GIVEN** the command runs from an OpenSpec workspace +- **WHEN** the user creates a change for workspace planning +- **THEN** OpenSpec SHALL create the change under the workspace planning path +- **AND** it SHALL treat the workspace as the planning home for that change +- **AND** it SHALL use the workspace planning schema when no explicit schema is provided + +#### Scenario: Workspace planning artifact structure +- **GIVEN** a workspace change uses the workspace planning schema +- **WHEN** OpenSpec reports or creates planning artifacts for that change +- **THEN** it SHALL use workspace-level artifacts for proposal, affected areas, cross-area design, and coordination tasks +- **AND** those artifacts SHALL live under the workspace change root + +#### Scenario: Capturing the shared goal once +- **WHEN** a workspace change is proposed +- **THEN** OpenSpec SHALL capture the product goal at the workspace change level +- **AND** it SHALL avoid requiring separate repo-local proposals before the affected areas are understood + +#### Scenario: Preserving linked repos during change creation +- **WHEN** OpenSpec creates a workspace-level change +- **THEN** it SHALL not create repo-local OpenSpec change directories inside linked repos or folders +- **AND** it SHALL not edit implementation files in linked repos or folders + +### Requirement: Workspace affected areas +OpenSpec SHALL represent ownership or implementation boundaries in a workspace change as affected areas. + +#### Scenario: Using registered workspace links as areas +- **GIVEN** a workspace has linked repos or folders +- **WHEN** a workspace change identifies affected areas by registered link name +- **THEN** OpenSpec SHALL validate those area names against the workspace links +- **AND** it SHALL report invalid area names clearly + +#### Scenario: Planning before all areas are known +- **WHEN** a user is still exploring a workspace change +- **THEN** OpenSpec SHALL allow the shared plan to exist before all affected areas are finalized +- **AND** it SHALL keep unresolved affected area state visible to agents + +#### Scenario: Separating areas from delivery slices +- **WHEN** a workspace change reports affected areas +- **THEN** OpenSpec SHALL distinguish affected areas from delivery slices or phases +- **AND** it SHALL not require users to define delivery slices for a small cross-area change + +### Requirement: Workspace planning source of truth +OpenSpec SHALL keep the workspace change plan as the source of truth until implementation begins for a selected affected area. + +#### Scenario: Exploring before implementation +- **WHEN** an agent explores a workspace change +- **THEN** it SHALL use workspace-level planning artifacts as the shared planning source +- **AND** it SHALL treat linked repos and folders as available context rather than committed implementation targets + +#### Scenario: Deferring repo-local implementation +- **WHEN** repo-local implementation work is needed for a workspace change +- **THEN** OpenSpec SHALL require an explicit implementation workflow with a selected affected area +- **AND** it SHALL expose the allowed edit root for that selected area before implementation edits begin diff --git a/openspec/changes/workspace-change-planning/specs/workspace-links/spec.md b/openspec/changes/workspace-change-planning/specs/workspace-links/spec.md new file mode 100644 index 000000000..0a1f94b4a --- /dev/null +++ b/openspec/changes/workspace-change-planning/specs/workspace-links/spec.md @@ -0,0 +1,81 @@ +## ADDED Requirements + +### Requirement: Workspace setup installs agent skills +OpenSpec SHALL let users install OpenSpec agent skills into a workspace during workspace setup. + +#### Scenario: Prompting for workspace agent skills +- **WHEN** interactive workspace setup reaches agent skill installation +- **THEN** OpenSpec SHALL ask which agents should get OpenSpec skills in this workspace +- **AND** the prompt SHALL use agent-skill language rather than "AI tools" language + +#### Scenario: Preselecting the preferred opener +- **GIVEN** the user selected a preferred opener that supports OpenSpec skill generation +- **WHEN** interactive workspace setup asks which agents should get skills +- **THEN** OpenSpec SHALL preselect the matching agent +- **AND** the user SHALL be able to select additional agents or deselect the preselected agent + +#### Scenario: Installing selected workspace skills +- **WHEN** workspace setup completes with one or more selected agents +- **THEN** OpenSpec SHALL generate or refresh OpenSpec skill files under the workspace root for each selected agent +- **AND** it SHALL report which agents received skills + +#### Scenario: Installing skills only during setup +- **WHEN** workspace setup installs agent skills +- **THEN** OpenSpec SHALL generate skill files only +- **AND** it SHALL not generate slash command files or global command files as part of workspace setup + +#### Scenario: Preserving linked repos during skill installation +- **WHEN** workspace setup installs agent skills +- **THEN** OpenSpec SHALL leave linked repos and folders unchanged +- **AND** generated skills SHALL be scoped to the workspace planning home + +#### Scenario: Non-interactive setup tool selection +- **WHEN** non-interactive workspace setup receives `--tools all`, `--tools none`, or `--tools ` +- **THEN** OpenSpec SHALL use the selected tool set for workspace agent skill installation +- **AND** it SHALL validate tool IDs using the same supported tool IDs as skill generation for repo initialization + +#### Scenario: Reporting setup skills in JSON output +- **WHEN** non-interactive workspace setup installs agent skills with JSON output enabled +- **THEN** OpenSpec SHALL include generated, refreshed, skipped, or failed skill installation results in machine-readable output + +### Requirement: Workspace update manages agent skills +OpenSpec SHALL provide a workspace update flow for refreshing agent skills after setup. + +#### Scenario: Updating the current workspace +- **GIVEN** the command runs from inside an OpenSpec workspace +- **WHEN** the user runs `openspec workspace update` +- **THEN** OpenSpec SHALL update that current workspace + +#### Scenario: Updating a named workspace +- **GIVEN** a workspace named `platform` is known locally +- **WHEN** the user runs `openspec workspace update platform` +- **THEN** OpenSpec SHALL update the `platform` workspace + +#### Scenario: Updating a workspace selected by flag +- **GIVEN** a workspace named `platform` is known locally +- **WHEN** the user runs `openspec workspace update --workspace platform` +- **THEN** OpenSpec SHALL update the `platform` workspace + +#### Scenario: Updating selected workspace skills +- **WHEN** workspace update completes with selected agents +- **THEN** OpenSpec SHALL refresh OpenSpec skills for selected agents +- **AND** it SHALL add skills for newly selected agents +- **AND** it SHALL remove OpenSpec-managed workflow skill directories for agents that are no longer selected + +#### Scenario: Removing only managed skill directories +- **WHEN** workspace update removes skills for an unselected agent +- **THEN** OpenSpec SHALL remove only known OpenSpec-managed workflow skill directories +- **AND** it SHALL preserve unrelated files in the agent directory + +#### Scenario: Non-interactive update tool selection +- **WHEN** workspace update receives `--tools all`, `--tools none`, or `--tools ` +- **THEN** OpenSpec SHALL update workspace agent skills using that selected tool set +- **AND** it SHALL avoid prompting for agent selection + +#### Scenario: Reporting workspace skill update results +- **WHEN** workspace update changes agent skill state +- **THEN** OpenSpec SHALL report which agents were refreshed, added, removed, skipped, or failed + +#### Scenario: Reporting workspace update results in JSON output +- **WHEN** workspace update runs with JSON output enabled +- **THEN** OpenSpec SHALL include refreshed, added, removed, skipped, or failed skill results in machine-readable output diff --git a/openspec/changes/workspace-change-planning/tasks.md b/openspec/changes/workspace-change-planning/tasks.md new file mode 100644 index 000000000..7c75841a5 --- /dev/null +++ b/openspec/changes/workspace-change-planning/tasks.md @@ -0,0 +1,53 @@ +## 1. Workspace Setup Skills + +- [ ] 1.1 Add an interactive workspace setup step named "Install agent skills" that asks which agents should get OpenSpec skills in this workspace. +- [ ] 1.2 Preselect the preferred opener when that opener supports skills, while allowing users to choose different or additional agents. +- [ ] 1.3 Support non-interactive agent selection with the existing `--tools all|none|` style. +- [ ] 1.4 Validate workspace setup tool IDs using the same supported skill-generation tool set as repo initialization. +- [ ] 1.5 Ensure `openspec workspace setup` generates or refreshes OpenSpec agent skills in the workspace root for the selected agents. +- [ ] 1.6 Keep setup-time skill generation scoped to the workspace planning home; do not write skills or OpenSpec artifacts into linked repos or folders during workspace setup. +- [ ] 1.7 Keep workspace setup skill generation skills-only for this slice; do not generate slash commands or global command files. +- [ ] 1.8 Define how setup reports generated, refreshed, skipped, or failed skill installation work in human and JSON output. + +## 2. Workspace Skill Updates + +- [ ] 2.1 Add a workspace update flow that refreshes, adds, or removes OpenSpec agent skills in an existing workspace. +- [ ] 2.2 Let `openspec workspace update` resolve the current workspace when run from inside a workspace. +- [ ] 2.3 Support named and selected-workspace update forms such as `openspec workspace update platform` and `openspec workspace update --workspace platform`. +- [ ] 2.4 Support non-interactive update forms such as `openspec workspace update platform --tools codex,claude`. +- [ ] 2.5 Remove only known OpenSpec-managed workflow skill directories for agents that are no longer selected. +- [ ] 2.6 Define how update reports refreshed, added, removed, skipped, or failed skill work in human and JSON output. + +## 3. Workspace Change Creation + +- [ ] 3.1 Add a built-in `workspace-planning` schema and templates. +- [ ] 3.2 Add workspace-aware change creation from the workspace coordination root. +- [ ] 3.3 Default workspace-scoped change creation to the `workspace-planning` schema. +- [ ] 3.4 Store workspace-level changes under the workspace planning path rather than under linked repos or folders. +- [ ] 3.5 Capture the product goal once at the workspace change level. +- [ ] 3.6 Record or validate affected areas using registered workspace link names where applicable. +- [ ] 3.7 Ensure creating a workspace change does not create repo-local OpenSpec artifacts or edit linked repos. +- [ ] 3.8 Preserve repo-local change creation behavior outside workspaces. + +## 4. Planning Home And Agent Context + +- [ ] 4.1 Introduce a shared planning-home resolver that identifies repo-local versus workspace planning homes. +- [ ] 4.2 Enrich `openspec status --change --json` with planning home, change root, relevant artifact paths, affected areas, next steps, and action context. +- [ ] 4.3 Enrich `openspec instructions --change --json` with resolved artifact paths for repo-local and workspace-scoped changes. +- [ ] 4.4 Keep workspace-level planning as the source of truth until an explicit implementation workflow selects an affected area. + +## 5. Workflow Skill Instructions + +- [ ] 5.1 Update generated workflow skill templates to run `openspec status --change --json` before artifact work and trust returned planning context. +- [ ] 5.2 Update generated workflow skill templates to run `openspec instructions --change --json` before writing artifacts and use the resolved output path. +- [ ] 5.3 Audit source workflow templates for hardcoded `openspec/changes/` assumptions and replace them with CLI-reported path guidance. +- [ ] 5.4 Keep a separate artifact-context command out of this slice unless enriched status/instructions prove insufficient during implementation. + +## 6. Verification + +- [ ] 6.1 Add tests that workspace setup installs skills in the workspace root and leaves linked repos unchanged. +- [ ] 6.2 Add tests that workspace update refreshes, adds, and removes only managed workspace skill directories. +- [ ] 6.3 Add tests that registered repos are visible before change creation. +- [ ] 6.4 Add tests that workspace change creation does not imply repo-local artifact creation. +- [ ] 6.5 Add cross-platform path tests for workspace-root skill paths and workspace change paths. +- [ ] 6.6 Run `openspec validate workspace-change-planning --strict`. From 28ce27ef4553f3b411575aa5a7abd1dbdce642fc Mon Sep 17 00:00:00 2001 From: TabishB Date: Thu, 14 May 2026 01:49:12 +1000 Subject: [PATCH 02/14] Implement workspace setup skills phase --- .../workspace-change-planning/design.md | 111 +++++++- .../workspace-change-planning/proposal.md | 20 +- .../specs/artifact-graph/spec.md | 16 +- .../specs/change-creation/spec.md | 9 +- .../specs/cli-artifact-workflow/spec.md | 19 +- .../specs/cli-config/spec.md | 55 ++++ .../specs/cli-update/spec.md | 21 ++ .../specs/workspace-change-planning/spec.md | 12 +- .../specs/workspace-links/spec.md | 82 ++++++ .../workspace-change-planning/tasks.md | 131 ++++++--- src/commands/workspace.ts | 166 +++++++++++- src/commands/workspace/types.ts | 1 + src/core/completions/command-registry.ts | 5 + src/core/workspace/foundation.ts | 21 ++ src/core/workspace/index.ts | 1 + src/core/workspace/skills.ts | 256 ++++++++++++++++++ test/commands/workspace.interactive.test.ts | 65 +++++ test/commands/workspace.test.ts | 158 +++++++++++ test/core/workspace/skills.test.ts | 20 ++ 19 files changed, 1112 insertions(+), 57 deletions(-) create mode 100644 openspec/changes/workspace-change-planning/specs/cli-config/spec.md create mode 100644 openspec/changes/workspace-change-planning/specs/cli-update/spec.md create mode 100644 src/core/workspace/skills.ts create mode 100644 test/core/workspace/skills.test.ts diff --git a/openspec/changes/workspace-change-planning/design.md b/openspec/changes/workspace-change-planning/design.md index 0b651cde0..225aa0923 100644 --- a/openspec/changes/workspace-change-planning/design.md +++ b/openspec/changes/workspace-change-planning/design.md @@ -10,8 +10,13 @@ The existing artifact workflow assumes a change lives under a repo-local `opensp **Goals:** - Install OpenSpec agent skills into the workspace root during workspace setup. +- Use the active global profile to select which workflow skills are installed in the workspace. - Let users choose which agents receive skills with familiar `--tools` semantics. +- Persist workspace-local agent skill selection so update can refresh the same agents later. - Let users refresh, add, or remove workspace-local skills later through `workspace update`. +- Detect and report workspace-local skill drift from the active global profile. +- Let `openspec config profile` offer to apply changed profile settings to the current workspace when run from inside a workspace. +- Redirect workspace users from repo-local `openspec update` to `openspec workspace update`. - Add a built-in workspace planning schema for workspace-scoped changes. - Create workspace changes under the workspace planning path. - Represent affected areas without forcing implementation artifacts into linked repos. @@ -20,7 +25,9 @@ The existing artifact workflow assumes a change lives under a repo-local `opensp **Non-Goals:** - Generating slash commands as part of workspace setup. +- Honoring global `delivery: commands` by generating workspace command files. - Installing skills into linked repos or folders. +- Adding workspace-local workflow profiles separate from global config. - Solving workspace-scoped artifact path discovery in the first setup-skill step. - Adding a separate artifact-context CLI command in the first version. - Implementing workspace apply, verify, or archive semantics end to end. @@ -40,27 +47,71 @@ The CLI should use the existing `--tools all|none|` grammar for non-interac Alternative considered: add `--agents`. That reads better in isolation, but it creates unnecessary parallel vocabulary next to `openspec init --tools`. +### Let profile choose workflows and tools choose agents + +Workspace setup/update should use the active global profile to decide which OpenSpec workflow skills are installed. The profile answers "which actions are available?" while `--tools` answers "which agents get those actions?" Keeping those concerns separate preserves the existing profile model and avoids adding workspace-local workflow selection in this slice. + +If global profile is `core`, workspace skills should include the core workflow set. If global profile is `custom`, workspace skills should include only the configured custom workflows. `--tools none` should still mean no agent skills are installed, regardless of profile. + +Alternative considered: add a workspace-local profile file. That might be useful later for team-shared workspace defaults, but this slice already stores machine-local agent paths and should avoid introducing another config authority before the global profile behavior works. + ### Preselect the preferred opener when possible Interactive setup should preselect the preferred opener when that opener maps to a skill-capable agent. The user can accept the default, add more agents, or deselect it. Alternative considered: install skills only for the preferred opener. That is simpler, but opener choice means "how should I open this workspace" while skill selection means "which agents should understand OpenSpec here." +### Persist selected workspace skill agents locally + +Workspace setup should store the selected skill-capable agents in `.openspec-workspace/local.yaml` because agent paths and installed tool surfaces are machine-local. Workspace update should use that stored selection when the user does not pass `--tools` or make a new interactive selection. + +Explicit `--tools` on workspace setup/update should replace the stored selection. `--tools none` should store an empty selection and remove only known OpenSpec-managed workspace skill directories. + +The local state should also record enough last-applied information to support drift detection, such as the workflow IDs installed for each selected agent and the effective global profile/delivery at the time of the last successful sync. This is diagnostic state, not a second source of truth. + +Alternative considered: infer selected agents by scanning `.codex/skills/`, `.claude/skills/`, and similar directories. Scanning is useful as a fallback, but persisted selection gives predictable update behavior and avoids treating unrelated user-authored files as OpenSpec-managed state. + +### Keep non-interactive setup backward-compatible + +`openspec workspace setup --no-interactive` should not require `--tools`. If `--tools` is omitted, setup should create the workspace and skip skill installation, preserving existing scripted workspace setup behavior. Human and JSON output should say that no workspace skills were installed and that `openspec workspace update --tools ` can add them later. + +`openspec workspace update --no-interactive` without `--tools` should refresh the stored workspace skill agent selection. If no selection is stored, it should complete without installing skills and report a clear no-op with guidance to pass `--tools`. + +Alternative considered: require `--tools` whenever workspace setup/update is non-interactive. That mirrors repo-local init, but it would break existing workspace setup scripts that predate workspace-local skill installation. + ### Generate workspace-local skills only Workspace setup/update should generate skills under the workspace root, such as `.codex/skills/` or `.claude/skills/`. It should not generate slash commands in this slice because some command adapters resolve to global locations, and workspace setup should remain local and predictable. +When global delivery is `commands` or `both`, workspace setup/update should still generate only skills and report that workspace command generation is not part of this slice. This keeps profile workflow selection useful without making workspace setup perform global or repo-local command writes. + Alternative considered: mirror `init` exactly and generate both skills and commands. That risks surprising global writes and makes the setup boundary harder to explain. ### Add `workspace update` for skill refresh `openspec workspace update` should refresh, add, or remove workspace-local OpenSpec skills after setup. It should resolve the current workspace when run from inside a workspace, and also support named and non-interactive forms. +Workspace update should compare the active global profile's workflow selection with the last applied workspace skill state. If they differ, update should add/remove only OpenSpec-managed workflow skill directories for the selected agents. Workspace doctor/list/status surfaces may report the drift as a warning, and `openspec config profile` no-op inside a workspace should use the same drift check for guidance. + Alternative considered: reuse `openspec update` from inside the workspace. That command currently means repo/project update, while workspace update needs workspace selection, workspace JSON/status behavior, and linked-repo safety rules. +### Make `config profile` workspace-aware + +`openspec config profile` should remain a global configuration command. When it runs inside a repo-local OpenSpec project and the user chooses to apply changes, it should continue to run `openspec update`. + +When it runs inside an OpenSpec workspace and the profile or delivery settings actually change, it should prompt to apply changes to the current workspace. If confirmed, it should run `openspec workspace update` for that workspace. If declined, it should explain that the global config changed and the user can run `openspec workspace update` later. + +The preset shortcut `openspec config profile core` should keep its non-interactive character and not launch an apply prompt. When run from inside a workspace, it should save global config and print workspace-specific follow-up guidance to run `openspec workspace update`. When run inside a repo-local project, it should keep the existing repo-local guidance. + +For this slice, automatic workspace context should come from the workspace planning home and its own subdirectories. Running a command from inside a linked repo or folder should keep that location's repo-local behavior unless the user explicitly selects the workspace with a workspace command option. This avoids surprising repo-local commands merely because the repo is registered as a workspace link. + +If a directory is both inside a workspace planning home and inside a repo-local OpenSpec project, the nearest planning home should determine the apply prompt. This avoids applying a workspace profile change to a linked repo when the user is intentionally operating from the workspace planning home. + +Alternative considered: make `openspec config profile` update all known workspaces. That would be convenient in small setups, but global config changes should not fan out into multiple planning homes without an explicit per-workspace action. + ### Resolve a planning home before acting -Workflow commands should resolve whether the current change belongs to a repo-local planning home or a workspace planning home before computing paths. The resolver should identify the planning root, change root, linked areas when present, and whether implementation edits are allowed. +Workflow commands should resolve whether the current change belongs to a repo-local planning home or a workspace planning home before computing paths. The resolver should identify the planning root, change root, linked areas when present, and whether implementation edits are allowed. Linked repos are not implicitly treated as workspace planning homes just because they are registered in a workspace; workspace-scoped behavior is selected from the workspace planning home or through explicit workspace selection. Alternative considered: add workspace-specific command branches wherever paths are used. That would make the workspace model leak into every workflow and make generated skills more fragile. @@ -80,17 +131,25 @@ Initial artifact shape: changes// .openspec.yaml # schema: workspace-planning proposal.md # shared goal and scope - areas.md # known, suspected, and unresolved affected areas design.md # cross-area decisions - tasks.md # coordination and planning tasks + tasks.md # coordination tasks, optionally grouped by affected area + specs/ + / + /spec.md ``` -The first schema should stay intentionally small. Area-specific folders can come later once scoped artifact paths and apply semantics are stable. +The first schema should stay intentionally close to the normal OpenSpec artifact shape: proposal, specs, design, and tasks. Area-specific requirements live under `specs/` and area-specific work can be represented as sections in `tasks.md`. This slice does not introduce another area manifest beside those normal planning artifacts. -Alternative considered: reuse `spec-driven` and make all workspace differences implicit in status output. That hides the fact that workspace planning needs an affected-area artifact and different instructions from repo-local spec work. +Alternative considered: reuse `spec-driven` unchanged and make all workspace differences implicit in status output. That hides the fact that workspace planning needs different instructions for organizing requirements and tasks by affected area. Alternative considered: create separate workspace workflow skills instead of a schema. That would duplicate workflow guidance and make workspace mode feel like a different product. +### Support nested workspace spec paths in the schema + +The `workspace-planning` schema should define its specs artifact so nested workspace paths are first-class, not accidental. The intended output pattern is `specs/**/*.md`, and the schema instructions should explicitly describe `specs///spec.md` as the default convention for area-specific requirements. + +Status and instructions output should preserve the concrete nested paths it discovers. Repo-local spec sync, archive, and validation paths that assume `specs//spec.md` should not treat workspace-scoped specs as repo-local capability specs until a later explicit implementation, sync, or archive workflow selects an affected area and defines the destination. + ### Use affected areas, not targets or repo slices The planning model should call ownership or implementation boundaries "affected areas." Affected areas can start with registered workspace link names, but the language should leave room for folders, packages, services, apps, or docs sites. Delivery breakdown remains a separate concept and should not be called an area. @@ -118,11 +177,53 @@ This keeps the same skill usable in repo-local and workspace-scoped changes. If Alternative considered: add a new `openspec context` command now. That may become useful, but it adds a new surface before we have proven that enriched status/instructions are insufficient. +### Guard unsupported workspace workflow actions + +The global profile may select workflows whose workspace-scoped behavior is not implemented in this slice, such as full workspace apply, verify, or archive. Generated workspace-local skills for those workflows should be safe: they should inspect status/instructions, explain the unsupported workspace action, and avoid editing linked repos unless a later explicit implementation workflow supplies an allowed edit root. + +This keeps the workspace skill set aligned with the user's profile while preventing repo-local fallbacks from pretending to implement workspace semantics. + +Alternative considered: filter unsupported workflows out of workspace skill generation. That would avoid unsupported commands, but it would make the workspace skill set silently diverge from the user's profile and make drift harder to explain. + +### Redirect repo update from workspace roots + +`openspec update` should remain the repo/project update command. When it is run from an OpenSpec workspace planning home, it should not try to treat the workspace as a repo-local project. It should fail or redirect with clear guidance to run `openspec workspace update`. + +Alternative considered: make `openspec update` polymorphic and perform workspace update inside workspaces. That would be convenient, but it blurs the repo/project versus workspace boundary this change is trying to make explicit. + +### Update docs, help, and completions + +The CLI help, command registry/completions, and user docs should include `openspec workspace update`, its `--tools` behavior, the global-profile relationship, and the skills-only workspace delivery rule. + +Alternative considered: document this only after implementation. Because profile/update behavior is easy to confuse with repo-local update, the docs and help updates are part of the user-facing feature. + +### Treat manual acceptance and UX review as phase gates + +Each phase should produce a user-testable increment, even when most of the work is internal. The phase is not done until a user can exercise the named behavior through the CLI, inspect the resulting output or files, and understand what changed. + +Each implementation phase should include a manual acceptance pass in addition to automated tests. The manual pass should exercise the real CLI flow, inspect the generated files or output, and confirm linked repos or folders stay untouched where that is part of the contract. + +Each phase should also include a lightweight UX review of prompts, command forms, human output, JSON output, artifact paths, and next-step guidance. Any confusing UX found during review should be fixed in the same phase or recorded as an intentional follow-up before the phase is considered done. + +Alternative considered: keep manual review only in the final verification phase. That would catch end-to-end issues late, but workspace planning is mostly workflow and agent-facing UX, so each phase needs its own human check while the behavior is still fresh. + +### Reduce self-validation bias with evidence-based review + +Implementation should define acceptance evidence before marking tasks done. For each phase, the implementer should capture the exact manual commands or interaction path, expected observations, and actual observations. A task is not complete merely because the implementer believes the code matches the design. + +When practical, a separate reviewer or fresh agent context should run the manual acceptance checklist and UX review using only the change artifacts, CLI output, and observed filesystem state. If a separate reviewer is not available, the implementer should rerun the checklist from a clean temporary workspace and record the evidence in the change notes or final implementation summary. + +Alternative considered: rely on automated tests plus the implementer's final review. Automated tests are necessary, but this change is workflow-heavy and agent-facing, so independent evidence is more useful than confidence alone. + ## Risks / Trade-offs - Skill generation logic may drift from `init/update` → share the same template generation and tool validation helpers where practical. - Removing unselected skills could remove user-modified files → remove only known OpenSpec-managed workflow skill directories by explicit workflow list. - `--tools` is less precise than `--agents` in workspace UX → keep `--tools` for CLI consistency, but use "agents" in prompts and human output. +- Global delivery can say `commands` while workspace update remains skills-only → report this explicitly so users know command generation is deferred, not silently broken. +- `config profile` may run from a linked repo inside an opened workspace → resolve the current planning home carefully and apply only to that home. +- Stored workspace skill state can become stale or hand-edited → treat it as diagnostic machine-local state and always reconcile managed files from the active global profile during update. +- Profile-selected workflows may not yet have full workspace semantics → generated skills must guard unsupported actions and avoid repo-local fallbacks. - Existing generated skills still contain repo-local path assumptions → handle that as a later artifact-context step after workspace-local skills can be installed. - Status JSON may become too broad → keep fields plain and action-oriented, such as `planningHome`, `artifacts`, `affectedAreas`, `nextSteps`, and `actionContext`. - Affected area discovery may be ambiguous → start with explicit registered workspace links and allow later refinement instead of parsing free-form Markdown headings as the only source of truth. diff --git a/openspec/changes/workspace-change-planning/proposal.md b/openspec/changes/workspace-change-planning/proposal.md index dde0b0bb1..6f6442d84 100644 --- a/openspec/changes/workspace-change-planning/proposal.md +++ b/openspec/changes/workspace-change-planning/proposal.md @@ -17,6 +17,8 @@ Planning should be the commitment point. Repo visibility alone should remain lig Add workspace-level change planning: - install and refresh OpenSpec agent skills from the workspace root so agents can operate from the planning home +- use the active global workflow profile to decide which workflow skills are installed in the workspace +- keep `--tools` focused on which agents receive those workspace-local skills - add a workspace-specific planning schema for workspace changes - create a workspace change from the coordination root - capture the product goal once @@ -31,6 +33,16 @@ Workspace setup and update may write agent skill files into the workspace root, Interactive setup should ask which agents should get OpenSpec skills in the workspace, preselecting the preferred opener when that opener supports skills. Workspace update should let users refresh or change those installed agent skills later, including when run from inside the workspace. +Workspace setup and update should treat the global profile as the workflow selection source. For this slice, workspace setup and update are skills-only even when global delivery is `commands` or `both`; command generation for workspaces is deferred. + +`openspec config profile` should remain global, but when it runs from inside an OpenSpec workspace and changes the global profile or delivery settings, it should offer to apply the new workflow selection to the current workspace by running `openspec workspace update`. + +Workspace-local skill selection should be machine-local state: setup records which agents received skills, update refreshes that stored selection by default, and explicit `--tools` changes the stored selection. OpenSpec should detect when workspace-local skills drift from the current global profile and give clear update guidance. + +Selected profile workflows that are not yet fully implemented for workspace-scoped changes should still be safe. Generated skills and CLI guidance must guard unsupported workspace actions instead of falling back to repo-local behavior or editing linked repos implicitly. + +Workspace help, docs, and completions should make the distinction legible: `openspec update` remains repo/project sync, while `openspec workspace update` syncs workspace-local agent skills. + Planning dependency: - Depends on `workspace-open-agent-context`. @@ -44,7 +56,8 @@ Planning dependency: ### Modified Capabilities - `workspace-links`: Adds workspace setup/update behavior for workspace-local agent skill installation. -- `change-creation`: Adds workspace-aware change creation semantics and affected area selection. +- `cli-config`: Makes `openspec config profile` aware of workspace roots and able to apply global profile changes to the current workspace. +- `change-creation`: Adds workspace-aware change creation semantics and affected area identification. - `cli-artifact-workflow`: Enriches workflow status and instructions so agents can discover planning context and artifact paths without hardcoded repo-local assumptions. - `artifact-graph`: Adds a built-in workspace planning schema for workspace-scoped changes. - `schema-resolution`: Ensures workspace-scoped change creation and workflow commands can resolve the workspace planning schema. @@ -56,5 +69,10 @@ Planning dependency: - Workspace-specific planning schema and templates. - Affected area metadata and validation. - Workspace setup and update behavior for installing or refreshing agent skills in the workspace root. +- Global profile integration for workspace-local skill workflow selection. +- Workspace-aware `openspec config profile` apply prompt behavior. +- Workspace-local agent skill selection state and drift detection. +- Guarded workflow guidance for profile workflows whose workspace behavior is not implemented in this slice. +- Docs, help, and completions for workspace skill update behavior. - Agent instructions for proposing cross-repo changes without hardcoded change paths. - Tests that registered repos are visible before change creation and that creating a change does not imply repo-local artifact creation. diff --git a/openspec/changes/workspace-change-planning/specs/artifact-graph/spec.md b/openspec/changes/workspace-change-planning/specs/artifact-graph/spec.md index d7a7234bf..84b44f39d 100644 --- a/openspec/changes/workspace-change-planning/specs/artifact-graph/spec.md +++ b/openspec/changes/workspace-change-planning/specs/artifact-graph/spec.md @@ -10,13 +10,25 @@ The artifact graph SHALL provide a built-in workspace planning schema for worksp #### Scenario: Workspace planning schema artifacts - **WHEN** the `workspace-planning` schema is loaded -- **THEN** it SHALL include artifacts for a shared proposal, affected areas, cross-area design, and coordination tasks -- **AND** the affected areas artifact SHALL be distinct from delivery slices or phases +- **THEN** it SHALL include the normal planning artifacts for a shared proposal, workspace-scoped specs, cross-area design, and coordination tasks +- **AND** it SHALL not require an additional area manifest outside those normal planning artifacts + +#### Scenario: Workspace planning schema supports nested specs +- **WHEN** the `workspace-planning` schema defines its specs artifact +- **THEN** the specs artifact SHALL resolve workspace-scoped spec files under `specs/**/*.md` +- **AND** schema guidance SHALL describe `specs///spec.md` as the default convention for area-specific requirements #### Scenario: Workspace planning schema templates - **WHEN** artifact instructions are requested for the `workspace-planning` schema - **THEN** the schema SHALL provide templates that guide agents to write workspace-level planning content - **AND** those templates SHALL avoid instructing agents to create repo-local implementation artifacts +- **AND** specs instructions SHALL support organizing area-specific requirements under workspace-scoped `specs/` paths + +#### Scenario: Workspace nested spec paths stay workspace-scoped +- **GIVEN** a workspace change has spec files under `specs///spec.md` +- **WHEN** OpenSpec reports status or artifact instructions for the workspace change +- **THEN** it SHALL preserve the concrete nested workspace spec paths +- **AND** it SHALL not treat those files as repo-local specs to sync or archive without an explicit affected-area implementation context #### Scenario: Workspace planning apply readiness - **WHEN** the `workspace-planning` schema defines apply readiness diff --git a/openspec/changes/workspace-change-planning/specs/change-creation/spec.md b/openspec/changes/workspace-change-planning/specs/change-creation/spec.md index 0e2c5f29e..b0191e073 100644 --- a/openspec/changes/workspace-change-planning/specs/change-creation/spec.md +++ b/openspec/changes/workspace-change-planning/specs/change-creation/spec.md @@ -11,12 +11,19 @@ Change creation SHALL support both repo-local and workspace planning homes. - **AND** it SHALL use the `workspace-planning` schema when no explicit schema is provided #### Scenario: Creating a change from inside a workspace -- **GIVEN** the command runs from a subdirectory of an OpenSpec workspace +- **GIVEN** the command runs from a subdirectory of an OpenSpec workspace planning home - **WHEN** the user creates a new change - **THEN** OpenSpec SHALL resolve the current workspace as the planning home - **AND** it SHALL create the change under that workspace's planning path - **AND** it SHALL use the `workspace-planning` schema when no explicit schema is provided +#### Scenario: Creating a change from inside a linked repo +- **GIVEN** a repo or folder is registered as a workspace link +- **AND** the command runs from inside that linked repo or folder rather than from the workspace planning home +- **WHEN** the user creates a new change without explicitly selecting a workspace +- **THEN** OpenSpec SHALL preserve repo-local change creation behavior for that location +- **AND** it SHALL not create a workspace-scoped change merely because the location is registered as a workspace link + #### Scenario: Preserving repo-local change creation - **GIVEN** the command runs outside an OpenSpec workspace - **WHEN** the user creates a new change in a repo-local OpenSpec project diff --git a/openspec/changes/workspace-change-planning/specs/cli-artifact-workflow/spec.md b/openspec/changes/workspace-change-planning/specs/cli-artifact-workflow/spec.md index 09a5feb82..5a1c91232 100644 --- a/openspec/changes/workspace-change-planning/specs/cli-artifact-workflow/spec.md +++ b/openspec/changes/workspace-change-planning/specs/cli-artifact-workflow/spec.md @@ -12,12 +12,13 @@ The status command SHALL provide machine-readable planning context for repo-loca - **WHEN** a user runs `openspec status --change --json` - **THEN** the output SHALL include concrete paths for existing artifacts - **AND** agents SHALL be able to read those paths without assuming `openspec/changes//` +- **AND** workspace-scoped nested spec paths SHALL be reported without flattening the area or capability path #### Scenario: Reporting workspace affected areas - **GIVEN** the change is workspace-scoped - **WHEN** a user runs `openspec status --change --json` - **THEN** the output SHALL include known affected areas -- **AND** it SHALL indicate when affected areas are unresolved +- **AND** it SHALL indicate when affected areas remain unresolved without requiring an additional area manifest artifact #### Scenario: Reporting next steps - **WHEN** a user runs `openspec status --change --json` @@ -75,6 +76,13 @@ Generated workflow skills SHALL use OpenSpec CLI output as the source of truth f - **THEN** they SHALL avoid hardcoded examples that require changes to live under `openspec/changes//` - **AND** any examples SHALL defer to CLI-reported paths for repo-local and workspace-scoped changes +#### Scenario: Skills guard unsupported workspace workflows +- **GIVEN** a generated workflow skill is selected by the global profile +- **AND** the workflow does not yet have full workspace-scoped behavior in this slice +- **WHEN** the skill is used for a workspace-scoped change +- **THEN** it SHALL tell the agent that the workspace action is not supported yet +- **AND** it SHALL not instruct the agent to fall back to repo-local paths or edit linked repos without an explicit allowed edit root + ### Requirement: Workspace schema instructions Workflow commands SHALL use the workspace planning schema instructions for workspace-scoped changes that use that schema. @@ -82,10 +90,11 @@ Workflow commands SHALL use the workspace planning schema instructions for works - **GIVEN** a workspace-scoped change uses schema `workspace-planning` - **WHEN** a user runs `openspec status --change --json` - **THEN** the artifact list SHALL reflect the workspace planning schema -- **AND** it SHALL include the affected areas artifact +- **AND** it SHALL include the normal proposal, specs, design, and tasks artifacts -#### Scenario: Workspace areas instructions +#### Scenario: Workspace specs instructions - **GIVEN** a workspace-scoped change uses schema `workspace-planning` -- **WHEN** a user requests instructions for the affected areas artifact -- **THEN** instruction output SHALL guide the agent to capture known, suspected, and unresolved affected areas +- **WHEN** a user requests instructions for the specs artifact +- **THEN** instruction output SHALL guide the agent to organize area-specific requirements under workspace-scoped `specs/` paths - **AND** it SHALL not require all affected areas to be finalized before planning can continue +- **AND** it SHALL not instruct the agent to create repo-local spec files while the change is still in workspace planning diff --git a/openspec/changes/workspace-change-planning/specs/cli-config/spec.md b/openspec/changes/workspace-change-planning/specs/cli-config/spec.md new file mode 100644 index 000000000..356957146 --- /dev/null +++ b/openspec/changes/workspace-change-planning/specs/cli-config/spec.md @@ -0,0 +1,55 @@ +## ADDED Requirements + +### Requirement: Config profile applies to current workspace +The `openspec config profile` command SHALL remain global while offering an explicit workspace apply path when run from inside an OpenSpec workspace. + +#### Scenario: Config profile run inside a workspace +- **GIVEN** the command runs from inside an OpenSpec workspace +- **WHEN** the user changes profile or delivery settings with interactive `openspec config profile` +- **THEN** OpenSpec SHALL save the global config changes +- **AND** it SHALL prompt: `Apply changes to this workspace now?` + +#### Scenario: User confirms workspace apply +- **GIVEN** `openspec config profile` changed global profile or delivery settings inside a workspace +- **WHEN** the user confirms the workspace apply prompt +- **THEN** OpenSpec SHALL run `openspec workspace update` for the current workspace +- **AND** it SHALL not run repo-local `openspec update` unless the current planning home is repo-local + +#### Scenario: User declines workspace apply +- **GIVEN** `openspec config profile` changed global profile or delivery settings inside a workspace +- **WHEN** the user declines the workspace apply prompt +- **THEN** OpenSpec SHALL explain that global config was updated +- **AND** it SHALL tell the user to run `openspec workspace update` later to apply the profile to workspace-local skills +- **AND** it SHALL not modify workspace skill files + +#### Scenario: No-op inside workspace +- **GIVEN** the command runs from inside an OpenSpec workspace +- **WHEN** `openspec config profile` exits with no effective config changes +- **THEN** OpenSpec SHALL not prompt to apply changes +- **AND** it SHALL warn if workspace-local skills are out of sync with the current global profile +- **AND** the warning SHALL suggest `openspec workspace update` + +#### Scenario: Core preset shortcut inside a workspace +- **GIVEN** the command runs from inside an OpenSpec workspace +- **WHEN** the user runs `openspec config profile core` +- **THEN** OpenSpec SHALL save the global config change without prompting to apply immediately +- **AND** it SHALL tell the user to run `openspec workspace update` to apply the profile to workspace-local skills + +#### Scenario: Core preset shortcut inside a repo project +- **GIVEN** the command runs from inside a repo-local OpenSpec project +- **WHEN** the user runs `openspec config profile core` +- **THEN** OpenSpec SHALL preserve existing repo-local shortcut behavior +- **AND** it SHALL tell the user to run `openspec update` to apply the profile to project files + +#### Scenario: Workspace planning home wins over linked repo project +- **GIVEN** the command runs in a path under a workspace planning home where a repo-local OpenSpec project could also be detected +- **WHEN** OpenSpec decides which apply prompt to show +- **THEN** the nearest current planning home SHALL determine whether to offer `openspec workspace update` or repo-local `openspec update` +- **AND** OpenSpec SHALL not apply profile changes to a linked repo when the current planning home is the workspace + +#### Scenario: Linked repo keeps repo-local profile behavior +- **GIVEN** a repo-local OpenSpec project is registered as a workspace link +- **AND** the command runs from inside that linked repo rather than from the workspace planning home +- **WHEN** OpenSpec decides which apply prompt or guidance to show +- **THEN** OpenSpec SHALL preserve repo-local `openspec update` behavior for that repo +- **AND** it SHALL not offer `openspec workspace update` unless the workspace is explicitly selected diff --git a/openspec/changes/workspace-change-planning/specs/cli-update/spec.md b/openspec/changes/workspace-change-planning/specs/cli-update/spec.md new file mode 100644 index 000000000..71b234225 --- /dev/null +++ b/openspec/changes/workspace-change-planning/specs/cli-update/spec.md @@ -0,0 +1,21 @@ +## ADDED Requirements + +### Requirement: Repo update redirects 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 +- **GIVEN** the command runs from an OpenSpec workspace root +- **WHEN** the user runs `openspec update` +- **THEN** OpenSpec SHALL not generate repo-local project files in the workspace root +- **AND** it SHALL tell the user to run `openspec workspace update` + +#### Scenario: Running update from inside a workspace planning directory +- **GIVEN** the command runs from a subdirectory of an OpenSpec workspace planning home +- **WHEN** the user runs `openspec update` +- **THEN** OpenSpec SHALL not run repo-local update behavior +- **AND** it SHALL tell the user to run `openspec workspace update` + +#### Scenario: Running update from a repo-local project +- **GIVEN** the command runs from inside a repo-local OpenSpec project +- **WHEN** the user runs `openspec update` +- **THEN** OpenSpec SHALL preserve existing repo-local update behavior diff --git a/openspec/changes/workspace-change-planning/specs/workspace-change-planning/spec.md b/openspec/changes/workspace-change-planning/specs/workspace-change-planning/spec.md index ef97aa3a5..2fa852533 100644 --- a/openspec/changes/workspace-change-planning/specs/workspace-change-planning/spec.md +++ b/openspec/changes/workspace-change-planning/specs/workspace-change-planning/spec.md @@ -13,8 +13,9 @@ OpenSpec SHALL support workspace-level changes whose shared plan lives in the wo #### Scenario: Workspace planning artifact structure - **GIVEN** a workspace change uses the workspace planning schema - **WHEN** OpenSpec reports or creates planning artifacts for that change -- **THEN** it SHALL use workspace-level artifacts for proposal, affected areas, cross-area design, and coordination tasks +- **THEN** it SHALL use workspace-level artifacts for proposal, specs, cross-area design, and coordination tasks - **AND** those artifacts SHALL live under the workspace change root +- **AND** it SHALL not require an additional area manifest outside those normal planning artifacts #### Scenario: Capturing the shared goal once - **WHEN** a workspace change is proposed @@ -38,7 +39,14 @@ OpenSpec SHALL represent ownership or implementation boundaries in a workspace c #### Scenario: Planning before all areas are known - **WHEN** a user is still exploring a workspace change - **THEN** OpenSpec SHALL allow the shared plan to exist before all affected areas are finalized -- **AND** it SHALL keep unresolved affected area state visible to agents +- **AND** it SHALL keep unresolved affected area questions visible in the normal planning artifacts and status output + +#### Scenario: Organizing requirements by area +- **GIVEN** a workspace change has requirements owned by one or more affected areas +- **WHEN** OpenSpec reports or creates workspace-scoped specs +- **THEN** it SHALL allow area-specific requirements to be organized under `specs///spec.md` +- **AND** it SHALL not require separate area folders outside the normal `specs/` artifact tree +- **AND** it SHALL preserve the area-or-repo path segment as workspace planning context rather than flattening it into a repo-local capability name #### Scenario: Separating areas from delivery slices - **WHEN** a workspace change reports affected areas diff --git a/openspec/changes/workspace-change-planning/specs/workspace-links/spec.md b/openspec/changes/workspace-change-planning/specs/workspace-links/spec.md index 0a1f94b4a..4e050dd9e 100644 --- a/openspec/changes/workspace-change-planning/specs/workspace-links/spec.md +++ b/openspec/changes/workspace-change-planning/specs/workspace-links/spec.md @@ -18,12 +18,26 @@ OpenSpec SHALL let users install OpenSpec agent skills into a workspace during w - **WHEN** workspace setup completes with one or more selected agents - **THEN** OpenSpec SHALL generate or refresh OpenSpec skill files under the workspace root for each selected agent - **AND** it SHALL report which agents received skills +- **AND** it SHALL store the selected agents in workspace-local machine state + +#### Scenario: Installing profile-selected workflows +- **GIVEN** global config resolves to a workflow profile +- **WHEN** workspace setup installs agent skills +- **THEN** OpenSpec SHALL install workspace-local skills for the workflows selected by that profile +- **AND** it SHALL treat `--tools` as agent selection, not workflow selection +- **AND** it SHALL record the last applied workflow IDs for drift detection #### Scenario: Installing skills only during setup - **WHEN** workspace setup installs agent skills - **THEN** OpenSpec SHALL generate skill files only - **AND** it SHALL not generate slash command files or global command files as part of workspace setup +#### Scenario: Ignoring command delivery for workspace setup +- **GIVEN** global config delivery is `commands` or `both` +- **WHEN** workspace setup installs agent skills +- **THEN** OpenSpec SHALL still generate workspace-local skills only +- **AND** it SHALL report that workspace command generation is not part of this slice + #### Scenario: Preserving linked repos during skill installation - **WHEN** workspace setup installs agent skills - **THEN** OpenSpec SHALL leave linked repos and folders unchanged @@ -34,6 +48,12 @@ OpenSpec SHALL let users install OpenSpec agent skills into a workspace during w - **THEN** OpenSpec SHALL use the selected tool set for workspace agent skill installation - **AND** it SHALL validate tool IDs using the same supported tool IDs as skill generation for repo initialization +#### Scenario: Non-interactive setup without tool selection +- **WHEN** non-interactive workspace setup omits `--tools` +- **THEN** OpenSpec SHALL create the workspace without installing agent skills +- **AND** it SHALL report that no workspace skills were installed +- **AND** it SHALL tell the user to run `openspec workspace update --tools ` to install skills later + #### Scenario: Reporting setup skills in JSON output - **WHEN** non-interactive workspace setup installs agent skills with JSON output enabled - **THEN** OpenSpec SHALL include generated, refreshed, skipped, or failed skill installation results in machine-readable output @@ -61,17 +81,60 @@ OpenSpec SHALL provide a workspace update flow for refreshing agent skills after - **THEN** OpenSpec SHALL refresh OpenSpec skills for selected agents - **AND** it SHALL add skills for newly selected agents - **AND** it SHALL remove OpenSpec-managed workflow skill directories for agents that are no longer selected +- **AND** it SHALL update the stored workspace-local selected agent list + +#### Scenario: Updating profile-selected workflows +- **GIVEN** global config resolves to a workflow profile +- **WHEN** workspace update refreshes workspace-local skills +- **THEN** OpenSpec SHALL sync the workspace-local skill workflow set to the workflows selected by that profile +- **AND** deselected workflow skill directories SHALL be removed only when they are known OpenSpec-managed workflow skill directories +- **AND** it SHALL update the last applied workflow IDs used for drift detection + +#### Scenario: Ignoring command delivery for workspace update +- **GIVEN** global config delivery is `commands` or `both` +- **WHEN** workspace update refreshes workspace-local skills +- **THEN** OpenSpec SHALL still update workspace-local skills only +- **AND** it SHALL not generate slash command files or global command files #### Scenario: Removing only managed skill directories - **WHEN** workspace update removes skills for an unselected agent - **THEN** OpenSpec SHALL remove only known OpenSpec-managed workflow skill directories - **AND** it SHALL preserve unrelated files in the agent directory +#### Scenario: Updating stored agent selection by flag +- **WHEN** workspace update receives `--tools ` or `--tools none` +- **THEN** OpenSpec SHALL replace the stored workspace-local selected agent list with that selection +- **AND** future workspace updates without `--tools` SHALL use the stored selection + #### Scenario: Non-interactive update tool selection - **WHEN** workspace update receives `--tools all`, `--tools none`, or `--tools ` - **THEN** OpenSpec SHALL update workspace agent skills using that selected tool set - **AND** it SHALL avoid prompting for agent selection +#### Scenario: Non-interactive update without tool selection +- **GIVEN** workspace-local selected agents are stored +- **WHEN** non-interactive workspace update omits `--tools` +- **THEN** OpenSpec SHALL refresh the stored selected agents using the active global profile +- **AND** it SHALL avoid prompting for agent selection + +#### Scenario: Non-interactive update without stored selection +- **GIVEN** no workspace-local selected agents are stored +- **WHEN** non-interactive workspace update omits `--tools` +- **THEN** OpenSpec SHALL complete without installing agent skills +- **AND** it SHALL report a no-op with guidance to pass `--tools` + +#### Scenario: Reporting workspace skill drift +- **GIVEN** workspace-local skill state records last applied workflow IDs +- **AND** the active global profile resolves to a different workflow set +- **WHEN** OpenSpec reports workspace skill state +- **THEN** it SHALL report that workspace-local skills are out of sync with the global profile +- **AND** it SHALL suggest `openspec workspace update` + +#### Scenario: Reporting clean workspace skill sync +- **GIVEN** workspace-local skill state matches the active global profile and selected agents +- **WHEN** OpenSpec reports workspace skill state +- **THEN** it SHALL not report profile drift + #### Scenario: Reporting workspace skill update results - **WHEN** workspace update changes agent skill state - **THEN** OpenSpec SHALL report which agents were refreshed, added, removed, skipped, or failed @@ -79,3 +142,22 @@ OpenSpec SHALL provide a workspace update flow for refreshing agent skills after #### Scenario: Reporting workspace update results in JSON output - **WHEN** workspace update runs with JSON output enabled - **THEN** OpenSpec SHALL include refreshed, added, removed, skipped, or failed skill results in machine-readable output + +### Requirement: Workspace skill update surface is documented +OpenSpec SHALL expose workspace skill setup/update behavior in user-facing command surfaces. + +#### Scenario: Workspace update appears in help +- **WHEN** a user runs `openspec workspace --help` +- **THEN** OpenSpec SHALL list `workspace update` +- **AND** it SHALL describe it as refreshing workspace-local agent skills + +#### Scenario: Workspace update options appear in help +- **WHEN** a user runs `openspec workspace update --help` +- **THEN** OpenSpec SHALL document workspace selection options +- **AND** it SHALL document `--tools all|none|` +- **AND** it SHALL state that global profile selects workflows and `--tools` selects agents + +#### Scenario: Workspace update appears in completions +- **WHEN** shell completions are generated +- **THEN** the workspace command registry SHALL include `workspace update` +- **AND** it SHALL include relevant options such as `--workspace`, `--tools`, `--json`, and `--no-interactive` diff --git a/openspec/changes/workspace-change-planning/tasks.md b/openspec/changes/workspace-change-planning/tasks.md index 7c75841a5..eaaa0da1d 100644 --- a/openspec/changes/workspace-change-planning/tasks.md +++ b/openspec/changes/workspace-change-planning/tasks.md @@ -1,53 +1,110 @@ -## 1. Workspace Setup Skills +## Phase 1: Workspace Setup Skills -- [ ] 1.1 Add an interactive workspace setup step named "Install agent skills" that asks which agents should get OpenSpec skills in this workspace. -- [ ] 1.2 Preselect the preferred opener when that opener supports skills, while allowing users to choose different or additional agents. -- [ ] 1.3 Support non-interactive agent selection with the existing `--tools all|none|` style. -- [ ] 1.4 Validate workspace setup tool IDs using the same supported skill-generation tool set as repo initialization. -- [ ] 1.5 Ensure `openspec workspace setup` generates or refreshes OpenSpec agent skills in the workspace root for the selected agents. -- [ ] 1.6 Keep setup-time skill generation scoped to the workspace planning home; do not write skills or OpenSpec artifacts into linked repos or folders during workspace setup. -- [ ] 1.7 Keep workspace setup skill generation skills-only for this slice; do not generate slash commands or global command files. -- [ ] 1.8 Define how setup reports generated, refreshed, skipped, or failed skill installation work in human and JSON output. +User-testable outcome: A user can run workspace setup, choose which agents get the active profile's OpenSpec skills, and verify the selected skills are generated in the workspace root only. -## 2. Workspace Skill Updates +- [x] 1.1 Add an interactive workspace setup step named "Install agent skills" that asks which agents should get OpenSpec skills in this workspace. +- [x] 1.2 Preselect the preferred opener when that opener supports skills, while allowing users to choose different or additional agents. +- [x] 1.3 Support non-interactive agent selection with the existing `--tools all|none|` style. +- [x] 1.4 Validate workspace setup tool IDs using the same supported skill-generation tool set as repo initialization. +- [x] 1.5 Resolve the active global profile and use it to choose which workflow skills workspace setup installs. +- [x] 1.6 Ensure `openspec workspace setup` generates or refreshes OpenSpec agent skills in the workspace root for the selected agents. +- [x] 1.7 Keep setup-time skill generation scoped to the workspace planning home; do not write skills or OpenSpec artifacts into linked repos or folders during workspace setup. +- [x] 1.8 Keep workspace setup skill generation skills-only for this slice; do not generate slash commands or global command files even when global delivery includes commands. +- [x] 1.9 Define how setup reports generated, refreshed, skipped, failed, and skills-only delivery work in human and JSON output. +- [x] 1.10 Store the selected workspace skill agents and last-applied workflow IDs in workspace-local machine state. +- [x] 1.11 Preserve non-interactive setup compatibility when `--tools` is omitted by skipping skill installation with clear guidance. +- [x] 1.12 Manually run workspace setup in interactive and non-interactive modes and verify the selected profile workflows land only in the workspace root. +- [x] 1.13 Review the setup UX: prompt wording, defaults, skip path, profile/delivery messaging, success output, and JSON output are clear before moving on. + +## Phase 2: Workspace Skill Updates + +User-testable outcome: A user can change the global profile, run workspace update in an existing workspace, and see workspace-local skills refresh to the selected workflows with clear human and JSON output. - [ ] 2.1 Add a workspace update flow that refreshes, adds, or removes OpenSpec agent skills in an existing workspace. - [ ] 2.2 Let `openspec workspace update` resolve the current workspace when run from inside a workspace. - [ ] 2.3 Support named and selected-workspace update forms such as `openspec workspace update platform` and `openspec workspace update --workspace platform`. - [ ] 2.4 Support non-interactive update forms such as `openspec workspace update platform --tools codex,claude`. - [ ] 2.5 Remove only known OpenSpec-managed workflow skill directories for agents that are no longer selected. -- [ ] 2.6 Define how update reports refreshed, added, removed, skipped, or failed skill work in human and JSON output. +- [ ] 2.6 Sync workspace-local workflow skill directories to the current global profile selection. +- [ ] 2.7 Keep workspace update skills-only for this slice; do not generate slash commands or global command files even when global delivery includes commands. +- [ ] 2.8 Define how update reports refreshed, added, removed, skipped, failed, and skills-only delivery work in human and JSON output. +- [ ] 2.9 Use stored selected agents when workspace update runs without `--tools`, and update that stored selection when `--tools` is passed. +- [ ] 2.10 Detect workspace-local skill drift from the active global profile and report `openspec workspace update` guidance. +- [ ] 2.11 Manually run workspace update for refresh, add, remove, no-op, omitted-`--tools`, and profile-change cases and verify linked repos remain unchanged. +- [ ] 2.12 Review the update UX: command forms, current-workspace detection, profile/delivery messaging, drift messaging, removal messaging, and JSON output are understandable. + +## Phase 3: Config Profile Workspace Apply + +User-testable outcome: A user can run `openspec config profile` inside a workspace and choose whether to apply the changed global profile to that workspace now. + +- [ ] 3.1 Detect when `openspec config profile` runs from inside an OpenSpec workspace. +- [ ] 3.2 After an actual profile or delivery change inside a workspace, prompt to apply changes to the current workspace now. +- [ ] 3.3 When confirmed, run `openspec workspace update` for the current workspace instead of repo-local `openspec update`. +- [ ] 3.4 When declined, report that global config changed and that `openspec workspace update` applies it later. +- [ ] 3.5 Preserve existing repo-local `openspec config profile` apply behavior outside workspaces. +- [ ] 3.6 Keep `openspec config profile core` non-interactive, but print workspace-specific `openspec workspace update` guidance when run inside a workspace. +- [ ] 3.7 Warn on no-op config profile inside a workspace when workspace-local skills drift from the active global profile. +- [ ] 3.8 Manually run `openspec config profile` inside a workspace for confirm, decline, no-op, drift-warning, and `core` preset paths. +- [ ] 3.9 Review the config-profile UX: prompt wording, project/workspace distinction, no-op behavior, preset guidance, and follow-up guidance are clear. + +## Phase 4: Workspace Change Creation + +User-testable outcome: A user can create a workspace-level change from the coordination root, inspect its workspace planning artifacts, and confirm linked repos were not edited. + +- [ ] 4.1 Add a built-in `workspace-planning` schema and templates that keep the normal proposal/specs/design/tasks artifact shape. +- [ ] 4.2 Define the workspace-planning specs artifact with nested `specs/**/*.md` output support and instructions for `specs///spec.md`. +- [ ] 4.3 Add workspace-aware change creation from the workspace coordination root. +- [ ] 4.4 Default workspace-scoped change creation to the `workspace-planning` schema. +- [ ] 4.5 Store workspace-level changes under the workspace planning path rather than under linked repos or folders. +- [ ] 4.6 Capture the product goal once at the workspace change level. +- [ ] 4.7 Record or validate affected area names through workspace-scoped specs or task sections using registered workspace link names where applicable. +- [ ] 4.8 Ensure creating a workspace change does not create repo-local OpenSpec artifacts or edit linked repos. +- [ ] 4.9 Preserve repo-local change creation behavior outside workspaces. +- [ ] 4.10 Manually create a workspace change from a coordination root and verify the generated artifacts, workspace-scoped specs/tasks, affected areas, and untouched linked repos. +- [ ] 4.11 Review the change creation UX: goal capture, affected-area identification, artifact paths, and next-step guidance feel clear. + +## Phase 5: Planning Home And Agent Context -## 3. Workspace Change Creation +User-testable outcome: A user can run status and instructions for repo-local and workspace changes and see the resolved planning home, artifact paths, affected areas, constraints, and next steps. -- [ ] 3.1 Add a built-in `workspace-planning` schema and templates. -- [ ] 3.2 Add workspace-aware change creation from the workspace coordination root. -- [ ] 3.3 Default workspace-scoped change creation to the `workspace-planning` schema. -- [ ] 3.4 Store workspace-level changes under the workspace planning path rather than under linked repos or folders. -- [ ] 3.5 Capture the product goal once at the workspace change level. -- [ ] 3.6 Record or validate affected areas using registered workspace link names where applicable. -- [ ] 3.7 Ensure creating a workspace change does not create repo-local OpenSpec artifacts or edit linked repos. -- [ ] 3.8 Preserve repo-local change creation behavior outside workspaces. +- [ ] 5.1 Introduce a shared planning-home resolver that identifies repo-local versus workspace planning homes. +- [ ] 5.2 Enrich `openspec status --change --json` with planning home, change root, relevant artifact paths, affected areas, next steps, and action context. +- [ ] 5.3 Enrich `openspec instructions --change --json` with resolved artifact paths for repo-local and workspace-scoped changes. +- [ ] 5.4 Keep workspace-level planning as the source of truth until an explicit implementation workflow selects an affected area. +- [ ] 5.5 Preserve nested workspace spec paths in status and instructions output without flattening them into repo-local capability paths. +- [ ] 5.6 Manually run status and instructions for both repo-local and workspace-scoped changes and verify paths and action context are correct. +- [ ] 5.7 Review the planning-context UX: human output, JSON field names, and next-step guidance are easy for users and agents to follow. -## 4. Planning Home And Agent Context +## Phase 6: Workflow Skill Instructions -- [ ] 4.1 Introduce a shared planning-home resolver that identifies repo-local versus workspace planning homes. -- [ ] 4.2 Enrich `openspec status --change --json` with planning home, change root, relevant artifact paths, affected areas, next steps, and action context. -- [ ] 4.3 Enrich `openspec instructions --change --json` with resolved artifact paths for repo-local and workspace-scoped changes. -- [ ] 4.4 Keep workspace-level planning as the source of truth until an explicit implementation workflow selects an affected area. +User-testable outcome: A user can inspect regenerated workflow skills and verify they are path-agnostic and tell agents to use CLI-reported artifact paths. -## 5. Workflow Skill Instructions +- [ ] 6.1 Update generated workflow skill templates to run `openspec status --change --json` before artifact work and trust returned planning context. +- [ ] 6.2 Update generated workflow skill templates to run `openspec instructions --change --json` before writing artifacts and use the resolved output path. +- [ ] 6.3 Audit source workflow templates for hardcoded `openspec/changes/` assumptions and replace them with CLI-reported path guidance. +- [ ] 6.4 Keep a separate artifact-context command out of this slice unless enriched status/instructions prove insufficient during implementation. +- [ ] 6.5 Manually regenerate or inspect installed workflow skills and verify they follow CLI-reported artifact paths in a workspace change. +- [ ] 6.6 Guard profile-selected workflow skills whose workspace behavior is not implemented yet so they do not fall back to repo-local paths or edit linked repos. +- [ ] 6.7 Review the agent-instruction UX: instructions are concise, path-agnostic, safe for unsupported workspace workflows, and practical for both repo-local and workspace planning. -- [ ] 5.1 Update generated workflow skill templates to run `openspec status --change --json` before artifact work and trust returned planning context. -- [ ] 5.2 Update generated workflow skill templates to run `openspec instructions --change --json` before writing artifacts and use the resolved output path. -- [ ] 5.3 Audit source workflow templates for hardcoded `openspec/changes/` assumptions and replace them with CLI-reported path guidance. -- [ ] 5.4 Keep a separate artifact-context command out of this slice unless enriched status/instructions prove insufficient during implementation. +## Phase 7: Verification -## 6. Verification +User-testable outcome: A user or reviewer can run the full manual checklist from a clean workspace and compare expected versus actual evidence for every earlier phase. -- [ ] 6.1 Add tests that workspace setup installs skills in the workspace root and leaves linked repos unchanged. -- [ ] 6.2 Add tests that workspace update refreshes, adds, and removes only managed workspace skill directories. -- [ ] 6.3 Add tests that registered repos are visible before change creation. -- [ ] 6.4 Add tests that workspace change creation does not imply repo-local artifact creation. -- [ ] 6.5 Add cross-platform path tests for workspace-root skill paths and workspace change paths. -- [ ] 6.6 Run `openspec validate workspace-change-planning --strict`. +- [ ] 7.1 Add tests that workspace setup installs skills in the workspace root and leaves linked repos unchanged. +- [ ] 7.2 Add tests that workspace update refreshes, adds, and removes only managed workspace skill directories. +- [ ] 7.3 Add tests that workspace setup/update use the current global profile for workflow skill selection while keeping workspace delivery skills-only. +- [ ] 7.4 Add tests that `openspec config profile` inside a workspace can apply changes through `openspec workspace update`. +- [ ] 7.5 Add tests for stored workspace skill agent selection, omitted-`--tools` behavior, and profile drift reporting. +- [ ] 7.6 Add tests that `openspec update` from a workspace planning home redirects to `openspec workspace update`. +- [ ] 7.7 Add tests that unsupported workspace workflow skills are guarded and do not instruct repo-local fallback edits. +- [ ] 7.8 Add tests that registered repos are visible before change creation. +- [ ] 7.9 Add tests that workspace change creation does not imply repo-local artifact creation. +- [ ] 7.10 Add tests that the workspace-planning schema resolves nested `specs///spec.md` files as workspace-scoped specs. +- [ ] 7.11 Add cross-platform path tests for workspace-root skill paths and workspace change paths. +- [ ] 7.12 Update CLI docs, command help, and shell completion coverage for `workspace update`, `--tools`, profile behavior, and workspace skills-only delivery. +- [ ] 7.13 Run `openspec validate workspace-change-planning --strict`. +- [ ] 7.14 Run the full manual acceptance checklist across setup, update, config profile, change creation, planning context, and workflow skills before marking the change complete. +- [ ] 7.15 Complete a final UX review across the whole workflow and record any follow-up fixes or intentional deferrals. +- [ ] 7.16 Before implementation sign-off, record the manual commands or interaction paths, expected observations, and actual observations for each phase. +- [ ] 7.17 Have a separate reviewer or fresh agent context rerun the manual acceptance and UX checklist when available; otherwise rerun it from a clean temporary workspace and report the evidence. diff --git a/src/commands/workspace.ts b/src/commands/workspace.ts index 6d2eafca7..17809de3e 100644 --- a/src/commands/workspace.ts +++ b/src/commands/workspace.ts @@ -5,12 +5,20 @@ import * as path from 'node:path'; import { WorkspacePreferredOpener, + WorkspaceSkillInstallationReport, + createWorkspaceSkillSkippedReport, + generateWorkspaceAgentSkills, getDefaultWorkspaceOpenerChoiceValue, + getWorkspaceSkillCapableTools, + getWorkspaceSkillToolIds, getWorkspaceOpenerLabel, isWorkspaceAgentOpenerId, listWorkspaceOpenerChoices, parseWorkspacePreferredOpenerValue, + parseWorkspaceSkillToolsValue, listWorkspaceRegistryEntries, + readOptionalWorkspaceLocalState, + writeWorkspaceLocalState, } from '../core/workspace/index.js'; import { isInteractive, resolveNoInteractive } from '../utils/interactive.js'; import { @@ -96,7 +104,7 @@ async function promptWorkspaceName(initialName?: string): Promise { const { input } = await import('@inquirer/prompts'); - console.log(chalk.bold('[1/4] Name the workspace')); + console.log(chalk.bold('[1/5] Name the workspace')); console.log(chalk.dim('Use a stable name for the repo group, e.g. platform.')); console.log(''); @@ -165,7 +173,7 @@ async function promptSetupLinks(): Promise> { const links: Record = {}; console.log(''); - console.log(chalk.bold('[2/4] Link repos or folders')); + console.log(chalk.bold('[2/5] Link repos or folders')); console.log(chalk.dim('Start with the current directory, or enter another repo path.')); console.log(''); @@ -257,6 +265,61 @@ function parseSetupOpenerOption(opener: string | undefined): WorkspacePreferredO } } +function parseSetupToolsOption(tools: string): string[] { + try { + return parseWorkspaceSkillToolsValue(tools); + } catch (error) { + throw new WorkspaceCliError(asErrorMessage(error), 'invalid_workspace_setup_tools', { + target: 'workspace.skills', + fix: `Use --tools all, --tools none, or one of: ${getWorkspaceSkillToolIds().join(', ')}`, + }); + } +} + +function getPreferredWorkspaceSkillAgentId( + preferredOpener: WorkspacePreferredOpener | undefined +): string | null { + if (!preferredOpener || preferredOpener.kind !== 'agent') { + return null; + } + + return getWorkspaceSkillToolIds().includes(preferredOpener.id) ? preferredOpener.id : null; +} + +async function promptWorkspaceSkillAgents( + preferredOpener: WorkspacePreferredOpener | undefined +): Promise { + const { searchableMultiSelect } = await import('../prompts/searchable-multi-select.js'); + const preferredAgentId = getPreferredWorkspaceSkillAgentId(preferredOpener); + const tools = getWorkspaceSkillCapableTools(); + const sortedChoices = tools + .map((tool) => ({ + name: tool.name, + value: tool.value, + preSelected: tool.value === preferredAgentId, + })) + .sort((a, b) => { + if (a.preSelected !== b.preSelected) { + return a.preSelected ? -1 : 1; + } + + return a.name.localeCompare(b.name); + }); + + if (preferredAgentId) { + const preferredTool = tools.find((tool) => tool.value === preferredAgentId); + if (preferredTool) { + console.log(`${preferredTool.name} matches your preferred opener and is pre-selected.`); + } + } + + return searchableMultiSelect({ + message: 'Which agents should get OpenSpec skills in this workspace?', + pageSize: 15, + choices: sortedChoices, + }); +} + function parseAgentOverride(agent: string): WorkspacePreferredOpener { if (!isWorkspaceAgentOpenerId(agent)) { throw new WorkspaceCliError( @@ -406,6 +469,69 @@ function printLinkMutationHuman( console.log(`Workspace: ${payload.workspace.name}`); } +function formatWorkspaceSkillAgentResult(result: { name: string; workflow_ids?: string[] }): string { + const workflowCount = result.workflow_ids?.length ?? 0; + const workflowLabel = workflowCount === 1 ? '1 workflow' : `${workflowCount} workflows`; + return `${result.name} (${workflowLabel})`; +} + +function printWorkspaceSkillReportHuman(report: WorkspaceSkillInstallationReport): void { + console.log('Agent skills:'); + console.log(` Profile: ${report.profile}`); + console.log( + ` Workflows: ${report.workflow_ids.length > 0 ? report.workflow_ids.join(', ') : '(none selected)'}` + ); + + if (report.generated.length > 0) { + console.log(` Generated: ${report.generated.map(formatWorkspaceSkillAgentResult).join(', ')}`); + } + + if (report.refreshed.length > 0) { + console.log(` Refreshed: ${report.refreshed.map(formatWorkspaceSkillAgentResult).join(', ')}`); + } + + if (report.skipped.length > 0) { + for (const skipped of report.skipped) { + const prefix = skipped.name ? `${skipped.name}: ` : ''; + console.log(` Skipped: ${prefix}${skipped.message}`); + } + } + + if (report.failed.length > 0) { + console.log( + chalk.red( + ` Failed: ${report.failed.map((failure) => `${failure.name} (${failure.error})`).join(', ')}` + ) + ); + } + + if (report.delivery_notice) { + console.log(chalk.dim(` ${report.delivery_notice}`)); + } +} + +async function writeWorkspaceSkillState( + workspaceRoot: string, + selectedAgentIds: string[], + report: WorkspaceSkillInstallationReport +): Promise { + const localState = (await readOptionalWorkspaceLocalState(workspaceRoot)) ?? { + version: 1 as const, + paths: {}, + }; + + await writeWorkspaceLocalState(workspaceRoot, { + ...localState, + workspace_skills: { + selected_agents: selectedAgentIds, + last_applied_profile: report.profile, + last_applied_delivery: report.delivery, + last_applied_workflow_ids: report.workflow_ids, + last_applied_at: new Date().toISOString(), + }, + }); +} + async function resolveWorkspaceOpenOpener( localState: { preferred_opener?: WorkspacePreferredOpener }, options: WorkspaceOpenOptions @@ -571,12 +697,24 @@ class WorkspaceCommand { const links = interactive ? await promptSetupLinks() : await parseSetupLinks(options.link); if (interactive) { console.log(''); - console.log(chalk.bold('[3/4] Choose preferred opener')); + console.log(chalk.bold('[3/5] Choose preferred opener')); } const preferredOpener = interactive ? await promptPreferredOpener('Preferred opener:') : parseSetupOpenerOption(options.opener); + let selectedWorkspaceSkillAgents: string[] | undefined; + if (options.tools !== undefined) { + selectedWorkspaceSkillAgents = parseSetupToolsOption(options.tools); + } else if (interactive) { + console.log(''); + console.log(chalk.bold('[4/5] Install agent skills')); + console.log(chalk.dim('Choose which coding agents should get OpenSpec skills in this workspace.')); + console.log(chalk.dim('Press Enter with no agents selected to skip skill installation for now.')); + console.log(''); + selectedWorkspaceSkillAgents = await promptWorkspaceSkillAgents(preferredOpener); + } + if (Object.keys(links).length === 0) { throw new WorkspaceCliError( 'workspace setup --no-interactive requires --name and at least one --link .', @@ -589,10 +727,22 @@ class WorkspaceCommand { if (interactive) { console.log(''); - console.log(chalk.bold('[4/4] Create workspace files')); + console.log(chalk.bold('[5/5] Create workspace files')); } const workspace = await createManagedWorkspace(workspaceName, links, preferredOpener); + const skillReport = + selectedWorkspaceSkillAgents === undefined + ? createWorkspaceSkillSkippedReport( + 'tools_omitted', + 'No workspace skills were installed. Run openspec workspace update --tools to install them later.' + ) + : await generateWorkspaceAgentSkills(workspace.root, selectedWorkspaceSkillAgents); + + if (selectedWorkspaceSkillAgents !== undefined) { + await writeWorkspaceSkillState(workspace.root, selectedWorkspaceSkillAgents, skillReport); + } + const doctorResult = await loadWorkspaceForDoctor({ name: workspace.name, root: workspace.root, @@ -603,6 +753,7 @@ class WorkspaceCommand { if (options.json) { printJson({ workspace: doctorResult.workspace, + workspace_skills: skillReport, status: doctorResult.status, }); return; @@ -617,8 +768,11 @@ class WorkspaceCommand { console.log('Workspace check:'); printWorkspaceCheckSummaryHuman(doctorResult); console.log(''); + printWorkspaceSkillReportHuman(skillReport); + console.log(''); console.log('Next useful commands:'); console.log(` openspec workspace doctor --workspace ${workspace.name}`); + console.log(` openspec workspace update --workspace ${workspace.name} --tools `); console.log(' openspec workspace list'); } catch (error) { this.handleFailure(options.json, { workspace: null, status: [] }, error); @@ -812,6 +966,10 @@ export function registerWorkspaceCommand(program: Command): void { .option('--name ', 'Workspace name') .option('--link ', 'Repo or folder link. Use or =.', collectOption, []) .option('--opener ', 'Preferred opener: codex, claude, github-copilot, or editor') + .option( + '--tools ', + `Install OpenSpec skills for agents. Use "all", "none", or a comma-separated list of: ${getWorkspaceSkillToolIds().join(', ')}` + ) .option('--json', 'Output as JSON') .option('--no-interactive', 'Disable prompts') .action(async (options: WorkspaceSetupOptions) => { diff --git a/src/commands/workspace/types.ts b/src/commands/workspace/types.ts index 1c4c3215a..e5c418dfd 100644 --- a/src/commands/workspace/types.ts +++ b/src/commands/workspace/types.ts @@ -34,6 +34,7 @@ export interface WorkspaceSetupOptions { name?: string; link?: string[]; opener?: string; + tools?: string; json?: boolean; noInteractive?: boolean; interactive?: boolean; diff --git a/src/core/completions/command-registry.ts b/src/core/completions/command-registry.ts index fda6a2ddf..2f50a9d60 100644 --- a/src/core/completions/command-registry.ts +++ b/src/core/completions/command-registry.ts @@ -180,6 +180,11 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [ takesValue: true, values: ['codex', 'claude', 'github-copilot', 'editor'], }, + { + name: 'tools', + description: 'Install OpenSpec skills for agents (all, none, or comma-separated tool IDs)', + takesValue: true, + }, COMMON_FLAGS.json, COMMON_FLAGS.noInteractive, ], diff --git a/src/core/workspace/foundation.ts b/src/core/workspace/foundation.ts index 0e214fa1b..c5ac0aaf5 100644 --- a/src/core/workspace/foundation.ts +++ b/src/core/workspace/foundation.ts @@ -58,6 +58,15 @@ export interface WorkspaceLocalState { version: 1; paths: Record; preferred_opener?: WorkspacePreferredOpener; + workspace_skills?: WorkspaceSkillState; +} + +export interface WorkspaceSkillState { + selected_agents: string[]; + last_applied_profile?: 'core' | 'custom'; + last_applied_delivery?: 'both' | 'skills' | 'commands'; + last_applied_workflow_ids?: string[]; + last_applied_at?: string; } export interface WorkspaceRegistryState { @@ -255,6 +264,16 @@ const LocalStateSchema = z.object({ }) .strict() .optional(), + workspace_skills: z + .object({ + selected_agents: z.array(z.string()), + last_applied_profile: z.enum(['core', 'custom']).optional(), + last_applied_delivery: z.enum(['both', 'skills', 'commands']).optional(), + last_applied_workflow_ids: z.array(z.string()).optional(), + last_applied_at: z.string().optional(), + }) + .strict() + .optional(), }).strict(); const RegistryStateSchema = z.object({ @@ -389,6 +408,7 @@ export function parseWorkspaceLocalState(content: string): WorkspaceLocalState { version: 1, paths: result.data.paths, ...(preferredOpener ? { preferred_opener: preferredOpener } : {}), + ...(result.data.workspace_skills ? { workspace_skills: result.data.workspace_skills } : {}), }; } @@ -450,6 +470,7 @@ export function serializeWorkspaceLocalState(state: WorkspaceLocalState): string version: 1, paths: state.paths, ...(preferredOpener ? { preferred_opener: preferredOpener } : {}), + ...(state.workspace_skills ? { workspace_skills: state.workspace_skills } : {}), }); } diff --git a/src/core/workspace/index.ts b/src/core/workspace/index.ts index 965a7b903..a5630edcf 100644 --- a/src/core/workspace/index.ts +++ b/src/core/workspace/index.ts @@ -2,3 +2,4 @@ export * from './foundation.js'; export * from './link-input.js'; export * from './openers.js'; export * from './open-surface.js'; +export * from './skills.js'; diff --git a/src/core/workspace/skills.ts b/src/core/workspace/skills.ts new file mode 100644 index 000000000..ae468f18c --- /dev/null +++ b/src/core/workspace/skills.ts @@ -0,0 +1,256 @@ +import * as path from 'node:path'; +import { createRequire } from 'node:module'; + +import { FileSystemUtils } from '../../utils/file-system.js'; +import { transformToHyphenCommands } from '../../utils/command-references.js'; +import { AI_TOOLS, type AIToolOption } from '../config.js'; +import { getGlobalConfig, type Delivery, type Profile } from '../global-config.js'; +import { getProfileWorkflows } from '../profiles.js'; +import { + generateSkillContent, + getSkillTemplates, + getToolSkillStatus, + getToolsWithSkillsDir, +} from '../shared/index.js'; + +const require = createRequire(import.meta.url); +const { version: OPENSPEC_VERSION } = require('../../../package.json'); + +export interface WorkspaceSkillAgentResult { + tool_id: string; + name: string; + skills_path: string; + workflow_ids: string[]; +} + +export interface WorkspaceSkillSkippedResult { + tool_id?: string; + name?: string; + reason: string; + message: string; +} + +export interface WorkspaceSkillFailedResult { + tool_id: string; + name: string; + error: string; +} + +export interface WorkspaceSkillInstallationReport { + profile: Profile; + delivery: Delivery; + workflow_ids: string[]; + selected_agents: string[]; + skills_only: true; + delivery_notice: string | null; + generated: WorkspaceSkillAgentResult[]; + refreshed: WorkspaceSkillAgentResult[]; + skipped: WorkspaceSkillSkippedResult[]; + failed: WorkspaceSkillFailedResult[]; +} + +interface WorkspaceSkillProfileContext { + profile: Profile; + delivery: Delivery; + workflowIds: string[]; + deliveryNotice: string | null; +} + +type WorkspaceSkillCapableTool = AIToolOption & { skillsDir: string }; + +function resolveWorkspaceSkillProfileContext(): WorkspaceSkillProfileContext { + const globalConfig = getGlobalConfig(); + const profile = globalConfig.profile ?? 'core'; + const delivery = globalConfig.delivery ?? 'both'; + const workflowIds = [...getProfileWorkflows(profile, globalConfig.workflows)]; + const deliveryNotice = + delivery === 'skills' + ? null + : 'Workspace setup installs skills only; workspace command generation is not part of this slice.'; + + return { + profile, + delivery, + workflowIds, + deliveryNotice, + }; +} + +function makeBaseWorkspaceSkillReport( + selectedAgentIds: string[], + profileContext = resolveWorkspaceSkillProfileContext() +): WorkspaceSkillInstallationReport { + return { + profile: profileContext.profile, + delivery: profileContext.delivery, + workflow_ids: profileContext.workflowIds, + selected_agents: selectedAgentIds, + skills_only: true, + delivery_notice: profileContext.deliveryNotice, + generated: [], + refreshed: [], + skipped: [], + failed: [], + }; +} + +export function getWorkspaceSkillCapableTools(): WorkspaceSkillCapableTool[] { + return AI_TOOLS.filter((tool) => Boolean(tool.skillsDir)) as WorkspaceSkillCapableTool[]; +} + +export function getWorkspaceSkillToolIds(): string[] { + return getToolsWithSkillsDir(); +} + +export function parseWorkspaceSkillToolsValue(rawTools: string): string[] { + const raw = rawTools.trim(); + if (raw.length === 0) { + throw new Error( + 'The --tools option requires a value. Use "all", "none", or a comma-separated list of agent IDs.' + ); + } + + const availableTools = getWorkspaceSkillToolIds(); + const availableSet = new Set(availableTools); + const availableList = ['all', 'none', ...availableTools].join(', '); + const lowerRaw = raw.toLowerCase(); + + if (lowerRaw === 'all') { + return availableTools; + } + + if (lowerRaw === 'none') { + return []; + } + + const tokens = raw + .split(',') + .map((token) => token.trim()) + .filter((token) => token.length > 0); + + if (tokens.length === 0) { + throw new Error( + 'The --tools option requires at least one agent ID when not using "all" or "none".' + ); + } + + const normalizedTokens = tokens.map((token) => token.toLowerCase()); + + if (normalizedTokens.some((token) => token === 'all' || token === 'none')) { + throw new Error('Cannot combine reserved values "all" or "none" with specific agent IDs.'); + } + + const invalidTokens = tokens.filter( + (_token, index) => !availableSet.has(normalizedTokens[index]) + ); + + if (invalidTokens.length > 0) { + throw new Error(`Invalid agent(s): ${invalidTokens.join(', ')}. Available values: ${availableList}`); + } + + const deduped: string[] = []; + for (const token of normalizedTokens) { + if (!deduped.includes(token)) { + deduped.push(token); + } + } + + return deduped; +} + +export function createWorkspaceSkillSkippedReport( + reason: string, + message: string +): WorkspaceSkillInstallationReport { + const report = makeBaseWorkspaceSkillReport([]); + report.skipped.push({ + reason, + message, + }); + return report; +} + +function getWorkspaceSkillTool(toolId: string): WorkspaceSkillCapableTool { + const tool = getWorkspaceSkillCapableTools().find((candidate) => candidate.value === toolId); + if (!tool) { + throw new Error(`Unknown workspace skill agent '${toolId}'.`); + } + + return tool; +} + +function makeAgentResult( + workspaceRoot: string, + tool: WorkspaceSkillCapableTool, + workflowIds: string[] +): WorkspaceSkillAgentResult { + return { + tool_id: tool.value, + name: tool.name, + skills_path: path.join(workspaceRoot, tool.skillsDir, 'skills'), + workflow_ids: workflowIds, + }; +} + +export async function generateWorkspaceAgentSkills( + workspaceRoot: string, + selectedAgentIds: string[] +): Promise { + const profileContext = resolveWorkspaceSkillProfileContext(); + const report = makeBaseWorkspaceSkillReport(selectedAgentIds, profileContext); + + if (selectedAgentIds.length === 0) { + report.skipped.push({ + reason: 'no_agents_selected', + message: 'No workspace agent skills were selected.', + }); + return report; + } + + const skillTemplates = getSkillTemplates(profileContext.workflowIds); + + if (skillTemplates.length === 0) { + for (const toolId of selectedAgentIds) { + const tool = getWorkspaceSkillTool(toolId); + report.skipped.push({ + tool_id: tool.value, + name: tool.name, + reason: 'no_profile_workflows', + message: 'The active global profile does not select any workflows.', + }); + } + return report; + } + + for (const toolId of selectedAgentIds) { + const tool = getWorkspaceSkillTool(toolId); + const wasConfigured = getToolSkillStatus(workspaceRoot, tool.value).configured; + + try { + const skillsDir = path.join(workspaceRoot, tool.skillsDir, 'skills'); + const transformer = + tool.value === 'opencode' || tool.value === 'pi' ? transformToHyphenCommands : undefined; + + for (const { template, dirName } of skillTemplates) { + const skillFile = path.join(skillsDir, dirName, 'SKILL.md'); + const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer); + await FileSystemUtils.writeFile(skillFile, skillContent); + } + + const result = makeAgentResult(workspaceRoot, tool, profileContext.workflowIds); + if (wasConfigured) { + report.refreshed.push(result); + } else { + report.generated.push(result); + } + } catch (error) { + report.failed.push({ + tool_id: tool.value, + name: tool.name, + error: error instanceof Error ? error.message : String(error), + }); + } + } + + return report; +} diff --git a/test/commands/workspace.interactive.test.ts b/test/commands/workspace.interactive.test.ts index 1173f1ed8..9846346e2 100644 --- a/test/commands/workspace.interactive.test.ts +++ b/test/commands/workspace.interactive.test.ts @@ -10,12 +10,19 @@ import { parseWorkspaceLocalState, } from '../../src/core/workspace/index.js'; +const searchableMultiSelectMock = vi.hoisted(() => vi.fn(async () => [])); + vi.mock('@inquirer/prompts', () => ({ input: vi.fn(), confirm: vi.fn(), select: vi.fn(), })); +vi.mock('../../src/prompts/searchable-multi-select.js', () => ({ + default: searchableMultiSelectMock, + searchableMultiSelect: searchableMultiSelectMock, +})); + async function runWorkspaceCommand(args: string[]): Promise { const { registerWorkspaceCommand } = await import('../../src/commands/workspace.js'); const program = new Command(); @@ -39,6 +46,7 @@ async function getPromptMocks(): Promise<{ describe('workspace command interactive flows', () => { let tempDir: string; let dataHome: string; + let configHome: string; let originalEnv: NodeJS.ProcessEnv; let originalCwd: string; let originalStdinTTY: boolean | undefined; @@ -51,6 +59,7 @@ describe('workspace command interactive flows', () => { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openspec-workspace-interactive-')); dataHome = path.join(tempDir, 'data'); + configHome = path.join(tempDir, 'config'); originalEnv = { ...process.env }; originalCwd = process.cwd(); originalStdinTTY = (process.stdin as NodeJS.ReadStream & { isTTY?: boolean }).isTTY; @@ -59,6 +68,7 @@ describe('workspace command interactive flows', () => { process.env = { ...process.env, XDG_DATA_HOME: dataHome, + XDG_CONFIG_HOME: configHome, OPENSPEC_TELEMETRY: '0', }; delete process.env.CI; @@ -69,6 +79,8 @@ describe('workspace command interactive flows', () => { consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + searchableMultiSelectMock.mockReset(); + searchableMultiSelectMock.mockResolvedValue([]); }); afterEach(() => { @@ -211,6 +223,59 @@ describe('workspace command interactive flows', () => { }); }); + it('asks which agents get OpenSpec skills and preselects the preferred opener', async () => { + const api = mkdir('repos/api'); + const binDir = mkdir('bin'); + const codexPath = path.join(binDir, process.platform === 'win32' ? 'codex.cmd' : 'codex'); + fs.writeFileSync(codexPath, ''); + fs.chmodSync(codexPath, 0o755); + process.env.PATH = binDir; + const { input, select } = await getPromptMocks(); + + input.mockImplementation(async (options: { message: string }) => { + if (options.message === 'Workspace name:') { + return 'platform'; + } + + if (options.message === 'Repo or folder path:') { + return api; + } + + throw new Error(`Unexpected input prompt: ${options.message}`); + }); + select.mockImplementation(async (options: { message: string }) => { + if (options.message === 'Continue') { + return 'finish'; + } + + if (options.message === 'Preferred opener:') { + return 'codex'; + } + + throw new Error(`Unexpected select prompt: ${options.message}`); + }); + searchableMultiSelectMock.mockImplementationOnce(async (options: { + message: string; + choices: Array<{ value: string; preSelected?: boolean }>; + }) => { + expect(options.message).toBe('Which agents should get OpenSpec skills in this workspace?'); + expect(options.choices.find((choice) => choice.value === 'codex')?.preSelected).toBe(true); + expect(options.choices.find((choice) => choice.value === 'claude')?.preSelected).toBe(false); + return ['codex', 'claude']; + }); + + await runWorkspaceCommand(['setup']); + + expect(process.exitCode).toBeUndefined(); + expect(searchableMultiSelectMock).toHaveBeenCalledTimes(1); + expect(readLocalState('platform').workspace_skills).toEqual( + expect.objectContaining({ + selected_agents: ['codex', 'claude'], + last_applied_workflow_ids: ['propose', 'explore', 'apply', 'sync', 'archive'], + }) + ); + }); + it('lets users add another path and rename an inferred link-name conflict', async () => { const firstApi = mkdir('repos/current/api'); const secondApi = mkdir('repos/archive/api'); diff --git a/test/commands/workspace.test.ts b/test/commands/workspace.test.ts index 366c6f376..1aaa93c67 100644 --- a/test/commands/workspace.test.ts +++ b/test/commands/workspace.test.ts @@ -29,13 +29,16 @@ import { runCLI, type RunCLIResult } from '../helpers/run-cli.js'; describe('workspace command', () => { let tempDir: string; let dataHome: string; + let configHome: string; let env: NodeJS.ProcessEnv; beforeEach(() => { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openspec-workspace-command-')); dataHome = path.join(tempDir, 'data'); + configHome = path.join(tempDir, 'config'); env = { XDG_DATA_HOME: dataHome, + XDG_CONFIG_HOME: configHome, OPEN_SPEC_INTERACTIVE: '0', OPENSPEC_TELEMETRY: '0', }; @@ -127,6 +130,12 @@ describe('workspace command', () => { ); } + function writeGlobalConfig(config: Record): void { + const configDir = path.join(configHome, 'openspec'); + fs.mkdirSync(configDir, { recursive: true }); + fs.writeFileSync(path.join(configDir, 'config.json'), `${JSON.stringify(config, null, 2)}\n`); + } + function readSharedState(workspaceRoot: string) { return parseWorkspaceSharedState( fs.readFileSync(getWorkspaceSharedStatePath(workspaceRoot), 'utf-8') @@ -227,6 +236,155 @@ describe('workspace command', () => { ]); }); + it('keeps non-interactive setup compatible by skipping skills when --tools is omitted', async () => { + const api = mkdir('repos/api'); + const setup = await setupWorkspace('skip-skills', [`api=${api}`]); + + expect(setup.workspace_skills).toEqual( + expect.objectContaining({ + selected_agents: [], + generated: [], + refreshed: [], + failed: [], + skipped: [ + expect.objectContaining({ + reason: 'tools_omitted', + message: expect.stringContaining('openspec workspace update --tools '), + }), + ], + }) + ); + expect(readLocalState(setup.workspace.root).workspace_skills).toBeUndefined(); + expect(fs.existsSync(path.join(setup.workspace.root, '.codex'))).toBe(false); + }); + + it('installs profile-selected workspace skills in the workspace root only', async () => { + const api = mkdir('repos/api'); + const linkedEntriesBefore = fs.readdirSync(api).sort(); + const codexHome = path.join(tempDir, 'codex-home'); + writeGlobalConfig({ + profile: 'custom', + delivery: 'commands', + workflows: ['apply', 'archive'], + }); + + const result = await runCLI( + [ + 'workspace', + 'setup', + '--no-interactive', + '--json', + '--name', + 'skill-root', + '--link', + `api=${api}`, + '--opener', + 'codex', + '--tools', + 'codex', + ], + { + cwd: tempDir, + env: { + ...env, + CODEX_HOME: codexHome, + }, + } + ); + + expect(result.exitCode).toBe(0); + const payload = parseJson(result); + const workspaceRoot = payload.workspace.root; + expect(payload.workspace_skills).toEqual( + expect.objectContaining({ + profile: 'custom', + delivery: 'commands', + workflow_ids: ['apply', 'archive'], + selected_agents: ['codex'], + skills_only: true, + delivery_notice: expect.stringContaining('skills only'), + generated: [ + expect.objectContaining({ + tool_id: 'codex', + workflow_ids: ['apply', 'archive'], + }), + ], + refreshed: [], + failed: [], + }) + ); + + expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-apply-change', 'SKILL.md'))).toBe(true); + expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-archive-change', 'SKILL.md'))).toBe(true); + expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-propose', 'SKILL.md'))).toBe(false); + expect(fs.existsSync(path.join(codexHome, 'prompts'))).toBe(false); + expect(fs.readdirSync(api).sort()).toEqual(linkedEntriesBefore); + expect(fs.existsSync(path.join(api, '.codex'))).toBe(false); + + expect(readLocalState(workspaceRoot).workspace_skills).toEqual( + expect.objectContaining({ + selected_agents: ['codex'], + last_applied_profile: 'custom', + last_applied_delivery: 'commands', + last_applied_workflow_ids: ['apply', 'archive'], + last_applied_at: expect.any(String), + }) + ); + }); + + it('supports --tools none and records an empty workspace skill selection', async () => { + const api = mkdir('repos/api'); + const setup = await setupWorkspace('skills-none', [`api=${api}`], ['--tools', 'none']); + + expect(setup.workspace_skills).toEqual( + expect.objectContaining({ + selected_agents: [], + generated: [], + refreshed: [], + failed: [], + skipped: [ + expect.objectContaining({ + reason: 'no_agents_selected', + }), + ], + }) + ); + expect(readLocalState(setup.workspace.root).workspace_skills).toEqual( + expect.objectContaining({ + selected_agents: [], + last_applied_workflow_ids: ['propose', 'explore', 'apply', 'sync', 'archive'], + }) + ); + }); + + it('rejects invalid workspace setup tool IDs with structured JSON status', async () => { + const api = mkdir('repos/api'); + const invalid = await runCLI( + [ + 'workspace', + 'setup', + '--no-interactive', + '--json', + '--name', + 'invalid-skills', + '--link', + `api=${api}`, + '--tools', + 'codex,not-real', + ], + { cwd: tempDir, env } + ); + + expect(invalid.exitCode).toBe(1); + expect(parseJson(invalid).status[0]).toEqual( + expect.objectContaining({ + code: 'invalid_workspace_setup_tools', + target: 'workspace.skills', + message: expect.stringContaining('not-real'), + }) + ); + }); + it('preserves equals signs in inferred and explicit setup link paths', async () => { const inferred = mkdir('repos/foo=bar'); const explicit = mkdir('repos/api=service'); diff --git a/test/core/workspace/skills.test.ts b/test/core/workspace/skills.test.ts new file mode 100644 index 000000000..5fae5b80b --- /dev/null +++ b/test/core/workspace/skills.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest'; + +import { + getWorkspaceSkillToolIds, + parseWorkspaceSkillToolsValue, +} from '../../../src/core/workspace/skills.js'; + +describe('workspace skill helpers', () => { + it('parses workspace --tools values using the skill-capable tool set', () => { + expect(parseWorkspaceSkillToolsValue('all')).toEqual(getWorkspaceSkillToolIds()); + expect(parseWorkspaceSkillToolsValue('none')).toEqual([]); + expect(parseWorkspaceSkillToolsValue('Codex, claude,codex')).toEqual(['codex', 'claude']); + }); + + it('rejects invalid or mixed workspace --tools values', () => { + expect(() => parseWorkspaceSkillToolsValue('')).toThrow(/requires a value/); + expect(() => parseWorkspaceSkillToolsValue('all,codex')).toThrow(/Cannot combine/); + expect(() => parseWorkspaceSkillToolsValue('codex,missing')).toThrow(/missing/); + }); +}); From 939e1c0059b823abb9fc5554c9d47f4fdec48f59 Mon Sep 17 00:00:00 2001 From: TabishB Date: Thu, 14 May 2026 02:06:07 +1000 Subject: [PATCH 03/14] Implement workspace skill updates --- .../workspace-change-planning/tasks.md | 24 +- src/commands/workspace.ts | 125 ++++++++++ src/commands/workspace/operations.ts | 33 ++- src/commands/workspace/types.ts | 4 + src/core/completions/command-registry.ts | 25 ++ src/core/workspace/skills.ts | 219 ++++++++++++++++++ test/commands/workspace.test.ts | 216 +++++++++++++++++ 7 files changed, 632 insertions(+), 14 deletions(-) diff --git a/openspec/changes/workspace-change-planning/tasks.md b/openspec/changes/workspace-change-planning/tasks.md index eaaa0da1d..ad477749f 100644 --- a/openspec/changes/workspace-change-planning/tasks.md +++ b/openspec/changes/workspace-change-planning/tasks.md @@ -20,18 +20,18 @@ User-testable outcome: A user can run workspace setup, choose which agents get t User-testable outcome: A user can change the global profile, run workspace update in an existing workspace, and see workspace-local skills refresh to the selected workflows with clear human and JSON output. -- [ ] 2.1 Add a workspace update flow that refreshes, adds, or removes OpenSpec agent skills in an existing workspace. -- [ ] 2.2 Let `openspec workspace update` resolve the current workspace when run from inside a workspace. -- [ ] 2.3 Support named and selected-workspace update forms such as `openspec workspace update platform` and `openspec workspace update --workspace platform`. -- [ ] 2.4 Support non-interactive update forms such as `openspec workspace update platform --tools codex,claude`. -- [ ] 2.5 Remove only known OpenSpec-managed workflow skill directories for agents that are no longer selected. -- [ ] 2.6 Sync workspace-local workflow skill directories to the current global profile selection. -- [ ] 2.7 Keep workspace update skills-only for this slice; do not generate slash commands or global command files even when global delivery includes commands. -- [ ] 2.8 Define how update reports refreshed, added, removed, skipped, failed, and skills-only delivery work in human and JSON output. -- [ ] 2.9 Use stored selected agents when workspace update runs without `--tools`, and update that stored selection when `--tools` is passed. -- [ ] 2.10 Detect workspace-local skill drift from the active global profile and report `openspec workspace update` guidance. -- [ ] 2.11 Manually run workspace update for refresh, add, remove, no-op, omitted-`--tools`, and profile-change cases and verify linked repos remain unchanged. -- [ ] 2.12 Review the update UX: command forms, current-workspace detection, profile/delivery messaging, drift messaging, removal messaging, and JSON output are understandable. +- [x] 2.1 Add a workspace update flow that refreshes, adds, or removes OpenSpec agent skills in an existing workspace. +- [x] 2.2 Let `openspec workspace update` resolve the current workspace when run from inside a workspace. +- [x] 2.3 Support named and selected-workspace update forms such as `openspec workspace update platform` and `openspec workspace update --workspace platform`. +- [x] 2.4 Support non-interactive update forms such as `openspec workspace update platform --tools codex,claude`. +- [x] 2.5 Remove only known OpenSpec-managed workflow skill directories for agents that are no longer selected. +- [x] 2.6 Sync workspace-local workflow skill directories to the current global profile selection. +- [x] 2.7 Keep workspace update skills-only for this slice; do not generate slash commands or global command files even when global delivery includes commands. +- [x] 2.8 Define how update reports refreshed, added, removed, skipped, failed, and skills-only delivery work in human and JSON output. +- [x] 2.9 Use stored selected agents when workspace update runs without `--tools`, and update that stored selection when `--tools` is passed. +- [x] 2.10 Detect workspace-local skill drift from the active global profile and report `openspec workspace update` guidance. +- [x] 2.11 Manually run workspace update for refresh, add, remove, no-op, omitted-`--tools`, and profile-change cases and verify linked repos remain unchanged. +- [x] 2.12 Review the update UX: command forms, current-workspace detection, profile/delivery messaging, drift messaging, removal messaging, and JSON output are understandable. ## Phase 3: Config Profile Workspace Apply diff --git a/src/commands/workspace.ts b/src/commands/workspace.ts index 17809de3e..246649e32 100644 --- a/src/commands/workspace.ts +++ b/src/commands/workspace.ts @@ -16,6 +16,7 @@ import { listWorkspaceOpenerChoices, parseWorkspacePreferredOpenerValue, parseWorkspaceSkillToolsValue, + updateWorkspaceAgentSkills, listWorkspaceRegistryEntries, readOptionalWorkspaceLocalState, writeWorkspaceLocalState, @@ -28,7 +29,9 @@ import { loadWorkspaceForDoctor, loadWorkspaceForList, parseSetupLinks, + readWorkspaceForMutation, readRegistry, + recordSelectedWorkspaceAfterMutation, resolveExistingDirectory, updateWorkspaceLink, validateLinkNameForCommand, @@ -51,6 +54,7 @@ import { WorkspaceOutput, WorkspaceSetupOptions, WorkspaceStatus, + WorkspaceUpdateOptions, appendStatus, asErrorMessage, asStatus, @@ -276,6 +280,17 @@ function parseSetupToolsOption(tools: string): string[] { } } +function parseUpdateToolsOption(tools: string): string[] { + try { + return parseWorkspaceSkillToolsValue(tools); + } catch (error) { + throw new WorkspaceCliError(asErrorMessage(error), 'invalid_workspace_update_tools', { + target: 'workspace.skills', + fix: `Use --tools all, --tools none, or one of: ${getWorkspaceSkillToolIds().join(', ')}`, + }); + } +} + function getPreferredWorkspaceSkillAgentId( preferredOpener: WorkspacePreferredOpener | undefined ): string | null { @@ -475,6 +490,12 @@ function formatWorkspaceSkillAgentResult(result: { name: string; workflow_ids?: return `${result.name} (${workflowLabel})`; } +function formatWorkspaceSkillRemovedResult(result: { name: string; workflow_ids?: string[] }): string { + const workflowCount = result.workflow_ids?.length ?? 0; + const workflowLabel = workflowCount === 1 ? '1 workflow' : `${workflowCount} workflows`; + return `${result.name} (${workflowLabel} removed)`; +} + function printWorkspaceSkillReportHuman(report: WorkspaceSkillInstallationReport): void { console.log('Agent skills:'); console.log(` Profile: ${report.profile}`); @@ -486,10 +507,18 @@ function printWorkspaceSkillReportHuman(report: WorkspaceSkillInstallationReport console.log(` Generated: ${report.generated.map(formatWorkspaceSkillAgentResult).join(', ')}`); } + if (report.added.length > 0) { + console.log(` Added: ${report.added.map(formatWorkspaceSkillAgentResult).join(', ')}`); + } + if (report.refreshed.length > 0) { console.log(` Refreshed: ${report.refreshed.map(formatWorkspaceSkillAgentResult).join(', ')}`); } + if (report.removed.length > 0) { + console.log(` Removed: ${report.removed.map(formatWorkspaceSkillRemovedResult).join(', ')}`); + } + if (report.skipped.length > 0) { for (const skipped of report.skipped) { const prefix = skipped.name ? `${skipped.name}: ` : ''; @@ -638,6 +667,24 @@ function resolveOpenWorkspaceName( return positionalName ?? options.workspace; } +function resolveUpdateWorkspaceName( + positionalName: string | undefined, + options: WorkspaceUpdateOptions +): string | undefined { + if (positionalName && options.workspace && positionalName !== options.workspace) { + throw new WorkspaceCliError( + `Conflicting workspace selectors: positional '${positionalName}' and --workspace '${options.workspace}'.`, + 'workspace_selection_conflict', + { + target: 'workspace.name', + fix: 'Use either the positional workspace name or --workspace with the same value.', + } + ); + } + + return positionalName ?? options.workspace; +} + function printWorkspaceOpenHuman( selectedName: string, selectedRoot: string, @@ -878,6 +925,70 @@ class WorkspaceCommand { } } + async update( + positionalName: string | undefined, + options: WorkspaceUpdateOptions = {} + ): Promise { + try { + const workspaceName = resolveUpdateWorkspaceName(positionalName, options); + const selected = await selectWorkspaceForCommand( + { + ...options, + workspace: workspaceName, + }, + 'update', + { preferPositionalName: Boolean(positionalName) } + ); + const { localState } = await readWorkspaceForMutation(selected); + const hasExplicitToolSelection = options.tools !== undefined; + const selectedAgentIds = hasExplicitToolSelection + ? parseUpdateToolsOption(options.tools ?? '') + : localState.workspace_skills?.selected_agents ?? []; + const previousSkillState = + hasExplicitToolSelection + ? localState.workspace_skills ?? { selected_agents: [] } + : localState.workspace_skills; + const skillReport = await updateWorkspaceAgentSkills( + selected.root, + selectedAgentIds, + previousSkillState + ); + const shouldStoreSelection = hasExplicitToolSelection || Boolean(localState.workspace_skills); + + if (shouldStoreSelection) { + await writeWorkspaceSkillState(selected.root, selectedAgentIds, skillReport); + await recordSelectedWorkspaceAfterMutation(selected); + } + + const doctorResult = await loadWorkspaceForDoctor(selected); + + if (options.json) { + printJson({ + workspace: doctorResult.workspace, + workspace_skills: skillReport, + status: doctorResult.status, + }); + return; + } + + console.log(chalk.green('Workspace update complete')); + console.log(`Workspace: ${doctorResult.workspace.name}`); + console.log(`Location: ${doctorResult.workspace.root}`); + console.log(''); + printStatusLines(doctorResult.status); + if (doctorResult.status.length > 0) { + console.log(''); + } + printWorkspaceSkillReportHuman(skillReport); + console.log(''); + console.log('Next useful commands:'); + console.log(` openspec workspace doctor --workspace ${doctorResult.workspace.name}`); + console.log(` openspec workspace update --workspace ${doctorResult.workspace.name} --tools `); + } catch (error) { + this.handleFailure(options.json, { workspace: null, workspace_skills: null, status: [] }, error); + } + } + async open( positionalName: string | undefined, options: WorkspaceOpenOptions = {} @@ -1024,6 +1135,20 @@ export function registerWorkspaceCommand(program: Command): void { await workspaceCommand.doctor(options); }); + workspace + .command('update [name]') + .description('Refresh workspace-local OpenSpec agent skills from the active global profile') + .option('--workspace ', 'Workspace name from the local workspace registry') + .option( + '--tools ', + `Select agents for workspace skills. Use "all", "none", or a comma-separated list of: ${getWorkspaceSkillToolIds().join(', ')}. Global profile selects workflows; --tools selects agents.` + ) + .option('--json', 'Output as JSON') + .option('--no-interactive', 'Disable prompts') + .action(async (name: string | undefined, options: WorkspaceUpdateOptions) => { + await workspaceCommand.update(name, options); + }); + workspace .command('open [name]') .description('Open a workspace in an agent or VS Code editor') diff --git a/src/commands/workspace/operations.ts b/src/commands/workspace/operations.ts index 7d3ce0d1e..96c493105 100644 --- a/src/commands/workspace/operations.ts +++ b/src/commands/workspace/operations.ts @@ -8,6 +8,7 @@ import { WorkspaceRegistryState, WorkspaceSharedState, getManagedWorkspaceRoot, + hasWorkspaceSkillProfileDrift, getWorkspaceChangesDir, isWorkspaceRoot, parseWorkspaceSetupLinkInput, @@ -211,6 +212,28 @@ function localStateInvalidStatus(error: unknown): WorkspaceStatus { ); } +function workspaceSkillDriftStatus(workspaceName: string): WorkspaceStatus { + return makeStatus( + 'warning', + 'workspace_skills_out_of_sync', + 'Workspace-local agent skills are out of sync with the active global profile.', + { + target: 'workspace.skills', + fix: `openspec workspace update --workspace ${workspaceName}`, + } + ); +} + +function appendWorkspaceSkillDriftStatus( + statuses: WorkspaceStatus[], + workspaceName: string, + localState: WorkspaceLocalState | null +): void { + if (hasWorkspaceSkillProfileDrift(localState)) { + statuses.push(workspaceSkillDriftStatus(workspaceName)); + } +} + async function readLocalStateForMutation(workspaceRoot: string): Promise { try { return (await readOptionalWorkspaceLocalState(workspaceRoot)) ?? emptyLocalState(); @@ -375,6 +398,8 @@ export async function loadWorkspaceForList( workspaceStatus.push(localStateInvalidStatus(error)); } + appendWorkspaceSkillDriftStatus(workspaceStatus, sharedState.name, localState); + return { name: sharedState.name, root: entry.workspaceRoot, @@ -465,6 +490,10 @@ export async function loadWorkspaceForDoctor( workspaceStatus.push(localStateInvalidStatus(error)); } + if (!localStateInvalid) { + appendWorkspaceSkillDriftStatus(workspaceStatus, sharedState.name, localState); + } + if (!(await directoryExists(planningPath))) { workspaceStatus.push( makeStatus( @@ -553,7 +582,7 @@ export async function loadWorkspaceForDoctor( }; } -async function readWorkspaceForMutation( +export async function readWorkspaceForMutation( selected: SelectedWorkspace ): Promise<{ sharedState: WorkspaceSharedState; localState: WorkspaceLocalState }> { if (!(await directoryExists(selected.root)) || !(await isWorkspaceRoot(selected.root))) { @@ -573,7 +602,7 @@ async function readWorkspaceForMutation( }; } -async function recordSelectedWorkspaceAfterMutation(selected: SelectedWorkspace): Promise { +export async function recordSelectedWorkspaceAfterMutation(selected: SelectedWorkspace): Promise { if (selected.unregisteredCurrentWorkspace) { await recordWorkspaceInRegistry(selected.name, selected.root); } diff --git a/src/commands/workspace/types.ts b/src/commands/workspace/types.ts index e5c418dfd..5943c3a57 100644 --- a/src/commands/workspace/types.ts +++ b/src/commands/workspace/types.ts @@ -49,6 +49,10 @@ export interface WorkspaceSelectionOptions { export type WorkspaceLinkOptions = WorkspaceSelectionOptions; +export interface WorkspaceUpdateOptions extends WorkspaceSelectionOptions { + tools?: string; +} + export interface WorkspaceOpenOptions extends WorkspaceSelectionOptions { agent?: string; editor?: boolean; diff --git a/src/core/completions/command-registry.ts b/src/core/completions/command-registry.ts index 2f50a9d60..545d5d061 100644 --- a/src/core/completions/command-registry.ts +++ b/src/core/completions/command-registry.ts @@ -264,6 +264,31 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [ COMMON_FLAGS.noInteractive, ], }, + { + name: 'update', + description: 'Refresh workspace-local OpenSpec agent skills from the active global profile', + acceptsPositional: true, + positionals: [ + { + name: 'name', + optional: true, + }, + ], + flags: [ + { + name: 'workspace', + description: 'Workspace name from the local workspace registry', + takesValue: true, + }, + { + name: 'tools', + description: 'Select agents for workspace skills; global profile selects workflows', + takesValue: true, + }, + COMMON_FLAGS.json, + COMMON_FLAGS.noInteractive, + ], + }, { name: 'open', description: 'Open a workspace in an agent or VS Code editor', diff --git a/src/core/workspace/skills.ts b/src/core/workspace/skills.ts index ae468f18c..469d3ac5f 100644 --- a/src/core/workspace/skills.ts +++ b/src/core/workspace/skills.ts @@ -1,4 +1,5 @@ import * as path from 'node:path'; +import * as nodeFs from 'node:fs'; import { createRequire } from 'node:module'; import { FileSystemUtils } from '../../utils/file-system.js'; @@ -12,9 +13,11 @@ import { getToolSkillStatus, getToolsWithSkillsDir, } from '../shared/index.js'; +import type { WorkspaceLocalState, WorkspaceSkillState } from './foundation.js'; const require = createRequire(import.meta.url); const { version: OPENSPEC_VERSION } = require('../../../package.json'); +const fs = nodeFs.promises; export interface WorkspaceSkillAgentResult { tool_id: string; @@ -23,6 +26,10 @@ export interface WorkspaceSkillAgentResult { workflow_ids: string[]; } +export interface WorkspaceSkillRemovedResult extends WorkspaceSkillAgentResult { + reason: 'agent_unselected' | 'workflow_unselected'; +} + export interface WorkspaceSkillSkippedResult { tool_id?: string; name?: string; @@ -44,7 +51,9 @@ export interface WorkspaceSkillInstallationReport { skills_only: true; delivery_notice: string | null; generated: WorkspaceSkillAgentResult[]; + added: WorkspaceSkillAgentResult[]; refreshed: WorkspaceSkillAgentResult[]; + removed: WorkspaceSkillRemovedResult[]; skipped: WorkspaceSkillSkippedResult[]; failed: WorkspaceSkillFailedResult[]; } @@ -76,6 +85,45 @@ function resolveWorkspaceSkillProfileContext(): WorkspaceSkillProfileContext { }; } +export function getCurrentWorkspaceSkillProfileSelection(): { + profile: Profile; + delivery: Delivery; + workflow_ids: string[]; +} { + const profileContext = resolveWorkspaceSkillProfileContext(); + return { + profile: profileContext.profile, + delivery: profileContext.delivery, + workflow_ids: profileContext.workflowIds, + }; +} + +function arraysEqual(left: readonly string[] | undefined, right: readonly string[]): boolean { + if (!left || left.length !== right.length) { + return false; + } + + return left.every((value, index) => value === right[index]); +} + +export function hasWorkspaceSkillProfileDrift( + localState: Pick | null | undefined +): boolean { + const workspaceSkills = localState?.workspace_skills; + + if (!workspaceSkills) { + return false; + } + + const current = getCurrentWorkspaceSkillProfileSelection(); + + return ( + workspaceSkills.last_applied_profile !== current.profile || + workspaceSkills.last_applied_delivery !== current.delivery || + !arraysEqual(workspaceSkills.last_applied_workflow_ids, current.workflow_ids) + ); +} + function makeBaseWorkspaceSkillReport( selectedAgentIds: string[], profileContext = resolveWorkspaceSkillProfileContext() @@ -88,7 +136,9 @@ function makeBaseWorkspaceSkillReport( skills_only: true, delivery_notice: profileContext.deliveryNotice, generated: [], + added: [], refreshed: [], + removed: [], skipped: [], failed: [], }; @@ -192,6 +242,53 @@ function makeAgentResult( }; } +function getManagedWorkspaceSkillEntries(): Array<{ workflowId: string; dirName: string }> { + return getSkillTemplates().map(({ workflowId, dirName }) => ({ workflowId, dirName })); +} + +async function pathExists(targetPath: string): Promise { + try { + await fs.access(targetPath); + return true; + } catch { + return false; + } +} + +async function removeManagedWorkflowSkillDirs( + workspaceRoot: string, + tool: WorkspaceSkillCapableTool, + desiredWorkflowIds: readonly string[], + reason: WorkspaceSkillRemovedResult['reason'] +): Promise { + const desiredSet = new Set(desiredWorkflowIds); + const skillsDir = path.join(workspaceRoot, tool.skillsDir, 'skills'); + const removedWorkflowIds: string[] = []; + + for (const { workflowId, dirName } of getManagedWorkspaceSkillEntries()) { + if (desiredSet.has(workflowId)) { + continue; + } + + const skillDir = path.join(skillsDir, dirName); + if (!(await pathExists(skillDir))) { + continue; + } + + await fs.rm(skillDir, { recursive: true, force: true }); + removedWorkflowIds.push(workflowId); + } + + if (removedWorkflowIds.length === 0) { + return null; + } + + return { + ...makeAgentResult(workspaceRoot, tool, removedWorkflowIds), + reason, + }; +} + export async function generateWorkspaceAgentSkills( workspaceRoot: string, selectedAgentIds: string[] @@ -254,3 +351,125 @@ export async function generateWorkspaceAgentSkills( return report; } + +export async function updateWorkspaceAgentSkills( + workspaceRoot: string, + selectedAgentIds: string[], + previousSkillState?: WorkspaceSkillState +): Promise { + const profileContext = resolveWorkspaceSkillProfileContext(); + const report = makeBaseWorkspaceSkillReport(selectedAgentIds, profileContext); + const previousSelectedAgentIds = previousSkillState?.selected_agents ?? []; + const previousSelectedSet = new Set(previousSelectedAgentIds); + const selectedSet = new Set(selectedAgentIds); + const skillTemplates = getSkillTemplates(profileContext.workflowIds); + + for (const toolId of previousSelectedAgentIds) { + if (selectedSet.has(toolId)) { + continue; + } + + const tool = getWorkspaceSkillTool(toolId); + + try { + const removed = await removeManagedWorkflowSkillDirs( + workspaceRoot, + tool, + [], + 'agent_unselected' + ); + if (removed) { + report.removed.push(removed); + } + } catch (error) { + report.failed.push({ + tool_id: tool.value, + name: tool.name, + error: error instanceof Error ? error.message : String(error), + }); + } + } + + if (selectedAgentIds.length === 0) { + if (report.removed.length === 0) { + report.skipped.push({ + reason: previousSkillState ? 'no_agents_selected' : 'no_stored_agent_selection', + message: previousSkillState + ? 'No workspace agent skills were selected.' + : 'No workspace agent skill selection is stored. Pass --tools to install skills.', + }); + } + return report; + } + + if (skillTemplates.length === 0) { + for (const toolId of selectedAgentIds) { + const tool = getWorkspaceSkillTool(toolId); + try { + const removed = await removeManagedWorkflowSkillDirs( + workspaceRoot, + tool, + [], + 'workflow_unselected' + ); + if (removed) { + report.removed.push(removed); + } + } catch (error) { + report.failed.push({ + tool_id: tool.value, + name: tool.name, + error: error instanceof Error ? error.message : String(error), + }); + } + report.skipped.push({ + tool_id: tool.value, + name: tool.name, + reason: 'no_profile_workflows', + message: 'The active global profile does not select any workflows.', + }); + } + return report; + } + + for (const toolId of selectedAgentIds) { + const tool = getWorkspaceSkillTool(toolId); + + try { + const skillsDir = path.join(workspaceRoot, tool.skillsDir, 'skills'); + const transformer = + tool.value === 'opencode' || tool.value === 'pi' ? transformToHyphenCommands : undefined; + + for (const { template, dirName } of skillTemplates) { + const skillFile = path.join(skillsDir, dirName, 'SKILL.md'); + const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer); + await FileSystemUtils.writeFile(skillFile, skillContent); + } + + const removed = await removeManagedWorkflowSkillDirs( + workspaceRoot, + tool, + profileContext.workflowIds, + 'workflow_unselected' + ); + if (removed) { + report.removed.push(removed); + } + + const result = makeAgentResult(workspaceRoot, tool, profileContext.workflowIds); + if (previousSelectedSet.has(toolId)) { + report.refreshed.push(result); + } else { + report.added.push(result); + } + } catch (error) { + report.failed.push({ + tool_id: tool.value, + name: tool.name, + error: error instanceof Error ? error.message : String(error), + }); + } + } + + return report; +} diff --git a/test/commands/workspace.test.ts b/test/commands/workspace.test.ts index 1aaa93c67..b5abb35f3 100644 --- a/test/commands/workspace.test.ts +++ b/test/commands/workspace.test.ts @@ -357,6 +357,180 @@ describe('workspace command', () => { ); }); + it('updates stored workspace skills from the current workspace and clears profile drift', async () => { + const api = mkdir('repos/api'); + const linkedEntriesBefore = fs.readdirSync(api).sort(); + writeGlobalConfig({ + profile: 'custom', + delivery: 'commands', + workflows: ['apply', 'verify'], + }); + const setup = await setupWorkspace('profile-sync', [`api=${api}`], ['--tools', 'codex']); + const workspaceRoot = setup.workspace.root; + const customSkillDir = path.join(workspaceRoot, '.codex', 'skills', 'custom-note'); + fs.mkdirSync(customSkillDir, { recursive: true }); + fs.writeFileSync(path.join(customSkillDir, 'README.md'), 'user-owned\n'); + + expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-apply-change', 'SKILL.md'))).toBe(true); + expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-verify-change', 'SKILL.md'))).toBe(true); + + writeGlobalConfig({ + profile: 'core', + delivery: 'commands', + }); + + const drift = await runCLI( + ['workspace', 'doctor', '--workspace', 'profile-sync', '--json'], + { cwd: tempDir, env } + ); + expect(drift.exitCode).toBe(0); + expect(parseJson(drift).workspace.status).toContainEqual( + expect.objectContaining({ + code: 'workspace_skills_out_of_sync', + fix: 'openspec workspace update --workspace profile-sync', + }) + ); + + const update = await runCLI(['workspace', 'update', '--json'], { + cwd: path.join(workspaceRoot, WORKSPACE_CHANGES_DIR_NAME), + env, + }); + expect(update.exitCode).toBe(0); + const payload = parseJson(update); + + expect(payload.workspace.name).toBe('profile-sync'); + expect(payload.workspace_skills).toEqual( + expect.objectContaining({ + profile: 'core', + delivery: 'commands', + workflow_ids: ['propose', 'explore', 'apply', 'sync', 'archive'], + selected_agents: ['codex'], + skills_only: true, + delivery_notice: expect.stringContaining('skills only'), + refreshed: [ + expect.objectContaining({ + tool_id: 'codex', + workflow_ids: ['propose', 'explore', 'apply', 'sync', 'archive'], + }), + ], + removed: [ + expect.objectContaining({ + tool_id: 'codex', + reason: 'workflow_unselected', + workflow_ids: ['verify'], + }), + ], + failed: [], + }) + ); + expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-propose', 'SKILL.md'))).toBe(true); + expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-explore', 'SKILL.md'))).toBe(true); + expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-sync-specs', 'SKILL.md'))).toBe(true); + expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-archive-change', 'SKILL.md'))).toBe(true); + expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-verify-change'))).toBe(false); + expect(fs.existsSync(path.join(customSkillDir, 'README.md'))).toBe(true); + expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'prompts'))).toBe(false); + expect(fs.readdirSync(api).sort()).toEqual(linkedEntriesBefore); + expect(fs.existsSync(path.join(api, '.codex'))).toBe(false); + expect(readLocalState(workspaceRoot).workspace_skills).toEqual( + expect.objectContaining({ + selected_agents: ['codex'], + last_applied_profile: 'core', + last_applied_delivery: 'commands', + last_applied_workflow_ids: ['propose', 'explore', 'apply', 'sync', 'archive'], + }) + ); + + const clean = await runCLI( + ['workspace', 'doctor', '--workspace', 'profile-sync', '--json'], + { cwd: tempDir, env } + ); + expect(clean.exitCode).toBe(0); + expect(parseJson(clean).workspace.status).not.toContainEqual( + expect.objectContaining({ + code: 'workspace_skills_out_of_sync', + }) + ); + }); + + it('supports named and flag-selected workspace updates with explicit agent changes', async () => { + const api = mkdir('repos/api'); + writeGlobalConfig({ + profile: 'custom', + delivery: 'skills', + workflows: ['apply'], + }); + const setup = await setupWorkspace('agent-change', [`api=${api}`], ['--tools', 'codex']); + const workspaceRoot = setup.workspace.root; + const userSkillDir = path.join(workspaceRoot, '.codex', 'skills', 'user-skill'); + fs.mkdirSync(userSkillDir, { recursive: true }); + fs.writeFileSync(path.join(userSkillDir, 'SKILL.md'), 'user-owned\n'); + + const addAgent = await runCLI( + ['workspace', 'update', 'agent-change', '--tools', 'codex,claude', '--json'], + { cwd: tempDir, env } + ); + expect(addAgent.exitCode).toBe(0); + const addPayload = parseJson(addAgent); + expect(addPayload.workspace_skills.refreshed).toEqual([ + expect.objectContaining({ tool_id: 'codex', workflow_ids: ['apply'] }), + ]); + expect(addPayload.workspace_skills.added).toEqual([ + expect.objectContaining({ tool_id: 'claude', workflow_ids: ['apply'] }), + ]); + expect(fs.existsSync(path.join(workspaceRoot, '.claude', 'skills', 'openspec-apply-change', 'SKILL.md'))).toBe(true); + expect(readLocalState(workspaceRoot).workspace_skills?.selected_agents).toEqual(['codex', 'claude']); + + const removeAgent = await runCLI( + ['workspace', 'update', '--workspace', 'agent-change', '--tools', 'claude', '--json'], + { cwd: tempDir, env } + ); + expect(removeAgent.exitCode).toBe(0); + const removePayload = parseJson(removeAgent); + expect(removePayload.workspace_skills.removed).toEqual([ + expect.objectContaining({ + tool_id: 'codex', + reason: 'agent_unselected', + workflow_ids: ['apply'], + }), + ]); + expect(removePayload.workspace_skills.refreshed).toEqual([ + expect.objectContaining({ tool_id: 'claude', workflow_ids: ['apply'] }), + ]); + expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-apply-change'))).toBe(false); + expect(fs.existsSync(path.join(userSkillDir, 'SKILL.md'))).toBe(true); + expect(readLocalState(workspaceRoot).workspace_skills?.selected_agents).toEqual(['claude']); + }); + + it('reports a no-op workspace update when no stored skill selection exists', async () => { + const api = mkdir('repos/api'); + const setup = await setupWorkspace('no-stored-skills', [`api=${api}`]); + + const update = await runCLI( + ['workspace', 'update', '--workspace', 'no-stored-skills', '--json'], + { cwd: tempDir, env } + ); + expect(update.exitCode).toBe(0); + expect(parseJson(update).workspace_skills).toEqual( + expect.objectContaining({ + selected_agents: [], + generated: [], + added: [], + refreshed: [], + removed: [], + failed: [], + skipped: [ + expect.objectContaining({ + reason: 'no_stored_agent_selection', + message: expect.stringContaining('--tools '), + }), + ], + }) + ); + expect(readLocalState(setup.workspace.root).workspace_skills).toBeUndefined(); + expect(fs.existsSync(path.join(setup.workspace.root, '.codex'))).toBe(false); + }); + it('rejects invalid workspace setup tool IDs with structured JSON status', async () => { const api = mkdir('repos/api'); const invalid = await runCLI( @@ -383,6 +557,29 @@ describe('workspace command', () => { message: expect.stringContaining('not-real'), }) ); + + const setup = await setupWorkspace('update-invalid-skills', [`api=${api}`]); + const invalidUpdate = await runCLI( + [ + 'workspace', + 'update', + '--workspace', + 'update-invalid-skills', + '--json', + '--tools', + 'codex,not-real', + ], + { cwd: tempDir, env } + ); + expect(invalidUpdate.exitCode).toBe(1); + expect(parseJson(invalidUpdate).status[0]).toEqual( + expect.objectContaining({ + code: 'invalid_workspace_update_tools', + target: 'workspace.skills', + message: expect.stringContaining('not-real'), + }) + ); + expect(readLocalState(setup.workspace.root).workspace_skills).toBeUndefined(); }); it('preserves equals signs in inferred and explicit setup link paths', async () => { @@ -1324,9 +1521,17 @@ preferred_opener: const help = await runCLI(['workspace', '--help'], { cwd: tempDir, env }); expect(help.exitCode).toBe(0); expect(help.stdout).toContain('setup'); + expect(help.stdout).toContain('update'); expect(help.stdout).toContain('link'); expect(help.stdout).toContain('relink'); expect(help.stdout).not.toMatch(/\bcreate\b/u); + + const updateHelp = await runCLI(['workspace', 'update', '--help'], { cwd: tempDir, env }); + expect(updateHelp.exitCode).toBe(0); + expect(updateHelp.stdout).toContain('active global profile'); + expect(updateHelp.stdout).toContain('--workspace'); + expect(updateHelp.stdout).toContain('--tools'); + expect(updateHelp.stdout).toMatch(/Global profile\s+selects workflows/u); }); it('registers workspace subcommands for shell completions', () => { @@ -1334,6 +1539,7 @@ preferred_opener: const setup = workspace?.subcommands?.find((command) => command.name === 'setup'); const link = workspace?.subcommands?.find((command) => command.name === 'link'); const relink = workspace?.subcommands?.find((command) => command.name === 'relink'); + const update = workspace?.subcommands?.find((command) => command.name === 'update'); const open = workspace?.subcommands?.find((command) => command.name === 'open'); expect(workspace?.subcommands?.map((command) => command.name)).toEqual([ @@ -1343,6 +1549,7 @@ preferred_opener: 'link', 'relink', 'doctor', + 'update', 'open', ]); expect(setup?.flags?.some((flag) => flag.name === 'opener')).toBe(true); @@ -1360,6 +1567,15 @@ preferred_opener: { name: 'name' }, { name: 'path', type: 'path' }, ]); + expect(update?.positionals).toEqual([ + { name: 'name', optional: true }, + ]); + expect(update?.flags?.map((flag) => flag.name)).toEqual([ + 'workspace', + 'tools', + 'json', + 'no-interactive', + ]); expect(open?.positionals).toEqual([ { name: 'name', optional: true }, ]); From 7a62cc7eefce46acb5c5c25281b99e1fc64ca721 Mon Sep 17 00:00:00 2001 From: TabishB Date: Thu, 14 May 2026 02:18:36 +1000 Subject: [PATCH 04/14] Handle config profile workspace apply --- .../workspace-change-planning/tasks.md | 18 +-- src/commands/config.ts | 93 +++++++++++- test/commands/config-profile.test.ts | 132 ++++++++++++++++++ 3 files changed, 229 insertions(+), 14 deletions(-) diff --git a/openspec/changes/workspace-change-planning/tasks.md b/openspec/changes/workspace-change-planning/tasks.md index ad477749f..86a424479 100644 --- a/openspec/changes/workspace-change-planning/tasks.md +++ b/openspec/changes/workspace-change-planning/tasks.md @@ -37,15 +37,15 @@ User-testable outcome: A user can change the global profile, run workspace updat User-testable outcome: A user can run `openspec config profile` inside a workspace and choose whether to apply the changed global profile to that workspace now. -- [ ] 3.1 Detect when `openspec config profile` runs from inside an OpenSpec workspace. -- [ ] 3.2 After an actual profile or delivery change inside a workspace, prompt to apply changes to the current workspace now. -- [ ] 3.3 When confirmed, run `openspec workspace update` for the current workspace instead of repo-local `openspec update`. -- [ ] 3.4 When declined, report that global config changed and that `openspec workspace update` applies it later. -- [ ] 3.5 Preserve existing repo-local `openspec config profile` apply behavior outside workspaces. -- [ ] 3.6 Keep `openspec config profile core` non-interactive, but print workspace-specific `openspec workspace update` guidance when run inside a workspace. -- [ ] 3.7 Warn on no-op config profile inside a workspace when workspace-local skills drift from the active global profile. -- [ ] 3.8 Manually run `openspec config profile` inside a workspace for confirm, decline, no-op, drift-warning, and `core` preset paths. -- [ ] 3.9 Review the config-profile UX: prompt wording, project/workspace distinction, no-op behavior, preset guidance, and follow-up guidance are clear. +- [x] 3.1 Detect when `openspec config profile` runs from inside an OpenSpec workspace. +- [x] 3.2 After an actual profile or delivery change inside a workspace, prompt to apply changes to the current workspace now. +- [x] 3.3 When confirmed, run `openspec workspace update` for the current workspace instead of repo-local `openspec update`. +- [x] 3.4 When declined, report that global config changed and that `openspec workspace update` applies it later. +- [x] 3.5 Preserve existing repo-local `openspec config profile` apply behavior outside workspaces. +- [x] 3.6 Keep `openspec config profile core` non-interactive, but print workspace-specific `openspec workspace update` guidance when run inside a workspace. +- [x] 3.7 Warn on no-op config profile inside a workspace when workspace-local skills drift from the active global profile. +- [x] 3.8 Manually run `openspec config profile` inside a workspace for confirm, decline, no-op, drift-warning, and `core` preset paths. +- [x] 3.9 Review the config-profile UX: prompt wording, project/workspace distinction, no-op behavior, preset guidance, and follow-up guidance are clear. ## Phase 4: Workspace Change Creation diff --git a/src/commands/config.ts b/src/commands/config.ts index 42c736d14..42cede322 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -22,6 +22,11 @@ import { import { CORE_WORKFLOWS, ALL_WORKFLOWS, getProfileWorkflows } from '../core/profiles.js'; import { OPENSPEC_DIR_NAME } from '../core/config.js'; import { hasProjectConfigDrift } from '../core/profile-sync-drift.js'; +import { + findWorkspaceRoot, + hasWorkspaceSkillProfileDrift, + readOptionalWorkspaceLocalState, +} from '../core/workspace/index.js'; type ProfileAction = 'both' | 'delivery' | 'workflows' | 'keep'; @@ -41,6 +46,10 @@ interface WorkflowPromptMeta { description: string; } +interface WorkspaceConfigProfileContext { + root: string; +} + const WORKFLOW_PROMPT_META: Record = { propose: { name: 'Propose change', @@ -186,7 +195,20 @@ export function diffProfileState(before: ProfileState, after: ProfileState): Pro }; } -function maybeWarnConfigDrift( +async function resolveWorkspaceConfigProfileContext( + cwd = process.cwd() +): Promise { + const workspaceRoot = await findWorkspaceRoot(cwd); + if (!workspaceRoot) { + return null; + } + + return { + root: workspaceRoot, + }; +} + +function maybeWarnProjectConfigDrift( projectDir: string, state: ProfileState, colorize: (message: string) => string @@ -201,6 +223,41 @@ function maybeWarnConfigDrift( console.log(colorize('Warning: Global config is not applied to this project. Run `openspec update` to sync.')); } +async function maybeWarnConfigDrift( + state: ProfileState, + colorize: (message: string) => string +): Promise { + const workspaceContext = await resolveWorkspaceConfigProfileContext(); + if (workspaceContext) { + let localState = null; + try { + localState = await readOptionalWorkspaceLocalState(workspaceContext.root); + } catch { + return; + } + + if (hasWorkspaceSkillProfileDrift(localState)) { + console.log( + colorize( + 'Warning: Workspace-local agent skills are out of sync with the active global profile. Run `openspec workspace update` to sync.' + ) + ); + } + return; + } + + maybeWarnProjectConfigDrift(process.cwd(), state, colorize); +} + +function printConfigProfileApplyGuidance(workspaceContext: WorkspaceConfigProfileContext | null): void { + if (workspaceContext) { + console.log('Config updated. Run `openspec workspace update` to apply it to workspace-local skills.'); + return; + } + + console.log('Config updated. Run `openspec update` in your projects to apply.'); +} + /** * Register the config command and all its subcommands. * @@ -461,7 +518,8 @@ export function registerConfigCommand(program: Command): void { config.workflows = [...CORE_WORKFLOWS]; // Preserve delivery setting saveGlobalConfig(config); - console.log('Config updated. Run `openspec update` in your projects to apply.'); + const workspaceContext = await resolveWorkspaceConfigProfileContext(); + printConfigProfileApplyGuidance(workspaceContext); return; } @@ -521,7 +579,7 @@ export function registerConfigCommand(program: Command): void { if (action === 'keep') { console.log('No config changes.'); - maybeWarnConfigDrift(process.cwd(), currentState, chalk.yellow); + await maybeWarnConfigDrift(currentState, chalk.yellow); return; } @@ -596,7 +654,7 @@ export function registerConfigCommand(program: Command): void { const diff = diffProfileState(currentState, nextState); if (!diff.hasChanges) { console.log('No config changes.'); - maybeWarnConfigDrift(process.cwd(), nextState, chalk.yellow); + await maybeWarnConfigDrift(nextState, chalk.yellow); return; } @@ -611,6 +669,31 @@ export function registerConfigCommand(program: Command): void { config.workflows = nextState.workflows; saveGlobalConfig(config); + const workspaceContext = await resolveWorkspaceConfigProfileContext(); + if (workspaceContext) { + const applyNow = await confirm({ + message: 'Apply changes to this workspace now?', + default: true, + }); + + if (applyNow) { + try { + execSync('npx openspec workspace update', { + stdio: 'inherit', + cwd: workspaceContext.root, + }); + console.log('Run `openspec workspace update` in your other workspaces to apply.'); + } catch { + console.error('`openspec workspace update` failed. Please run it manually to apply the profile changes.'); + process.exitCode = 1; + } + return; + } + + printConfigProfileApplyGuidance(workspaceContext); + return; + } + // Check if inside an OpenSpec project const projectDir = process.cwd(); const openspecDir = path.join(projectDir, OPENSPEC_DIR_NAME); @@ -632,7 +715,7 @@ export function registerConfigCommand(program: Command): void { } } - console.log('Config updated. Run `openspec update` in your projects to apply.'); + printConfigProfileApplyGuidance(null); } catch (error) { if (isPromptCancellationError(error)) { console.log('Config profile cancelled.'); diff --git a/test/commands/config-profile.test.ts b/test/commands/config-profile.test.ts index 6208403c2..3cd40b3ac 100644 --- a/test/commands/config-profile.test.ts +++ b/test/commands/config-profile.test.ts @@ -3,6 +3,15 @@ import { Command } from 'commander'; import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; +import { execSync } from 'node:child_process'; + +vi.mock('node:child_process', async () => { + const actual = await vi.importActual('node:child_process'); + return { + ...actual, + execSync: vi.fn(), + }; +}); vi.mock('@inquirer/prompts', () => ({ select: vi.fn(), @@ -122,6 +131,49 @@ describe('config profile interactive flow', () => { fs.writeFileSync(verifyCommandPath, '# verify\n', 'utf-8'); } + function setupWorkspaceState( + workspaceRoot: string, + options: { driftedSkills?: boolean } = {} + ): void { + const metadataDir = path.join(workspaceRoot, '.openspec-workspace'); + fs.mkdirSync(metadataDir, { recursive: true }); + fs.writeFileSync( + path.join(metadataDir, 'workspace.yaml'), + 'version: 1\nname: platform\nlinks: {}\n', + 'utf-8' + ); + + const workspaceSkills = options.driftedSkills + ? [ + 'workspace_skills:', + ' selected_agents:', + ' - codex', + ' last_applied_profile: custom', + ' last_applied_delivery: both', + ' last_applied_workflow_ids:', + ' - explore', + ].join('\n') + : [ + 'workspace_skills:', + ' selected_agents:', + ' - codex', + ' last_applied_profile: core', + ' last_applied_delivery: both', + ' last_applied_workflow_ids:', + ' - propose', + ' - explore', + ' - apply', + ' - sync', + ' - archive', + ].join('\n'); + + fs.writeFileSync( + path.join(metadataDir, 'local.yaml'), + `version: 1\npaths: {}\n${workspaceSkills}\n`, + 'utf-8' + ); + } + beforeEach(() => { vi.resetModules(); @@ -140,6 +192,7 @@ describe('config profile interactive flow', () => { consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + vi.mocked(execSync).mockReset(); }); afterEach(() => { @@ -366,6 +419,67 @@ describe('config profile interactive flow', () => { }); }); + it('changed config should ask to apply to the current workspace and print workspace guidance when declined', async () => { + const { saveGlobalConfig, getGlobalConfig } = await import('../../src/core/global-config.js'); + const { select, confirm } = await getPromptMocks(); + + setupWorkspaceState(tempDir); + saveGlobalConfig({ featureFlags: {}, profile: 'core', delivery: 'both', workflows: ['propose', 'explore', 'apply', 'sync', 'archive'] }); + + select.mockResolvedValueOnce('delivery'); + select.mockResolvedValueOnce('skills'); + confirm.mockResolvedValueOnce(false); + + await runConfigCommand(['profile']); + + expect(getGlobalConfig().delivery).toBe('skills'); + expect(confirm).toHaveBeenCalledWith({ + message: 'Apply changes to this workspace now?', + default: true, + }); + expect(execSync).not.toHaveBeenCalled(); + expect(consoleLogSpy).toHaveBeenCalledWith('Config updated. Run `openspec workspace update` to apply it to workspace-local skills.'); + }); + + it('confirmed workspace apply should run workspace update instead of repo-local update', async () => { + const { saveGlobalConfig } = await import('../../src/core/global-config.js'); + const { select, confirm } = await getPromptMocks(); + + setupWorkspaceState(tempDir); + fs.mkdirSync(path.join(tempDir, 'openspec'), { recursive: true }); + saveGlobalConfig({ featureFlags: {}, profile: 'core', delivery: 'both', workflows: ['propose', 'explore', 'apply', 'sync', 'archive'] }); + + select.mockResolvedValueOnce('delivery'); + select.mockResolvedValueOnce('skills'); + confirm.mockResolvedValueOnce(true); + + await runConfigCommand(['profile']); + + expect(execSync).toHaveBeenCalledWith('npx openspec workspace update', { + stdio: 'inherit', + cwd: process.cwd(), + }); + expect(execSync).not.toHaveBeenCalledWith('npx openspec update', expect.anything()); + }); + + it('no-op inside a workspace should warn when workspace skills drift', async () => { + const { saveGlobalConfig } = await import('../../src/core/global-config.js'); + const { select, confirm } = await getPromptMocks(); + + setupWorkspaceState(tempDir, { driftedSkills: true }); + saveGlobalConfig({ featureFlags: {}, profile: 'core', delivery: 'both', workflows: ['propose', 'explore', 'apply', 'sync', 'archive'] }); + + select.mockResolvedValueOnce('delivery'); + select.mockResolvedValueOnce('both'); + + await runConfigCommand(['profile']); + + expect(confirm).not.toHaveBeenCalled(); + expect(consoleLogSpy).toHaveBeenCalledWith('No config changes.'); + expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Workspace-local agent skills are out of sync')); + expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('openspec workspace update')); + }); + it('core preset should preserve delivery setting', async () => { const { saveGlobalConfig, getGlobalConfig } = await import('../../src/core/global-config.js'); const { select, checkbox, confirm } = await getPromptMocks(); @@ -383,6 +497,24 @@ describe('config profile interactive flow', () => { expect(confirm).not.toHaveBeenCalled(); }); + it('core preset inside a workspace should stay non-interactive and print workspace update guidance', async () => { + const { saveGlobalConfig, getGlobalConfig } = await import('../../src/core/global-config.js'); + const { select, checkbox, confirm } = await getPromptMocks(); + + setupWorkspaceState(tempDir, { driftedSkills: true }); + saveGlobalConfig({ featureFlags: {}, profile: 'custom', delivery: 'skills', workflows: ['explore'] }); + + await runConfigCommand(['profile', 'core']); + + const config = getGlobalConfig(); + expect(config.profile).toBe('core'); + expect(config.delivery).toBe('skills'); + expect(select).not.toHaveBeenCalled(); + expect(checkbox).not.toHaveBeenCalled(); + expect(confirm).not.toHaveBeenCalled(); + expect(consoleLogSpy).toHaveBeenCalledWith('Config updated. Run `openspec workspace update` to apply it to workspace-local skills.'); + }); + it('Ctrl+C should cancel without stack trace and set interrupted exit code', async () => { const { select, checkbox, confirm } = await getPromptMocks(); const cancellationError = new Error('User force closed the prompt with SIGINT'); From 25eaa0d02d5ba8398c4eb12f791bf34dc7d814d1 Mon Sep 17 00:00:00 2001 From: TabishB Date: Thu, 14 May 2026 02:28:16 +1000 Subject: [PATCH 05/14] Implement workspace change creation phase --- .../workspace-change-planning/tasks.md | 22 +-- schemas/workspace-planning/schema.yaml | 72 ++++++++ .../workspace-planning/templates/design.md | 33 ++++ .../workspace-planning/templates/proposal.md | 28 +++ schemas/workspace-planning/templates/spec.md | 9 + schemas/workspace-planning/templates/tasks.md | 15 ++ src/cli/index.ts | 2 + src/commands/workflow/new-change.ts | 73 +++++++- src/core/artifact-graph/types.ts | 7 +- src/core/index.ts | 1 + src/core/planning-home.ts | 165 ++++++++++++++++++ src/utils/change-utils.ts | 22 ++- 12 files changed, 426 insertions(+), 23 deletions(-) create mode 100644 schemas/workspace-planning/schema.yaml create mode 100644 schemas/workspace-planning/templates/design.md create mode 100644 schemas/workspace-planning/templates/proposal.md create mode 100644 schemas/workspace-planning/templates/spec.md create mode 100644 schemas/workspace-planning/templates/tasks.md create mode 100644 src/core/planning-home.ts diff --git a/openspec/changes/workspace-change-planning/tasks.md b/openspec/changes/workspace-change-planning/tasks.md index 86a424479..f26f4e64d 100644 --- a/openspec/changes/workspace-change-planning/tasks.md +++ b/openspec/changes/workspace-change-planning/tasks.md @@ -51,17 +51,17 @@ User-testable outcome: A user can run `openspec config profile` inside a workspa User-testable outcome: A user can create a workspace-level change from the coordination root, inspect its workspace planning artifacts, and confirm linked repos were not edited. -- [ ] 4.1 Add a built-in `workspace-planning` schema and templates that keep the normal proposal/specs/design/tasks artifact shape. -- [ ] 4.2 Define the workspace-planning specs artifact with nested `specs/**/*.md` output support and instructions for `specs///spec.md`. -- [ ] 4.3 Add workspace-aware change creation from the workspace coordination root. -- [ ] 4.4 Default workspace-scoped change creation to the `workspace-planning` schema. -- [ ] 4.5 Store workspace-level changes under the workspace planning path rather than under linked repos or folders. -- [ ] 4.6 Capture the product goal once at the workspace change level. -- [ ] 4.7 Record or validate affected area names through workspace-scoped specs or task sections using registered workspace link names where applicable. -- [ ] 4.8 Ensure creating a workspace change does not create repo-local OpenSpec artifacts or edit linked repos. -- [ ] 4.9 Preserve repo-local change creation behavior outside workspaces. -- [ ] 4.10 Manually create a workspace change from a coordination root and verify the generated artifacts, workspace-scoped specs/tasks, affected areas, and untouched linked repos. -- [ ] 4.11 Review the change creation UX: goal capture, affected-area identification, artifact paths, and next-step guidance feel clear. +- [x] 4.1 Add a built-in `workspace-planning` schema and templates that keep the normal proposal/specs/design/tasks artifact shape. +- [x] 4.2 Define the workspace-planning specs artifact with nested `specs/**/*.md` output support and instructions for `specs///spec.md`. +- [x] 4.3 Add workspace-aware change creation from the workspace coordination root. +- [x] 4.4 Default workspace-scoped change creation to the `workspace-planning` schema. +- [x] 4.5 Store workspace-level changes under the workspace planning path rather than under linked repos or folders. +- [x] 4.6 Capture the product goal once at the workspace change level. +- [x] 4.7 Record or validate affected area names through workspace-scoped specs or task sections using registered workspace link names where applicable. +- [x] 4.8 Ensure creating a workspace change does not create repo-local OpenSpec artifacts or edit linked repos. +- [x] 4.9 Preserve repo-local change creation behavior outside workspaces. +- [x] 4.10 Manually create a workspace change from a coordination root and verify the generated artifacts, workspace-scoped specs/tasks, affected areas, and untouched linked repos. +- [x] 4.11 Review the change creation UX: goal capture, affected-area identification, artifact paths, and next-step guidance feel clear. ## Phase 5: Planning Home And Agent Context diff --git a/schemas/workspace-planning/schema.yaml b/schemas/workspace-planning/schema.yaml new file mode 100644 index 000000000..f8bf64252 --- /dev/null +++ b/schemas/workspace-planning/schema.yaml @@ -0,0 +1,72 @@ +name: workspace-planning +version: 1 +description: Workspace planning workflow for cross-area changes +artifacts: + - id: proposal + generates: proposal.md + description: Shared workspace proposal with the product goal, scope, affected areas, and impact + template: proposal.md + instruction: | + Create the workspace-level proposal that captures the shared product goal once. + + Sections: + - **Why**: Explain the product goal or problem in 1-2 concise paragraphs. + - **What Changes**: List the cross-area behavior, workflow, or capability changes. + - **Affected Areas**: Name known affected areas using registered workspace link names where applicable. If scope is still being explored, say what remains unresolved. + - **Capabilities**: Identify workspace-scoped capabilities that need specs. Area-specific requirements should later live under `specs///spec.md`. + - **Impact**: Summarize user-facing impact, planning impact, and likely implementation homes without creating repo-local artifacts. + + Keep linked repos and folders as exploration context until an explicit implementation workflow selects an affected area. + requires: [] + + - id: specs + generates: "specs/**/*.md" + description: Workspace-scoped specs organized by affected area and capability + template: spec.md + instruction: | + Create workspace-scoped specification files that define WHAT should change. + + Use `specs///spec.md` for area-specific requirements. The first path segment should be a registered workspace link name when a registered area owns the requirement. If the area is unresolved, use an exploratory area name and make the unresolved question explicit in the requirement or scenario. + + These specs are planning artifacts under the workspace change root. Do not create repo-local spec files in linked repos during workspace planning. + + Delta operations (use ## headers): + - **ADDED Requirements**: New workspace-scoped behavior. + - **MODIFIED Requirements**: Changed behavior; include the full updated requirement. + - **REMOVED Requirements**: Deprecated behavior with Reason and Migration. + - **RENAMED Requirements**: Name changes only; use FROM:/TO: format. + + Each requirement must use SHALL/MUST language and include at least one `#### Scenario:` block. + requires: + - proposal + + - id: design + generates: design.md + description: Cross-area technical design and coordination decisions + template: design.md + instruction: | + Create the cross-area design document for workspace planning. + + Focus on decisions that affect multiple areas, handoffs between areas, shared constraints, sequencing risks, and how the workspace plan should stay the source of truth. Avoid line-by-line implementation details and do not instruct agents to edit linked repos until an explicit implementation workflow provides an allowed edit root. + requires: + - proposal + + - id: tasks + generates: tasks.md + description: Coordination checklist for workspace planning and later affected-area implementation + template: tasks.md + instruction: | + Create the workspace coordination task list. + + Group tasks by phase or affected area as useful. Each actionable item must be a checkbox using `- [ ]`. When implementation tasks are area-specific, name the affected area and keep the task at planning granularity until a later implementation workflow selects an allowed edit root. + requires: + - specs + - design + +apply: + requires: [tasks] + tracks: tasks.md + instruction: | + Read the workspace planning context from status and instructions output before applying. + Select an affected area and confirm an allowed edit root before making implementation edits. + Until an explicit implementation context is available, treat linked repos and folders as read-only exploration context. diff --git a/schemas/workspace-planning/templates/design.md b/schemas/workspace-planning/templates/design.md new file mode 100644 index 000000000..d8a491ef3 --- /dev/null +++ b/schemas/workspace-planning/templates/design.md @@ -0,0 +1,33 @@ +## Context + +Summarize the workspace planning context, relevant linked areas, and constraints. + +## Goals / Non-Goals + +**Goals:** +- + +**Non-Goals:** +- Creating repo-local implementation artifacts before an affected area is selected. + +## Decisions + +### Decision: + +<decision and rationale> + +Alternative considered: <alternative and why it was not chosen> + +## Risks / Trade-offs + +- <risk> -> <mitigation> + +## Coordination Notes + +- Affected areas: +- Open handoffs: +- Implementation entry criteria: + +## Open Questions + +- diff --git a/schemas/workspace-planning/templates/proposal.md b/schemas/workspace-planning/templates/proposal.md new file mode 100644 index 000000000..b8c018276 --- /dev/null +++ b/schemas/workspace-planning/templates/proposal.md @@ -0,0 +1,28 @@ +## Why + +Describe the shared product goal, problem, or opportunity that makes this workspace-level change worth planning. + +## What Changes + +- + +## Affected Areas + +- Known: +- Unresolved: + +## Capabilities + +### New Capabilities + +- + +### Modified Capabilities + +- + +## Impact + +- Workspace planning: +- Linked repos or folders: +- User-facing behavior: diff --git a/schemas/workspace-planning/templates/spec.md b/schemas/workspace-planning/templates/spec.md new file mode 100644 index 000000000..2826b3f07 --- /dev/null +++ b/schemas/workspace-planning/templates/spec.md @@ -0,0 +1,9 @@ +## ADDED Requirements + +### Requirement: <workspace requirement name> +The workspace plan SHALL describe the required behavior and affected area without creating repo-local artifacts during planning. + +#### Scenario: <scenario name> +- **GIVEN** <context> +- **WHEN** <action> +- **THEN** <observable result> diff --git a/schemas/workspace-planning/templates/tasks.md b/schemas/workspace-planning/templates/tasks.md new file mode 100644 index 000000000..c24ee7155 --- /dev/null +++ b/schemas/workspace-planning/templates/tasks.md @@ -0,0 +1,15 @@ +## 1. Workspace Planning + +- [ ] 1.1 Confirm the shared product goal and unresolved scope questions. +- [ ] 1.2 Identify affected areas using registered workspace link names where applicable. +- [ ] 1.3 Review workspace-scoped specs and design before selecting implementation areas. + +## 2. Affected Area Implementation + +- [ ] 2.1 Select an affected area and confirm its allowed edit root before implementation. +- [ ] 2.2 Create or update repo-local implementation artifacts only after the area is selected. + +## 3. Verification + +- [ ] 3.1 Verify workspace planning artifacts remain the source of truth. +- [ ] 3.2 Record manual acceptance evidence and follow-up fixes. diff --git a/src/cli/index.ts b/src/cli/index.ts index f1278dbd7..d40fdf29c 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -498,6 +498,8 @@ newCmd .command('change <name>') .description('Create a new change directory') .option('--description <text>', 'Description to add to README.md') + .option('--goal <text>', 'Workspace product goal to store with the change') + .option('--areas <names>', 'Comma-separated affected workspace link names') .option('--schema <name>', `Workflow schema to use (default: ${DEFAULT_SCHEMA})`) .action(async (name: string, options: NewChangeOptions) => { try { diff --git a/src/commands/workflow/new-change.ts b/src/commands/workflow/new-change.ts index 1435e1add..8a1d91d38 100644 --- a/src/commands/workflow/new-change.ts +++ b/src/commands/workflow/new-change.ts @@ -7,6 +7,11 @@ import ora from 'ora'; import path from 'path'; import { createChange, validateChangeName } from '../../utils/change-utils.js'; +import { + formatChangeLocation, + resolveCurrentPlanningHomeSync, + type PlanningHome, +} from '../../core/planning-home.js'; import { validateSchemaExists } from './shared.js'; // ----------------------------------------------------------------------------- @@ -15,6 +20,8 @@ import { validateSchemaExists } from './shared.js'; export interface NewChangeOptions { description?: string; + goal?: string; + areas?: string; schema?: string; } @@ -22,6 +29,35 @@ export interface NewChangeOptions { // Command Implementation // ----------------------------------------------------------------------------- +function parseAffectedAreas(value: string | undefined): string[] { + return (value ?? '') + .split(',') + .map((area) => area.trim()) + .filter((area) => area.length > 0); +} + +function validateWorkspaceAffectedAreas(planningHome: PlanningHome, affectedAreas: string[]): void { + if (affectedAreas.length === 0) { + return; + } + + if (planningHome.kind !== 'workspace') { + throw new Error('--areas can only be used when creating a workspace-scoped change'); + } + + const validAreas = new Set(planningHome.workspace?.links ?? []); + const invalidAreas = affectedAreas.filter((area) => !validAreas.has(area)); + + if (invalidAreas.length > 0) { + const validList = [...validAreas].sort((a, b) => a.localeCompare(b)); + const validMessage = validList.length > 0 ? validList.join(', ') : '(no registered links)'; + throw new Error( + `Invalid affected area${invalidAreas.length === 1 ? '' : 's'}: ${invalidAreas.join(', ')}. ` + + `Valid workspace link names: ${validMessage}` + ); + } +} + export async function newChangeCommand(name: string | undefined, options: NewChangeOptions): Promise<void> { if (!name) { throw new Error('Missing required argument <name>'); @@ -32,28 +68,53 @@ export async function newChangeCommand(name: string | undefined, options: NewCha throw new Error(validation.error); } - const projectRoot = process.cwd(); + const planningHome = resolveCurrentPlanningHomeSync(); + const projectRoot = planningHome.root; + const affectedAreas = parseAffectedAreas(options.areas); + validateWorkspaceAffectedAreas(planningHome, affectedAreas); // Validate schema if provided if (options.schema) { validateSchemaExists(options.schema, projectRoot); } - const schemaDisplay = options.schema ? ` with schema '${options.schema}'` : ''; + const resolvedSchema = options.schema ?? planningHome.defaultSchema; + const schemaDisplay = ` with schema '${resolvedSchema}'`; const spinner = ora(`Creating change '${name}'${schemaDisplay}...`).start(); try { - const result = await createChange(projectRoot, name, { schema: options.schema }); + const workspaceGoal = planningHome.kind === 'workspace' + ? options.goal ?? options.description + : options.goal; + const result = await createChange(projectRoot, name, { + schema: options.schema, + defaultSchema: planningHome.defaultSchema, + changesDir: planningHome.changesDir, + metadata: { + ...(workspaceGoal ? { goal: workspaceGoal } : {}), + ...(affectedAreas.length > 0 ? { affected_areas: affectedAreas } : {}), + }, + }); // If description provided, create README.md with description if (options.description) { const { promises: fs } = await import('fs'); - const changeDir = path.join(projectRoot, 'openspec', 'changes', name); - const readmePath = path.join(changeDir, 'README.md'); + const readmePath = path.join(result.changeDir, 'README.md'); await fs.writeFile(readmePath, `# ${name}\n\n${options.description}\n`, 'utf-8'); } - spinner.succeed(`Created change '${name}' at openspec/changes/${name}/ (schema: ${result.schema})`); + const location = formatChangeLocation(planningHome, name); + const scope = planningHome.kind === 'workspace' ? 'workspace change' : 'change'; + spinner.succeed(`Created ${scope} '${name}' at ${location}/ (schema: ${result.schema})`); + + if (planningHome.kind === 'workspace') { + if (affectedAreas.length > 0) { + console.log(`Affected areas: ${affectedAreas.join(', ')}`); + } else { + console.log('Affected areas: unresolved; identify them in workspace specs or tasks as planning continues.'); + } + console.log('Next: run openspec status --change "' + name + '" to inspect workspace planning artifacts.'); + } } catch (error) { spinner.fail(`Failed to create change '${name}'`); throw error; diff --git a/src/core/artifact-graph/types.ts b/src/core/artifact-graph/types.ts index fb0d12703..03b34cc3b 100644 --- a/src/core/artifact-graph/types.ts +++ b/src/core/artifact-graph/types.ts @@ -49,6 +49,12 @@ export const ChangeMetadataSchema = z.object({ message: 'created must be YYYY-MM-DD format', }) .optional(), + + // Optional workspace planning metadata. These fields are intentionally + // lightweight and do not replace the normal proposal/specs/design/tasks + // artifacts as the source of planning detail. + goal: z.string().min(1).optional(), + affected_areas: z.array(z.string().min(1)).optional(), }); export type ChangeMetadata = z.infer<typeof ChangeMetadataSchema>; @@ -62,4 +68,3 @@ export type CompletedSet = Set<string>; export interface BlockedArtifacts { [artifactId: string]: string[]; } - diff --git a/src/core/index.ts b/src/core/index.ts index d9aa8afb8..a4b65abdf 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -13,3 +13,4 @@ export { } from './global-config.js'; export * from './workspace/index.js'; +export * from './planning-home.js'; diff --git a/src/core/planning-home.ts b/src/core/planning-home.ts new file mode 100644 index 000000000..b0fef2ae6 --- /dev/null +++ b/src/core/planning-home.ts @@ -0,0 +1,165 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import { + getWorkspaceChangesDir, + getWorkspaceSharedStatePath, + parseWorkspaceSharedState, + type WorkspaceSharedState, +} from './workspace/index.js'; +import { FileSystemUtils } from '../utils/file-system.js'; + +export type PlanningHomeKind = 'repo' | 'workspace'; + +export interface PlanningHome { + kind: PlanningHomeKind; + root: string; + changesDir: string; + defaultSchema: string; + workspace?: { + name: string; + links: string[]; + }; +} + +export interface ResolvePlanningHomeOptions { + startPath?: string; + allowImplicitRepoRoot?: boolean; +} + +const REPO_DEFAULT_SCHEMA = 'spec-driven'; +const WORKSPACE_DEFAULT_SCHEMA = 'workspace-planning'; + +function pathExistsAsDirectory(candidatePath: string): boolean { + try { + return fs.statSync(candidatePath).isDirectory(); + } catch { + return false; + } +} + +function pathExistsAsFile(candidatePath: string): boolean { + try { + return fs.statSync(candidatePath).isFile(); + } catch { + return false; + } +} + +function getSearchStartDirectory(startPath: string): string { + const resolved = path.resolve(startPath); + + try { + const stats = fs.statSync(resolved); + return stats.isDirectory() ? resolved : path.dirname(resolved); + } catch { + return resolved; + } +} + +function findNearestAncestor(startPath: string, predicate: (dirPath: string) => boolean): string | null { + let currentDir = getSearchStartDirectory(startPath); + + while (true) { + if (predicate(currentDir)) { + return FileSystemUtils.canonicalizeExistingPath(currentDir); + } + + const parentDir = path.dirname(currentDir); + if (parentDir === currentDir) { + return null; + } + + currentDir = parentDir; + } +} + +export function findWorkspacePlanningRootSync(startPath = process.cwd()): string | null { + return findNearestAncestor(startPath, (dirPath) => + pathExistsAsFile(getWorkspaceSharedStatePath(dirPath)) + ); +} + +export function findRepoPlanningRootSync(startPath = process.cwd()): string | null { + return findNearestAncestor(startPath, (dirPath) => + pathExistsAsDirectory(path.join(dirPath, 'openspec')) + ); +} + +function isSameOrDescendant(rootPath: string, candidatePath: string): boolean { + const relative = path.relative(rootPath, candidatePath); + return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative)); +} + +function countPathSegments(candidatePath: string): number { + return path.resolve(candidatePath).split(path.sep).filter(Boolean).length; +} + +function readWorkspaceSharedStateSync(workspaceRoot: string): WorkspaceSharedState | null { + try { + return parseWorkspaceSharedState( + fs.readFileSync(getWorkspaceSharedStatePath(workspaceRoot), 'utf-8') + ); + } catch { + return null; + } +} + +function workspacePlanningHome(workspaceRoot: string): PlanningHome { + const sharedState = readWorkspaceSharedStateSync(workspaceRoot); + + return { + kind: 'workspace', + root: workspaceRoot, + changesDir: getWorkspaceChangesDir(workspaceRoot), + defaultSchema: WORKSPACE_DEFAULT_SCHEMA, + workspace: { + name: sharedState?.name ?? path.basename(workspaceRoot), + links: Object.keys(sharedState?.links ?? {}).sort((a, b) => a.localeCompare(b)), + }, + }; +} + +function repoPlanningHome(repoRoot: string): PlanningHome { + return { + kind: 'repo', + root: repoRoot, + changesDir: path.join(repoRoot, 'openspec', 'changes'), + defaultSchema: REPO_DEFAULT_SCHEMA, + }; +} + +export function resolveCurrentPlanningHomeSync( + options: ResolvePlanningHomeOptions = {} +): PlanningHome { + const startPath = options.startPath ?? process.cwd(); + const searchStart = getSearchStartDirectory(startPath); + const workspaceRoot = findWorkspacePlanningRootSync(searchStart); + const repoRoot = findRepoPlanningRootSync(searchStart); + + if (workspaceRoot && isSameOrDescendant(workspaceRoot, searchStart)) { + if (!repoRoot || countPathSegments(workspaceRoot) >= countPathSegments(repoRoot)) { + return workspacePlanningHome(workspaceRoot); + } + } + + if (repoRoot) { + return repoPlanningHome(repoRoot); + } + + if (options.allowImplicitRepoRoot === false) { + throw new Error('No OpenSpec planning home found from the current directory.'); + } + + return repoPlanningHome(FileSystemUtils.canonicalizeExistingPath(searchStart)); +} + +export function getChangeDir(planningHome: PlanningHome, changeName: string): string { + return path.join(planningHome.changesDir, changeName); +} + +export function formatChangeLocation(planningHome: PlanningHome, changeName: string): string { + const changeDir = getChangeDir(planningHome, changeName); + const relative = path.relative(planningHome.root, changeDir); + return relative.length > 0 ? relative : changeDir; +} diff --git a/src/utils/change-utils.ts b/src/utils/change-utils.ts index 671a92b79..ce25afa52 100644 --- a/src/utils/change-utils.ts +++ b/src/utils/change-utils.ts @@ -2,6 +2,7 @@ import path from 'path'; import { FileSystemUtils } from './file-system.js'; import { writeChangeMetadata, validateSchemaName } from './change-metadata.js'; import { readProjectConfig } from '../core/project-config.js'; +import type { ChangeMetadata } from '../core/artifact-graph/types.js'; const DEFAULT_SCHEMA = 'spec-driven'; @@ -11,6 +12,12 @@ const DEFAULT_SCHEMA = 'spec-driven'; export interface CreateChangeOptions { /** The workflow schema to use (default: 'spec-driven') */ schema?: string; + /** Default schema to use when no explicit schema or project config is present */ + defaultSchema?: string; + /** Directory that should contain the change directories */ + changesDir?: string; + /** Additional metadata to persist in the change's .openspec.yaml */ + metadata?: Partial<Pick<ChangeMetadata, 'goal' | 'affected_areas'>>; } /** @@ -19,6 +26,8 @@ export interface CreateChangeOptions { export interface CreateChangeResult { /** The schema that was actually used (resolved from options, config, or default) */ schema: string; + /** Absolute path to the created change directory */ + changeDir: string; } /** @@ -120,7 +129,9 @@ export async function createChange( throw new Error(validation.error); } - // Determine schema: explicit option → project config → hardcoded default + const defaultSchema = options.defaultSchema ?? DEFAULT_SCHEMA; + + // Determine schema: explicit option → project config → supplied default let schemaName: string; if (options.schema) { schemaName = options.schema; @@ -128,10 +139,10 @@ export async function createChange( // Try to read from project config try { const config = readProjectConfig(projectRoot); - schemaName = config?.schema ?? DEFAULT_SCHEMA; + schemaName = config?.schema ?? defaultSchema; } catch { // If config read fails, use default - schemaName = DEFAULT_SCHEMA; + schemaName = defaultSchema; } } @@ -139,7 +150,7 @@ export async function createChange( validateSchemaName(schemaName, projectRoot); // Build the change directory path - const changeDir = path.join(projectRoot, 'openspec', 'changes', name); + const changeDir = path.join(options.changesDir ?? path.join(projectRoot, 'openspec', 'changes'), name); // Check if change already exists if (await FileSystemUtils.directoryExists(changeDir)) { @@ -154,7 +165,8 @@ export async function createChange( writeChangeMetadata(changeDir, { schema: schemaName, created: today, + ...options.metadata, }, projectRoot); - return { schema: schemaName }; + return { schema: schemaName, changeDir }; } From 92528decfcc063be09e6a14e2fe942a28a260af9 Mon Sep 17 00:00:00 2001 From: TabishB <tabishbidiwale@gmail.com> Date: Thu, 14 May 2026 02:35:28 +1000 Subject: [PATCH 06/14] Enrich planning context for workspace changes --- .../workspace-change-planning/tasks.md | 14 +- src/commands/workflow/instructions.ts | 43 +++- src/commands/workflow/shared.ts | 16 +- src/commands/workflow/status.ts | 24 ++- src/core/artifact-graph/index.ts | 5 + src/core/artifact-graph/instruction-loader.ts | 188 +++++++++++++++++- src/utils/change-metadata.ts | 5 +- 7 files changed, 262 insertions(+), 33 deletions(-) diff --git a/openspec/changes/workspace-change-planning/tasks.md b/openspec/changes/workspace-change-planning/tasks.md index f26f4e64d..790b29756 100644 --- a/openspec/changes/workspace-change-planning/tasks.md +++ b/openspec/changes/workspace-change-planning/tasks.md @@ -67,13 +67,13 @@ User-testable outcome: A user can create a workspace-level change from the coord User-testable outcome: A user can run status and instructions for repo-local and workspace changes and see the resolved planning home, artifact paths, affected areas, constraints, and next steps. -- [ ] 5.1 Introduce a shared planning-home resolver that identifies repo-local versus workspace planning homes. -- [ ] 5.2 Enrich `openspec status --change <id> --json` with planning home, change root, relevant artifact paths, affected areas, next steps, and action context. -- [ ] 5.3 Enrich `openspec instructions <artifact> --change <id> --json` with resolved artifact paths for repo-local and workspace-scoped changes. -- [ ] 5.4 Keep workspace-level planning as the source of truth until an explicit implementation workflow selects an affected area. -- [ ] 5.5 Preserve nested workspace spec paths in status and instructions output without flattening them into repo-local capability paths. -- [ ] 5.6 Manually run status and instructions for both repo-local and workspace-scoped changes and verify paths and action context are correct. -- [ ] 5.7 Review the planning-context UX: human output, JSON field names, and next-step guidance are easy for users and agents to follow. +- [x] 5.1 Introduce a shared planning-home resolver that identifies repo-local versus workspace planning homes. +- [x] 5.2 Enrich `openspec status --change <id> --json` with planning home, change root, relevant artifact paths, affected areas, next steps, and action context. +- [x] 5.3 Enrich `openspec instructions <artifact> --change <id> --json` with resolved artifact paths for repo-local and workspace-scoped changes. +- [x] 5.4 Keep workspace-level planning as the source of truth until an explicit implementation workflow selects an affected area. +- [x] 5.5 Preserve nested workspace spec paths in status and instructions output without flattening them into repo-local capability paths. +- [x] 5.6 Manually run status and instructions for both repo-local and workspace-scoped changes and verify paths and action context are correct. +- [x] 5.7 Review the planning-context UX: human output, JSON field names, and next-step guidance are easy for users and agents to follow. ## Phase 6: Workflow Skill Instructions diff --git a/src/commands/workflow/instructions.ts b/src/commands/workflow/instructions.ts index 7afba1475..b3ca42e37 100644 --- a/src/commands/workflow/instructions.ts +++ b/src/commands/workflow/instructions.ts @@ -15,6 +15,7 @@ import { resolveArtifactOutputs, type ArtifactInstructions, } from '../../core/artifact-graph/index.js'; +import { getChangeDir, resolveCurrentPlanningHomeSync } from '../../core/planning-home.js'; import { validateChangeExists, validateSchemaExists, @@ -49,8 +50,13 @@ export async function instructionsCommand( const spinner = options.json ? undefined : ora('Generating instructions...').start(); try { - const projectRoot = process.cwd(); - const changeName = await validateChangeExists(options.change, projectRoot); + const planningHome = resolveCurrentPlanningHomeSync(); + const projectRoot = planningHome.root; + const changeName = await validateChangeExists( + options.change, + projectRoot, + planningHome.changesDir + ); // Validate schema if explicitly provided if (options.schema) { @@ -58,7 +64,10 @@ export async function instructionsCommand( } // loadChangeContext will auto-detect schema from metadata if not provided - const context = loadChangeContext(projectRoot, changeName, options.schema); + const context = loadChangeContext(projectRoot, changeName, options.schema, { + changeDir: getChangeDir(planningHome, changeName), + planningHome, + }); if (!artifactId) { spinner?.stop(); @@ -101,7 +110,7 @@ export function printInstructionsText(instructions: ArtifactInstructions, isBloc changeName, schemaName, changeDir, - outputPath, + resolvedOutputPath, description, instruction, context, @@ -171,7 +180,7 @@ export function printInstructionsText(instructions: ArtifactInstructions, isBloc // Output location console.log('<output>'); - console.log(`Write to: ${path.join(changeDir, outputPath)}`); + console.log(`Write to: ${resolvedOutputPath}`); console.log('</output>'); console.log(); @@ -246,10 +255,14 @@ function parseTasksFile(content: string): TaskItem[] { export async function generateApplyInstructions( projectRoot: string, changeName: string, - schemaName?: string + schemaName?: string, + planningHome = resolveCurrentPlanningHomeSync({ startPath: projectRoot }) ): Promise<ApplyInstructions> { // loadChangeContext will auto-detect schema from metadata if not provided - const context = loadChangeContext(projectRoot, changeName, schemaName); + const context = loadChangeContext(projectRoot, changeName, schemaName, { + changeDir: getChangeDir(planningHome, changeName), + planningHome, + }); const changeDir = context.changeDir; // Get the full schema to access the apply phase configuration @@ -343,8 +356,13 @@ export async function applyInstructionsCommand(options: ApplyInstructionsOptions const spinner = options.json ? undefined : ora('Generating apply instructions...').start(); try { - const projectRoot = process.cwd(); - const changeName = await validateChangeExists(options.change, projectRoot); + const planningHome = resolveCurrentPlanningHomeSync(); + const projectRoot = planningHome.root; + const changeName = await validateChangeExists( + options.change, + projectRoot, + planningHome.changesDir + ); // Validate schema if explicitly provided if (options.schema) { @@ -352,7 +370,12 @@ export async function applyInstructionsCommand(options: ApplyInstructionsOptions } // generateApplyInstructions uses loadChangeContext which auto-detects schema - const instructions = await generateApplyInstructions(projectRoot, changeName, options.schema); + const instructions = await generateApplyInstructions( + projectRoot, + changeName, + options.schema, + planningHome + ); spinner?.stop(); diff --git a/src/commands/workflow/shared.ts b/src/commands/workflow/shared.ts index 43c9aa46c..638bfcb3b 100644 --- a/src/commands/workflow/shared.ts +++ b/src/commands/workflow/shared.ts @@ -90,8 +90,11 @@ export function getStatusIndicator(status: 'done' | 'ready' | 'blocked'): string * Returns the list of available change directory names under openspec/changes/. * Excludes the archive directory and hidden directories. */ -export async function getAvailableChanges(projectRoot: string): Promise<string[]> { - const changesPath = path.join(projectRoot, 'openspec', 'changes'); +export async function getAvailableChanges( + projectRoot: string, + changesDir = path.join(projectRoot, 'openspec', 'changes') +): Promise<string[]> { + const changesPath = changesDir; try { const entries = await fs.promises.readdir(changesPath, { withFileTypes: true }); return entries @@ -109,10 +112,11 @@ export async function getAvailableChanges(projectRoot: string): Promise<string[] */ export async function validateChangeExists( changeName: string | undefined, - projectRoot: string + projectRoot: string, + changesDir = path.join(projectRoot, 'openspec', 'changes') ): Promise<string> { if (!changeName) { - const available = await getAvailableChanges(projectRoot); + const available = await getAvailableChanges(projectRoot, changesDir); if (available.length === 0) { throw new Error('No changes found. Create one with: openspec new change <name>'); } @@ -128,11 +132,11 @@ export async function validateChangeExists( } // Check directory existence directly - const changePath = path.join(projectRoot, 'openspec', 'changes', changeName); + const changePath = path.join(changesDir, changeName); const exists = fs.existsSync(changePath) && fs.statSync(changePath).isDirectory(); if (!exists) { - const available = await getAvailableChanges(projectRoot); + const available = await getAvailableChanges(projectRoot, changesDir); if (available.length === 0) { throw new Error( `Change '${changeName}' not found. No changes exist. Create one with: openspec new change <name>` diff --git a/src/commands/workflow/status.ts b/src/commands/workflow/status.ts index 1109ab188..f5739fef8 100644 --- a/src/commands/workflow/status.ts +++ b/src/commands/workflow/status.ts @@ -6,6 +6,7 @@ import ora from 'ora'; import chalk from 'chalk'; +import { resolveCurrentPlanningHomeSync, getChangeDir } from '../../core/planning-home.js'; import { loadChangeContext, formatChangeStatus, @@ -37,12 +38,13 @@ export async function statusCommand(options: StatusOptions): Promise<void> { const spinner = options.json ? undefined : ora('Loading change status...').start(); try { - const projectRoot = process.cwd(); + const planningHome = resolveCurrentPlanningHomeSync(); + const projectRoot = planningHome.root; // Handle no-changes case gracefully — status is informational, // so "no changes" is a valid state, not an error. if (!options.change) { - const available = await getAvailableChanges(projectRoot); + const available = await getAvailableChanges(projectRoot, planningHome.changesDir); if (available.length === 0) { spinner?.stop(); if (options.json) { @@ -59,7 +61,11 @@ export async function statusCommand(options: StatusOptions): Promise<void> { ); } - const changeName = await validateChangeExists(options.change, projectRoot); + const changeName = await validateChangeExists( + options.change, + projectRoot, + planningHome.changesDir + ); // Validate schema if explicitly provided if (options.schema) { @@ -67,7 +73,10 @@ export async function statusCommand(options: StatusOptions): Promise<void> { } // loadChangeContext will auto-detect schema from metadata if not provided - const context = loadChangeContext(projectRoot, changeName, options.schema); + const context = loadChangeContext(projectRoot, changeName, options.schema, { + changeDir: getChangeDir(planningHome, changeName), + planningHome, + }); const status = formatChangeStatus(context); spinner?.stop(); @@ -90,6 +99,13 @@ export function printStatusText(status: ChangeStatus): void { console.log(`Change: ${status.changeName}`); console.log(`Schema: ${status.schemaName}`); + if (status.planningHome) { + const label = status.planningHome.kind === 'workspace' + ? `workspace${status.planningHome.workspaceName ? ` (${status.planningHome.workspaceName})` : ''}` + : 'repo'; + console.log(`Planning home: ${label}`); + console.log(`Change root: ${status.changeRoot}`); + } console.log(`Progress: ${doneCount}/${total} artifacts complete`); console.log(); diff --git a/src/core/artifact-graph/index.ts b/src/core/artifact-graph/index.ts index 8ec732846..24ab2d383 100644 --- a/src/core/artifact-graph/index.ts +++ b/src/core/artifact-graph/index.ts @@ -38,8 +38,13 @@ export { formatChangeStatus, TemplateLoadError, type ChangeContext, + type LoadChangeContextOptions, type ArtifactInstructions, type DependencyInfo, type ArtifactStatus, type ChangeStatus, + type ArtifactPathSummary, + type PlanningHomeSummary, + type AffectedAreasSummary, + type ActionContext, } from './instruction-loader.js'; diff --git a/src/core/artifact-graph/instruction-loader.ts b/src/core/artifact-graph/instruction-loader.ts index b8b2675bb..323c4df32 100644 --- a/src/core/artifact-graph/instruction-loader.ts +++ b/src/core/artifact-graph/instruction-loader.ts @@ -3,9 +3,11 @@ import * as path from 'node:path'; import { getSchemaDir, resolveSchema } from './resolver.js'; import { ArtifactGraph } from './graph.js'; import { detectCompleted } from './state.js'; -import { resolveSchemaForChange } from '../../utils/change-metadata.js'; +import { resolveArtifactOutputs } from './outputs.js'; +import { readChangeMetadata, resolveSchemaForChange } from '../../utils/change-metadata.js'; import { FileSystemUtils } from '../../utils/file-system.js'; import { readProjectConfig, validateConfigRules } from '../project-config.js'; +import type { PlanningHome } from '../planning-home.js'; import type { Artifact, CompletedSet } from './types.js'; // Session-level cache for validation warnings (avoid repeating same warnings) @@ -40,6 +42,13 @@ export interface ChangeContext { changeDir: string; /** Project root directory */ projectRoot: string; + /** Resolved planning home for this change */ + planningHome?: PlanningHome; +} + +export interface LoadChangeContextOptions { + changeDir?: string; + planningHome?: PlanningHome; } /** @@ -54,8 +63,14 @@ export interface ArtifactInstructions { schemaName: string; /** Full path to change directory */ changeDir: string; + /** Resolved planning home for this change */ + planningHome?: PlanningHomeSummary; /** Output path pattern (e.g., "proposal.md") */ outputPath: string; + /** Absolute output path or glob pattern resolved under the change directory */ + resolvedOutputPath: string; + /** Existing concrete output files for this artifact */ + existingOutputPaths: string[]; /** Artifact description */ description: string; /** Guidance on how to create this artifact (from schema instruction field) */ @@ -108,6 +123,18 @@ export interface ChangeStatus { changeName: string; /** Schema name */ schemaName: string; + /** Resolved planning home for this change */ + planningHome?: PlanningHomeSummary; + /** Full path to the change root */ + changeRoot: string; + /** Absolute artifact path details keyed by artifact ID */ + artifactPaths: Record<string, ArtifactPathSummary>; + /** Workspace affected-area summary, when available */ + affectedAreas?: AffectedAreasSummary; + /** Plain-language next steps for users and agents */ + nextSteps: string[]; + /** Machine-readable action constraints for agents */ + actionContext: ActionContext; /** Whether all artifacts are complete */ isComplete: boolean; /** Artifact IDs required before apply phase (from schema's apply.requires) */ @@ -116,6 +143,36 @@ export interface ChangeStatus { artifacts: ArtifactStatus[]; } +export interface ArtifactPathSummary { + outputPath: string; + resolvedOutputPath: string; + existingOutputPaths: string[]; +} + +export interface PlanningHomeSummary { + kind: 'repo' | 'workspace'; + root: string; + changesDir: string; + defaultSchema: string; + workspaceName?: string; +} + +export interface AffectedAreasSummary { + known: string[]; + unresolved: boolean; + invalid: string[]; +} + +export interface ActionContext { + mode: 'repo-local' | 'workspace-planning'; + sourceOfTruth: 'repo' | 'workspace'; + planningArtifacts: string[]; + linkedContext: Array<{ name: string }>; + allowedEditRoots: string[]; + requiresAffectedAreaSelection: boolean; + constraints: string[]; +} + /** * Loads a template from a schema's templates directory. * @@ -176,14 +233,15 @@ export function loadTemplate( export function loadChangeContext( projectRoot: string, changeName: string, - schemaName?: string + schemaName?: string, + options: LoadChangeContextOptions = {} ): ChangeContext { const changeDir = FileSystemUtils.canonicalizeExistingPath( - path.join(projectRoot, 'openspec', 'changes', changeName) + options.changeDir ?? path.join(projectRoot, 'openspec', 'changes', changeName) ); // Resolve schema: explicit > metadata > default - const resolvedSchemaName = resolveSchemaForChange(changeDir, schemaName); + const resolvedSchemaName = resolveSchemaForChange(changeDir, schemaName, projectRoot); const schema = resolveSchema(resolvedSchemaName, projectRoot); const graph = ArtifactGraph.fromSchema(schema); @@ -196,6 +254,7 @@ export function loadChangeContext( changeName, changeDir, projectRoot, + ...(options.planningHome ? { planningHome: options.planningHome } : {}), }; } @@ -268,7 +327,10 @@ export function generateInstructions( artifactId: artifact.id, schemaName: context.schemaName, changeDir: context.changeDir, + planningHome: summarizePlanningHome(context.planningHome), outputPath: artifact.generates, + resolvedOutputPath: path.join(context.changeDir, artifact.generates), + existingOutputPaths: resolveArtifactOutputs(context.changeDir, artifact.generates), description: artifact.description, instruction: artifact.instruction, context: configContext, @@ -313,6 +375,110 @@ function getUnlockedArtifacts(graph: ArtifactGraph, artifactId: string): string[ return unlocks.sort(); } +function summarizePlanningHome(planningHome: PlanningHome | undefined): PlanningHomeSummary | undefined { + if (!planningHome) { + return undefined; + } + + return { + kind: planningHome.kind, + root: planningHome.root, + changesDir: planningHome.changesDir, + defaultSchema: planningHome.defaultSchema, + ...(planningHome.workspace ? { workspaceName: planningHome.workspace.name } : {}), + }; +} + +function getWorkspaceSpecAreaSegments(context: ChangeContext): string[] { + if (context.planningHome?.kind !== 'workspace') { + return []; + } + + const specArtifact = context.graph.getArtifact('specs'); + if (!specArtifact) { + return []; + } + + return resolveArtifactOutputs(context.changeDir, specArtifact.generates) + .map((outputPath) => path.relative(path.join(context.changeDir, 'specs'), outputPath)) + .filter((relativePath) => relativePath.length > 0 && !relativePath.startsWith('..')) + .map((relativePath) => relativePath.split(path.sep)[0]) + .filter((areaName) => areaName.length > 0); +} + +function getAffectedAreasSummary(context: ChangeContext): AffectedAreasSummary | undefined { + if (context.planningHome?.kind !== 'workspace') { + return undefined; + } + + const metadata = readChangeMetadata(context.changeDir, context.projectRoot); + const known = Array.from( + new Set([...(metadata?.affected_areas ?? []), ...getWorkspaceSpecAreaSegments(context)]) + ).sort((a, b) => a.localeCompare(b)); + const validAreas = new Set(context.planningHome.workspace?.links ?? []); + const invalid = known.filter((areaName) => validAreas.size > 0 && !validAreas.has(areaName)); + + return { + known, + unresolved: known.length === 0, + invalid, + }; +} + +function buildActionContext(context: ChangeContext, artifactIds: string[]): ActionContext { + if (context.planningHome?.kind === 'workspace') { + return { + mode: 'workspace-planning', + sourceOfTruth: 'workspace', + planningArtifacts: artifactIds, + linkedContext: (context.planningHome.workspace?.links ?? []).map((name) => ({ name })), + allowedEditRoots: [], + requiresAffectedAreaSelection: true, + constraints: [ + 'Use workspace-level planning artifacts as the source of truth.', + 'Treat linked repos and folders as exploration context until an affected area is selected.', + 'Do not make implementation edits without an explicit allowed edit root.', + ], + }; + } + + return { + mode: 'repo-local', + sourceOfTruth: 'repo', + planningArtifacts: artifactIds, + linkedContext: [], + allowedEditRoots: [context.projectRoot], + requiresAffectedAreaSelection: false, + constraints: ['Repo-local change artifacts and implementation edits are scoped to this project.'], + }; +} + +function buildNextSteps( + context: ChangeContext, + artifactStatuses: ArtifactStatus[], + affectedAreas: AffectedAreasSummary | undefined +): string[] { + const readyArtifact = artifactStatuses.find((artifact) => artifact.status === 'ready'); + const steps: string[] = []; + + if (readyArtifact) { + steps.push( + `Run openspec instructions ${readyArtifact.id} --change "${context.changeName}" --json before writing that artifact.` + ); + } else if (context.graph.isComplete(context.completed)) { + steps.push('All planning artifacts are complete; review tasks before implementation.'); + } + + if (context.planningHome?.kind === 'workspace') { + if (affectedAreas?.unresolved) { + steps.push('Identify affected areas in workspace specs or coordination tasks as planning continues.'); + } + steps.push('Select an affected area and allowed edit root before implementation edits.'); + } + + return steps; +} + /** * Formats the status of all artifacts in a change. * @@ -328,7 +494,14 @@ export function formatChangeStatus(context: ChangeContext): ChangeStatus { const ready = new Set(context.graph.getNextArtifacts(context.completed)); const blocked = context.graph.getBlocked(context.completed); + const artifactPaths: Record<string, ArtifactPathSummary> = {}; const artifactStatuses: ArtifactStatus[] = artifacts.map(artifact => { + artifactPaths[artifact.id] = { + outputPath: artifact.generates, + resolvedOutputPath: path.join(context.changeDir, artifact.generates), + existingOutputPaths: resolveArtifactOutputs(context.changeDir, artifact.generates), + }; + if (context.completed.has(artifact.id)) { return { id: artifact.id, @@ -357,12 +530,19 @@ export function formatChangeStatus(context: ChangeContext): ChangeStatus { const buildOrder = context.graph.getBuildOrder(); const orderMap = new Map(buildOrder.map((id, idx) => [id, idx])); artifactStatuses.sort((a, b) => (orderMap.get(a.id) ?? 0) - (orderMap.get(b.id) ?? 0)); + const affectedAreas = getAffectedAreasSummary(context); return { changeName: context.changeName, schemaName: context.schemaName, + planningHome: summarizePlanningHome(context.planningHome), + changeRoot: context.changeDir, + artifactPaths, + affectedAreas, isComplete: context.graph.isComplete(context.completed), applyRequires, + nextSteps: buildNextSteps(context, artifactStatuses, affectedAreas), + actionContext: buildActionContext(context, artifactStatuses.map((artifact) => artifact.id)), artifacts: artifactStatuses, }; } diff --git a/src/utils/change-metadata.ts b/src/utils/change-metadata.ts index b43749582..46c66b3a4 100644 --- a/src/utils/change-metadata.ts +++ b/src/utils/change-metadata.ts @@ -161,10 +161,11 @@ export function readChangeMetadata( */ export function resolveSchemaForChange( changeDir: string, - explicitSchema?: string + explicitSchema?: string, + projectRootOverride?: string ): string { // Derive project root from changeDir (changeDir is typically projectRoot/openspec/changes/change-name) - const projectRoot = path.resolve(changeDir, '../../..'); + const projectRoot = projectRootOverride ?? path.resolve(changeDir, '../../..'); // 1. Explicit override wins if (explicitSchema) { From 3ba920f75461b1cf90e9f77db3f3a7e8ba74801e Mon Sep 17 00:00:00 2001 From: TabishB <tabishbidiwale@gmail.com> Date: Thu, 14 May 2026 02:48:32 +1000 Subject: [PATCH 07/14] Update workflow skills for planning context --- .../workspace-change-planning/tasks.md | 14 ++-- src/core/templates/workflows/apply-change.ts | 6 ++ .../templates/workflows/archive-change.ts | 36 +++++----- .../workflows/bulk-archive-change.ts | 24 ++++--- .../templates/workflows/continue-change.ts | 10 +-- src/core/templates/workflows/explore.ts | 18 +++-- src/core/templates/workflows/ff-change.ts | 14 ++-- src/core/templates/workflows/new-change.ts | 12 ++-- src/core/templates/workflows/onboard.ts | 28 ++++---- src/core/templates/workflows/propose.ts | 14 ++-- src/core/templates/workflows/sync-specs.ts | 38 ++++++++--- src/core/templates/workflows/verify-change.ts | 14 ++-- .../templates/skill-templates-parity.test.ts | 66 +++++++++---------- 13 files changed, 168 insertions(+), 126 deletions(-) diff --git a/openspec/changes/workspace-change-planning/tasks.md b/openspec/changes/workspace-change-planning/tasks.md index 790b29756..a36fe2979 100644 --- a/openspec/changes/workspace-change-planning/tasks.md +++ b/openspec/changes/workspace-change-planning/tasks.md @@ -79,13 +79,13 @@ User-testable outcome: A user can run status and instructions for repo-local and User-testable outcome: A user can inspect regenerated workflow skills and verify they are path-agnostic and tell agents to use CLI-reported artifact paths. -- [ ] 6.1 Update generated workflow skill templates to run `openspec status --change <id> --json` before artifact work and trust returned planning context. -- [ ] 6.2 Update generated workflow skill templates to run `openspec instructions <artifact> --change <id> --json` before writing artifacts and use the resolved output path. -- [ ] 6.3 Audit source workflow templates for hardcoded `openspec/changes/<name>` assumptions and replace them with CLI-reported path guidance. -- [ ] 6.4 Keep a separate artifact-context command out of this slice unless enriched status/instructions prove insufficient during implementation. -- [ ] 6.5 Manually regenerate or inspect installed workflow skills and verify they follow CLI-reported artifact paths in a workspace change. -- [ ] 6.6 Guard profile-selected workflow skills whose workspace behavior is not implemented yet so they do not fall back to repo-local paths or edit linked repos. -- [ ] 6.7 Review the agent-instruction UX: instructions are concise, path-agnostic, safe for unsupported workspace workflows, and practical for both repo-local and workspace planning. +- [x] 6.1 Update generated workflow skill templates to run `openspec status --change <id> --json` before artifact work and trust returned planning context. +- [x] 6.2 Update generated workflow skill templates to run `openspec instructions <artifact> --change <id> --json` before writing artifacts and use the resolved output path. +- [x] 6.3 Audit source workflow templates for hardcoded `openspec/changes/<name>` assumptions and replace them with CLI-reported path guidance. +- [x] 6.4 Keep a separate artifact-context command out of this slice unless enriched status/instructions prove insufficient during implementation. +- [x] 6.5 Manually regenerate or inspect installed workflow skills and verify they follow CLI-reported artifact paths in a workspace change. +- [x] 6.6 Guard profile-selected workflow skills whose workspace behavior is not implemented yet so they do not fall back to repo-local paths or edit linked repos. +- [x] 6.7 Review the agent-instruction UX: instructions are concise, path-agnostic, safe for unsupported workspace workflows, and practical for both repo-local and workspace planning. ## Phase 7: Verification diff --git a/src/core/templates/workflows/apply-change.ts b/src/core/templates/workflows/apply-change.ts index be60210a7..ec5b59ab1 100644 --- a/src/core/templates/workflows/apply-change.ts +++ b/src/core/templates/workflows/apply-change.ts @@ -31,6 +31,7 @@ export function getApplyChangeSkillTemplate(): SkillTemplate { \`\`\` Parse the JSON to understand: - \`schemaName\`: The workflow being used (e.g., "spec-driven") + - \`planningHome\`, \`changeRoot\`, and \`actionContext\`: planning scope and edit constraints - Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others) 3. **Get apply instructions** @@ -50,6 +51,8 @@ export function getApplyChangeSkillTemplate(): SkillTemplate { - If \`state: "all_done"\`: congratulate, suggest archive - Otherwise: proceed to implementation + **Workspace guard:** If status JSON reports \`actionContext.mode: "workspace-planning"\` and \`allowedEditRoots\` is empty, explain that full workspace apply is not supported in this slice. Treat linked repos and folders as read-only context, ask the user to select an affected area through an explicit implementation workflow, and STOP before editing files. + 4. **Read context files** Read every file path listed under \`contextFiles\` from the apply instructions output. @@ -188,6 +191,7 @@ export function getOpsxApplyCommandTemplate(): CommandTemplate { \`\`\` Parse the JSON to understand: - \`schemaName\`: The workflow being used (e.g., "spec-driven") + - \`planningHome\`, \`changeRoot\`, and \`actionContext\`: planning scope and edit constraints - Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others) 3. **Get apply instructions** @@ -207,6 +211,8 @@ export function getOpsxApplyCommandTemplate(): CommandTemplate { - If \`state: "all_done"\`: congratulate, suggest archive - Otherwise: proceed to implementation + **Workspace guard:** If status JSON reports \`actionContext.mode: "workspace-planning"\` and \`allowedEditRoots\` is empty, explain that full workspace apply is not supported in this slice. Treat linked repos and folders as read-only context, ask the user to select an affected area through an explicit implementation workflow, and STOP before editing files. + 4. **Read context files** Read every file path listed under \`contextFiles\` from the apply instructions output. diff --git a/src/core/templates/workflows/archive-change.ts b/src/core/templates/workflows/archive-change.ts index 1c37ffde0..41619c2b3 100644 --- a/src/core/templates/workflows/archive-change.ts +++ b/src/core/templates/workflows/archive-change.ts @@ -31,8 +31,11 @@ export function getArchiveChangeSkillTemplate(): SkillTemplate { Parse the JSON to understand: - \`schemaName\`: The workflow being used + - \`planningHome\`, \`changeRoot\`, \`artifactPaths\`, and \`actionContext\`: path and scope context - \`artifacts\`: List of artifacts with their status (\`done\` or other) + If status reports \`actionContext.mode: "workspace-planning"\`, explain that workspace archive is not supported in this slice and STOP. Do not move workspace changes into repo-local archives or edit linked repos. + **If any artifacts are not \`done\`:** - Display warning listing incomplete artifacts - Use **AskUserQuestion tool** to confirm user wants to proceed @@ -53,7 +56,7 @@ export function getArchiveChangeSkillTemplate(): SkillTemplate { 4. **Assess delta spec sync state** - Check for delta specs at \`openspec/changes/<name>/specs/\`. If none exist, proceed without sync prompt. + Use \`artifactPaths.specs.existingOutputPaths\` from status JSON to check for delta specs. If none exist, proceed without sync prompt. **If delta specs exist:** - Compare each delta spec with its corresponding main spec at \`openspec/specs/<capability>/spec.md\` @@ -68,19 +71,19 @@ export function getArchiveChangeSkillTemplate(): SkillTemplate { 5. **Perform the archive** - Create the archive directory if it doesn't exist: + Create an \`archive\` directory under \`planningHome.changesDir\` if it doesn't exist: \`\`\`bash - mkdir -p openspec/changes/archive + mkdir -p "<planningHome.changesDir>/archive" \`\`\` Generate target name using current date: \`YYYY-MM-DD-<change-name>\` **Check if target already exists:** - If yes: Fail with error, suggest renaming existing archive or using different date - - If no: Move the change directory to archive + - If no: Move \`changeRoot\` to the archive directory \`\`\`bash - mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name> + mv "<changeRoot>" "<planningHome.changesDir>/archive/YYYY-MM-DD-<name>" \`\`\` 6. **Display summary** @@ -99,7 +102,7 @@ export function getArchiveChangeSkillTemplate(): SkillTemplate { **Change:** <change-name> **Schema:** <schema-name> -**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/ +**Archived to:** the archive path derived from \`planningHome.changesDir\`/YYYY-MM-DD-<name>/ **Specs:** ✓ Synced to main specs (or "No delta specs" or "Sync skipped") All artifacts complete. All tasks complete. @@ -146,8 +149,11 @@ export function getOpsxArchiveCommandTemplate(): CommandTemplate { Parse the JSON to understand: - \`schemaName\`: The workflow being used + - \`planningHome\`, \`changeRoot\`, \`artifactPaths\`, and \`actionContext\`: path and scope context - \`artifacts\`: List of artifacts with their status (\`done\` or other) + If status reports \`actionContext.mode: "workspace-planning"\`, explain that workspace archive is not supported in this slice and STOP. Do not move workspace changes into repo-local archives or edit linked repos. + **If any artifacts are not \`done\`:** - Display warning listing incomplete artifacts - Prompt user for confirmation to continue @@ -168,7 +174,7 @@ export function getOpsxArchiveCommandTemplate(): CommandTemplate { 4. **Assess delta spec sync state** - Check for delta specs at \`openspec/changes/<name>/specs/\`. If none exist, proceed without sync prompt. + Use \`artifactPaths.specs.existingOutputPaths\` from status JSON to check for delta specs. If none exist, proceed without sync prompt. **If delta specs exist:** - Compare each delta spec with its corresponding main spec at \`openspec/specs/<capability>/spec.md\` @@ -183,19 +189,19 @@ export function getOpsxArchiveCommandTemplate(): CommandTemplate { 5. **Perform the archive** - Create the archive directory if it doesn't exist: + Create an \`archive\` directory under \`planningHome.changesDir\` if it doesn't exist: \`\`\`bash - mkdir -p openspec/changes/archive + mkdir -p "<planningHome.changesDir>/archive" \`\`\` Generate target name using current date: \`YYYY-MM-DD-<change-name>\` **Check if target already exists:** - If yes: Fail with error, suggest renaming existing archive or using different date - - If no: Move the change directory to archive + - If no: Move \`changeRoot\` to the archive directory \`\`\`bash - mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name> + mv "<changeRoot>" "<planningHome.changesDir>/archive/YYYY-MM-DD-<name>" \`\`\` 6. **Display summary** @@ -214,7 +220,7 @@ export function getOpsxArchiveCommandTemplate(): CommandTemplate { **Change:** <change-name> **Schema:** <schema-name> -**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/ +**Archived to:** the archive path derived from \`planningHome.changesDir\`/YYYY-MM-DD-<name>/ **Specs:** ✓ Synced to main specs All artifacts complete. All tasks complete. @@ -227,7 +233,7 @@ All artifacts complete. All tasks complete. **Change:** <change-name> **Schema:** <schema-name> -**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/ +**Archived to:** the archive path derived from \`planningHome.changesDir\`/YYYY-MM-DD-<name>/ **Specs:** No delta specs All artifacts complete. All tasks complete. @@ -240,7 +246,7 @@ All artifacts complete. All tasks complete. **Change:** <change-name> **Schema:** <schema-name> -**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/ +**Archived to:** the archive path derived from \`planningHome.changesDir\`/YYYY-MM-DD-<name>/ **Specs:** Sync skipped (user chose to skip) **Warnings:** @@ -257,7 +263,7 @@ Review the archive if this was not intentional. ## Archive Failed **Change:** <change-name> -**Target:** openspec/changes/archive/YYYY-MM-DD-<name>/ +**Target:** the archive path derived from \`planningHome.changesDir\`/YYYY-MM-DD-<name>/ Target archive directory already exists. diff --git a/src/core/templates/workflows/bulk-archive-change.ts b/src/core/templates/workflows/bulk-archive-change.ts index ed6d14452..647b75e1b 100644 --- a/src/core/templates/workflows/bulk-archive-change.ts +++ b/src/core/templates/workflows/bulk-archive-change.ts @@ -38,14 +38,16 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig For each selected change, collect: a. **Artifact status** - Run \`openspec status --change "<name>" --json\` - - Parse \`schemaName\` and \`artifacts\` list + - Parse \`schemaName\`, \`artifacts\`, \`planningHome\`, \`changeRoot\`, \`artifactPaths\`, and \`actionContext\` - Note which artifacts are \`done\` vs other states - b. **Task completion** - Read \`openspec/changes/<name>/tasks.md\` + If any selected change reports \`actionContext.mode: "workspace-planning"\`, explain that workspace bulk archive is not supported in this slice and STOP before syncing specs or moving changes. Do not fall back to repo-local paths or edit linked repos. + + b. **Task completion** - Read \`artifactPaths.tasks.existingOutputPaths\` from status JSON - Count \`- [ ]\` (incomplete) vs \`- [x]\` (complete) - If no tasks file exists, note as "No tasks" - c. **Delta specs** - Check \`openspec/changes/<name>/specs/\` directory + c. **Delta specs** - Check \`artifactPaths.specs.existingOutputPaths\` from status JSON - List which capability specs exist - For each, extract requirement names (lines matching \`### Requirement: <name>\`) @@ -128,8 +130,8 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig b. **Perform the archive**: \`\`\`bash - mkdir -p openspec/changes/archive - mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name> + mkdir -p "<planningHome.changesDir>/archive" + mv "<changeRoot>" "<planningHome.changesDir>/archive/YYYY-MM-DD-<name>" \`\`\` c. **Track outcome** for each change: @@ -285,14 +287,16 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig For each selected change, collect: a. **Artifact status** - Run \`openspec status --change "<name>" --json\` - - Parse \`schemaName\` and \`artifacts\` list + - Parse \`schemaName\`, \`artifacts\`, \`planningHome\`, \`changeRoot\`, \`artifactPaths\`, and \`actionContext\` - Note which artifacts are \`done\` vs other states - b. **Task completion** - Read \`openspec/changes/<name>/tasks.md\` + If any selected change reports \`actionContext.mode: "workspace-planning"\`, explain that workspace bulk archive is not supported in this slice and STOP before syncing specs or moving changes. Do not fall back to repo-local paths or edit linked repos. + + b. **Task completion** - Read \`artifactPaths.tasks.existingOutputPaths\` from status JSON - Count \`- [ ]\` (incomplete) vs \`- [x]\` (complete) - If no tasks file exists, note as "No tasks" - c. **Delta specs** - Check \`openspec/changes/<name>/specs/\` directory + c. **Delta specs** - Check \`artifactPaths.specs.existingOutputPaths\` from status JSON - List which capability specs exist - For each, extract requirement names (lines matching \`### Requirement: <name>\`) @@ -375,8 +379,8 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig b. **Perform the archive**: \`\`\`bash - mkdir -p openspec/changes/archive - mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name> + mkdir -p "<planningHome.changesDir>/archive" + mv "<changeRoot>" "<planningHome.changesDir>/archive/YYYY-MM-DD-<name>" \`\`\` c. **Track outcome** for each change: diff --git a/src/core/templates/workflows/continue-change.ts b/src/core/templates/workflows/continue-change.ts index 4b2176728..8fbe4c940 100644 --- a/src/core/templates/workflows/continue-change.ts +++ b/src/core/templates/workflows/continue-change.ts @@ -38,6 +38,7 @@ export function getContinueChangeSkillTemplate(): SkillTemplate { - \`schemaName\`: The workflow schema being used (e.g., "spec-driven") - \`artifacts\`: Array of artifacts with their status ("done", "ready", "blocked") - \`isComplete\`: Boolean indicating if all artifacts are complete + - \`planningHome\`, \`changeRoot\`, \`artifactPaths\`, and \`actionContext\`: path and scope context. Use these instead of assuming repo-local paths. 3. **Act based on status**: @@ -62,13 +63,13 @@ export function getContinueChangeSkillTemplate(): SkillTemplate { - \`rules\`: Artifact-specific rules (constraints for you - do NOT include in output) - \`template\`: The structure to use for your output file - \`instruction\`: Schema-specific guidance - - \`outputPath\`: Where to write the artifact + - \`resolvedOutputPath\`: Resolved path or pattern to write the artifact - \`dependencies\`: Completed artifacts to read for context - **Create the artifact file**: - Read any completed dependency files for context - Use \`template\` as the structure - fill in its sections - Apply \`context\` and \`rules\` as constraints when writing - but do NOT copy them into the file - - Write to the output path specified in instructions + - Write to the \`resolvedOutputPath\` specified in instructions. If it is a glob pattern, choose the concrete file path using the schema instruction and workspace planning context - Show what was created and what's now unlocked - STOP after creating ONE artifact @@ -157,6 +158,7 @@ export function getOpsxContinueCommandTemplate(): CommandTemplate { - \`schemaName\`: The workflow schema being used (e.g., "spec-driven") - \`artifacts\`: Array of artifacts with their status ("done", "ready", "blocked") - \`isComplete\`: Boolean indicating if all artifacts are complete + - \`planningHome\`, \`changeRoot\`, \`artifactPaths\`, and \`actionContext\`: path and scope context. Use these instead of assuming repo-local paths. 3. **Act based on status**: @@ -181,13 +183,13 @@ export function getOpsxContinueCommandTemplate(): CommandTemplate { - \`rules\`: Artifact-specific rules (constraints for you - do NOT include in output) - \`template\`: The structure to use for your output file - \`instruction\`: Schema-specific guidance - - \`outputPath\`: Where to write the artifact + - \`resolvedOutputPath\`: Resolved path or pattern to write the artifact - \`dependencies\`: Completed artifacts to read for context - **Create the artifact file**: - Read any completed dependency files for context - Use \`template\` as the structure - fill in its sections - Apply \`context\` and \`rules\` as constraints when writing - but do NOT copy them into the file - - Write to the output path specified in instructions + - Write to the \`resolvedOutputPath\` specified in instructions. If it is a glob pattern, choose the concrete file path using the schema instruction and workspace planning context - Show what was created and what's now unlocked - STOP after creating ONE artifact diff --git a/src/core/templates/workflows/explore.ts b/src/core/templates/workflows/explore.ts index 76db8ff8f..4b574bbf0 100644 --- a/src/core/templates/workflows/explore.ts +++ b/src/core/templates/workflows/explore.ts @@ -103,11 +103,10 @@ Think freely. When insights crystallize, you might offer: If the user mentions a change or you detect one is relevant: -1. **Read existing artifacts for context** - - \`openspec/changes/<name>/proposal.md\` - - \`openspec/changes/<name>/design.md\` - - \`openspec/changes/<name>/tasks.md\` - - etc. +1. **Resolve and read existing artifacts for context** + - Run \`openspec status --change "<name>" --json\`. + - Use \`changeRoot\`, \`artifactPaths\`, and \`actionContext\` from the status JSON. + - Read existing files from \`artifactPaths.<artifact>.existingOutputPaths\`. 2. **Reference them naturally in conversation** - "Your design mentions using Redis, but we just realized SQLite fits better..." @@ -401,11 +400,10 @@ Think freely. When insights crystallize, you might offer: If the user mentions a change or you detect one is relevant: -1. **Read existing artifacts for context** - - \`openspec/changes/<name>/proposal.md\` - - \`openspec/changes/<name>/design.md\` - - \`openspec/changes/<name>/tasks.md\` - - etc. +1. **Resolve and read existing artifacts for context** + - Run \`openspec status --change "<name>" --json\`. + - Use \`changeRoot\`, \`artifactPaths\`, and \`actionContext\` from the status JSON. + - Read existing files from \`artifactPaths.<artifact>.existingOutputPaths\`. 2. **Reference them naturally in conversation** - "Your design mentions using Redis, but we just realized SQLite fits better..." diff --git a/src/core/templates/workflows/ff-change.ts b/src/core/templates/workflows/ff-change.ts index 9e02983be..63b590efc 100644 --- a/src/core/templates/workflows/ff-change.ts +++ b/src/core/templates/workflows/ff-change.ts @@ -29,7 +29,7 @@ export function getFfChangeSkillTemplate(): SkillTemplate { \`\`\`bash openspec new change "<name>" \`\`\` - This creates a scaffolded change at \`openspec/changes/<name>/\`. + This creates a scaffolded change in the planning home resolved by the CLI. 3. **Get the artifact build order** \`\`\`bash @@ -38,6 +38,7 @@ export function getFfChangeSkillTemplate(): SkillTemplate { Parse the JSON to get: - \`applyRequires\`: array of artifact IDs needed before implementation (e.g., \`["tasks"]\`) - \`artifacts\`: list of all artifacts with their status and dependencies + - \`planningHome\`, \`changeRoot\`, \`artifactPaths\`, and \`actionContext\`: path and scope context. Use these instead of assuming repo-local paths. 4. **Create artifacts in sequence until apply-ready** @@ -55,10 +56,10 @@ export function getFfChangeSkillTemplate(): SkillTemplate { - \`rules\`: Artifact-specific rules (constraints for you - do NOT include in output) - \`template\`: The structure to use for your output file - \`instruction\`: Schema-specific guidance for this artifact type - - \`outputPath\`: Where to write the artifact + - \`resolvedOutputPath\`: Resolved path or pattern to write the artifact - \`dependencies\`: Completed artifacts to read for context - Read any completed dependency files for context - - Create the artifact file using \`template\` as the structure + - Create the artifact file using \`template\` as the structure and write it to \`resolvedOutputPath\` - Apply \`context\` and \`rules\` as constraints - but do NOT copy them into the file - Show brief progress: "✓ Created <artifact-id>" @@ -131,7 +132,7 @@ export function getOpsxFfCommandTemplate(): CommandTemplate { \`\`\`bash openspec new change "<name>" \`\`\` - This creates a scaffolded change at \`openspec/changes/<name>/\`. + This creates a scaffolded change in the planning home resolved by the CLI. 3. **Get the artifact build order** \`\`\`bash @@ -140,6 +141,7 @@ export function getOpsxFfCommandTemplate(): CommandTemplate { Parse the JSON to get: - \`applyRequires\`: array of artifact IDs needed before implementation (e.g., \`["tasks"]\`) - \`artifacts\`: list of all artifacts with their status and dependencies + - \`planningHome\`, \`changeRoot\`, \`artifactPaths\`, and \`actionContext\`: path and scope context. Use these instead of assuming repo-local paths. 4. **Create artifacts in sequence until apply-ready** @@ -157,10 +159,10 @@ export function getOpsxFfCommandTemplate(): CommandTemplate { - \`rules\`: Artifact-specific rules (constraints for you - do NOT include in output) - \`template\`: The structure to use for your output file - \`instruction\`: Schema-specific guidance for this artifact type - - \`outputPath\`: Where to write the artifact + - \`resolvedOutputPath\`: Resolved path or pattern to write the artifact - \`dependencies\`: Completed artifacts to read for context - Read any completed dependency files for context - - Create the artifact file using \`template\` as the structure + - Create the artifact file using \`template\` as the structure and write it to \`resolvedOutputPath\` - Apply \`context\` and \`rules\` as constraints - but do NOT copy them into the file - Show brief progress: "✓ Created <artifact-id>" diff --git a/src/core/templates/workflows/new-change.ts b/src/core/templates/workflows/new-change.ts index 10017422f..7f68a291f 100644 --- a/src/core/templates/workflows/new-change.ts +++ b/src/core/templates/workflows/new-change.ts @@ -40,13 +40,13 @@ export function getNewChangeSkillTemplate(): SkillTemplate { openspec new change "<name>" \`\`\` Add \`--schema <name>\` only if the user requested a specific workflow. - This creates a scaffolded change at \`openspec/changes/<name>/\` with the selected schema. + This creates a scaffolded change in the planning home resolved by the CLI. 4. **Show the artifact status** \`\`\`bash - openspec status --change "<name>" + openspec status --change "<name>" --json \`\`\` - This shows which artifacts need to be created and which are ready (dependencies satisfied). + Use the returned \`planningHome\`, \`changeRoot\`, \`artifactPaths\`, and \`nextSteps\` instead of assuming repo-local paths. 5. **Get instructions for the first artifact** The first artifact depends on the schema (e.g., \`proposal\` for spec-driven). @@ -115,13 +115,13 @@ export function getOpsxNewCommandTemplate(): CommandTemplate { openspec new change "<name>" \`\`\` Add \`--schema <name>\` only if the user requested a specific workflow. - This creates a scaffolded change at \`openspec/changes/<name>/\` with the selected schema. + This creates a scaffolded change in the planning home resolved by the CLI. 4. **Show the artifact status** \`\`\`bash - openspec status --change "<name>" + openspec status --change "<name>" --json \`\`\` - This shows which artifacts need to be created and which are ready (dependencies satisfied). + Use the returned \`planningHome\`, \`changeRoot\`, \`artifactPaths\`, and \`nextSteps\` instead of assuming repo-local paths. 5. **Get instructions for the first artifact** The first artifact depends on the schema. Check the status output to find the first artifact with status "ready". diff --git a/src/core/templates/workflows/onboard.ts b/src/core/templates/workflows/onboard.ts index 65218e165..4690d9d2c 100644 --- a/src/core/templates/workflows/onboard.ts +++ b/src/core/templates/workflows/onboard.ts @@ -176,7 +176,7 @@ Now let's create a change to hold our work. \`\`\` ## Creating a Change -A "change" in OpenSpec is a container for all the thinking and planning around a piece of work. It lives in \`openspec/changes/<name>/\` and holds your artifacts—proposal, specs, design, tasks. +A "change" in OpenSpec is a container for all the thinking and planning around a piece of work. It lives at the \`changeRoot\` reported by \`openspec status --change "<name>" --json\` and holds your artifacts—proposal, specs, design, tasks. Let me create one for our task. \`\`\` @@ -188,11 +188,11 @@ openspec new change "<derived-name>" **SHOW:** \`\`\` -Created: \`openspec/changes/<name>/\` +Created: <changeRoot from status JSON> The folder structure: \`\`\` -openspec/changes/<name>/ +<changeRoot>/ ├── proposal.md ← Why we're doing this (empty, we'll fill it) ├── design.md ← How we'll build it (empty) ├── specs/ ← Detailed requirements (empty) @@ -254,7 +254,7 @@ After approval, save the proposal: \`\`\`bash openspec instructions proposal --change "<name>" --json \`\`\` -Then write the content to \`openspec/changes/<name>/proposal.md\`. +Then write the content to the \`resolvedOutputPath\` from \`openspec instructions proposal --change "<name>" --json\`. \`\`\` Proposal saved. This is your "why" document—you can always come back and refine it as understanding evolves. @@ -275,12 +275,10 @@ Specs define **what** we're building in precise, testable terms. They use a requ For a small task like this, we might only need one spec file. \`\`\` -**DO:** Create the spec file: +**DO:** Resolve where the spec file should be created: \`\`\`bash -# Unix/macOS -mkdir -p openspec/changes/<name>/specs/<capability-name> -# Windows (PowerShell) -# New-Item -ItemType Directory -Force -Path "openspec/changes/<name>/specs/<capability-name>" +openspec instructions specs --change "<name>" --json +# Use resolvedOutputPath from the JSON. If it is a glob, choose the concrete file path using the schema instruction and workspace planning context. \`\`\` Draft the spec content: @@ -307,7 +305,7 @@ Here's the spec: This format—WHEN/THEN/AND—makes requirements testable. You can literally read them as test cases. \`\`\` -Save to \`openspec/changes/<name>/specs/<capability>/spec.md\`. +Save to the concrete file path chosen from \`resolvedOutputPath\`. --- @@ -352,7 +350,7 @@ Here's the design: For a small task, this captures the key decisions without over-engineering. \`\`\` -Save to \`openspec/changes/<name>/design.md\`. +Save to the \`resolvedOutputPath\` from \`openspec instructions design --change "<name>" --json\`. --- @@ -390,7 +388,7 @@ Each checkbox becomes a unit of work in the apply phase. Ready to implement? **PAUSE** - Wait for user to confirm they're ready to implement. -Save to \`openspec/changes/<name>/tasks.md\`. +Save to the \`resolvedOutputPath\` from \`openspec instructions tasks --change "<name>" --json\`. --- @@ -434,7 +432,7 @@ The change is implemented! One more step—let's archive it. \`\`\` ## Archiving -When a change is complete, we archive it. This moves it from \`openspec/changes/\` to \`openspec/changes/archive/YYYY-MM-DD-<name>/\`. +When a change is complete, we archive it. The archive path is derived from \`planningHome.changesDir\` and the date. Archived changes become your project's decision history—you can always find them later to understand why something was built a certain way. \`\`\` @@ -446,7 +444,7 @@ openspec archive "<name>" **SHOW:** \`\`\` -Archived to: \`openspec/changes/archive/YYYY-MM-DD-<name>/\` +Archived to: \`<planningHome.changesDir>/archive/YYYY-MM-DD-<name>/\` The change is now part of your project's history. The code is in your codebase, the decision record is preserved. \`\`\` @@ -509,7 +507,7 @@ Try \`/opsx:propose\` on something you actually want to build. You've got the rh If the user says they need to stop, want to pause, or seem disengaged: \`\`\` -No problem! Your change is saved at \`openspec/changes/<name>/\`. +No problem! Your change is saved at the \`changeRoot\` reported by \`openspec status --change "<name>" --json\`. To pick up where we left off later: - \`/opsx:continue <name>\` - Resume artifact creation diff --git a/src/core/templates/workflows/propose.ts b/src/core/templates/workflows/propose.ts index 74a9ce2d0..c288cf8d0 100644 --- a/src/core/templates/workflows/propose.ts +++ b/src/core/templates/workflows/propose.ts @@ -38,7 +38,7 @@ When ready to implement, run /opsx:apply \`\`\`bash openspec new change "<name>" \`\`\` - This creates a scaffolded change at \`openspec/changes/<name>/\` with \`.openspec.yaml\`. + This creates a scaffolded change in the planning home resolved by the CLI with \`.openspec.yaml\`. 3. **Get the artifact build order** \`\`\`bash @@ -47,6 +47,7 @@ When ready to implement, run /opsx:apply Parse the JSON to get: - \`applyRequires\`: array of artifact IDs needed before implementation (e.g., \`["tasks"]\`) - \`artifacts\`: list of all artifacts with their status and dependencies + - \`planningHome\`, \`changeRoot\`, \`artifactPaths\`, and \`actionContext\`: path and scope context. Use these instead of assuming repo-local paths. 4. **Create artifacts in sequence until apply-ready** @@ -64,10 +65,10 @@ When ready to implement, run /opsx:apply - \`rules\`: Artifact-specific rules (constraints for you - do NOT include in output) - \`template\`: The structure to use for your output file - \`instruction\`: Schema-specific guidance for this artifact type - - \`outputPath\`: Where to write the artifact + - \`resolvedOutputPath\`: Resolved path or pattern to write the artifact - \`dependencies\`: Completed artifacts to read for context - Read any completed dependency files for context - - Create the artifact file using \`template\` as the structure + - Create the artifact file using \`template\` as the structure and write it to \`resolvedOutputPath\` - Apply \`context\` and \`rules\` as constraints - but do NOT copy them into the file - Show brief progress: "Created <artifact-id>" @@ -149,7 +150,7 @@ When ready to implement, run /opsx:apply \`\`\`bash openspec new change "<name>" \`\`\` - This creates a scaffolded change at \`openspec/changes/<name>/\` with \`.openspec.yaml\`. + This creates a scaffolded change in the planning home resolved by the CLI with \`.openspec.yaml\`. 3. **Get the artifact build order** \`\`\`bash @@ -158,6 +159,7 @@ When ready to implement, run /opsx:apply Parse the JSON to get: - \`applyRequires\`: array of artifact IDs needed before implementation (e.g., \`["tasks"]\`) - \`artifacts\`: list of all artifacts with their status and dependencies + - \`planningHome\`, \`changeRoot\`, \`artifactPaths\`, and \`actionContext\`: path and scope context. Use these instead of assuming repo-local paths. 4. **Create artifacts in sequence until apply-ready** @@ -175,10 +177,10 @@ When ready to implement, run /opsx:apply - \`rules\`: Artifact-specific rules (constraints for you - do NOT include in output) - \`template\`: The structure to use for your output file - \`instruction\`: Schema-specific guidance for this artifact type - - \`outputPath\`: Where to write the artifact + - \`resolvedOutputPath\`: Resolved path or pattern to write the artifact - \`dependencies\`: Completed artifacts to read for context - Read any completed dependency files for context - - Create the artifact file using \`template\` as the structure + - Create the artifact file using \`template\` as the structure and write it to \`resolvedOutputPath\` - Apply \`context\` and \`rules\` as constraints - but do NOT copy them into the file - Show brief progress: "Created <artifact-id>" diff --git a/src/core/templates/workflows/sync-specs.ts b/src/core/templates/workflows/sync-specs.ts index 34da4276e..bbdb2c5e6 100644 --- a/src/core/templates/workflows/sync-specs.ts +++ b/src/core/templates/workflows/sync-specs.ts @@ -26,9 +26,18 @@ This is an **agent-driven** operation - you will read delta specs and directly e **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose. -2. **Find delta specs** +2. **Resolve change context** - Look for delta spec files in \`openspec/changes/<name>/specs/*/spec.md\`. + Run: + \`\`\`bash + openspec status --change "<name>" --json + \`\`\` + + If status reports \`actionContext.mode: "workspace-planning"\`, explain that workspace spec sync is not supported in this slice and STOP. Do not fall back to repo-local paths or edit linked repos. + +3. **Find delta specs** + + Use \`artifactPaths.specs.existingOutputPaths\` from the status JSON as the list of delta spec files. Each delta spec file contains sections like: - \`## ADDED Requirements\` - New requirements to add @@ -38,9 +47,9 @@ This is an **agent-driven** operation - you will read delta specs and directly e If no delta specs found, inform user and stop. -3. **For each delta spec, apply changes to main specs** +4. **For each delta spec, apply changes to main specs** - For each capability with a delta spec at \`openspec/changes/<name>/specs/<capability>/spec.md\`: + For each repo-local capability delta spec path returned by the CLI: a. **Read the delta spec** to understand the intended changes @@ -71,7 +80,7 @@ This is an **agent-driven** operation - you will read delta specs and directly e - Add Purpose section (can be brief, mark as TBD) - Add Requirements section with the ADDED requirements -4. **Show summary** +5. **Show summary** After applying all changes, summarize: - Which capabilities were updated @@ -165,9 +174,18 @@ This is an **agent-driven** operation - you will read delta specs and directly e **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose. -2. **Find delta specs** +2. **Resolve change context** + + Run: + \`\`\`bash + openspec status --change "<name>" --json + \`\`\` + + If status reports \`actionContext.mode: "workspace-planning"\`, explain that workspace spec sync is not supported in this slice and STOP. Do not fall back to repo-local paths or edit linked repos. + +3. **Find delta specs** - Look for delta spec files in \`openspec/changes/<name>/specs/*/spec.md\`. + Use \`artifactPaths.specs.existingOutputPaths\` from the status JSON as the list of delta spec files. Each delta spec file contains sections like: - \`## ADDED Requirements\` - New requirements to add @@ -177,9 +195,9 @@ This is an **agent-driven** operation - you will read delta specs and directly e If no delta specs found, inform user and stop. -3. **For each delta spec, apply changes to main specs** +4. **For each delta spec, apply changes to main specs** - For each capability with a delta spec at \`openspec/changes/<name>/specs/<capability>/spec.md\`: + For each repo-local capability delta spec path returned by the CLI: a. **Read the delta spec** to understand the intended changes @@ -210,7 +228,7 @@ This is an **agent-driven** operation - you will read delta specs and directly e - Add Purpose section (can be brief, mark as TBD) - Add Requirements section with the ADDED requirements -4. **Show summary** +5. **Show summary** After applying all changes, summarize: - Which capabilities were updated diff --git a/src/core/templates/workflows/verify-change.ts b/src/core/templates/workflows/verify-change.ts index fdb6b6703..a9931bc76 100644 --- a/src/core/templates/workflows/verify-change.ts +++ b/src/core/templates/workflows/verify-change.ts @@ -32,9 +32,12 @@ export function getVerifyChangeSkillTemplate(): SkillTemplate { \`\`\` Parse the JSON to understand: - \`schemaName\`: The workflow being used (e.g., "spec-driven") + - \`planningHome\`, \`changeRoot\`, \`artifactPaths\`, and \`actionContext\`: path and scope context - Which artifacts exist for this change -3. **Get the change directory and load artifacts** + If status reports \`actionContext.mode: "workspace-planning"\`, explain that full workspace implementation verification is not supported in this slice and STOP. Do not infer repo-local implementation ownership or edit linked repos. + +3. **Get planning context and load artifacts** \`\`\`bash openspec instructions apply --change "<name>" --json @@ -62,7 +65,7 @@ export function getVerifyChangeSkillTemplate(): SkillTemplate { - Recommendation: "Complete task: <description>" or "Mark as done if already implemented" **Spec Coverage**: - - If delta specs exist in \`openspec/changes/<name>/specs/\`: + - If delta specs exist in \`contextFiles.specs\`: - Extract all requirements (marked with "### Requirement:") - For each requirement: - Search codebase for keywords related to the requirement @@ -201,9 +204,12 @@ export function getOpsxVerifyCommandTemplate(): CommandTemplate { \`\`\` Parse the JSON to understand: - \`schemaName\`: The workflow being used (e.g., "spec-driven") + - \`planningHome\`, \`changeRoot\`, \`artifactPaths\`, and \`actionContext\`: path and scope context - Which artifacts exist for this change -3. **Get the change directory and load artifacts** + If status reports \`actionContext.mode: "workspace-planning"\`, explain that full workspace implementation verification is not supported in this slice and STOP. Do not infer repo-local implementation ownership or edit linked repos. + +3. **Get planning context and load artifacts** \`\`\`bash openspec instructions apply --change "<name>" --json @@ -231,7 +237,7 @@ export function getOpsxVerifyCommandTemplate(): CommandTemplate { - Recommendation: "Complete task: <description>" or "Mark as done if already implemented" **Spec Coverage**: - - If delta specs exist in \`openspec/changes/<name>/specs/\`: + - If delta specs exist in \`contextFiles.specs\`: - Extract all requirements (marked with "### Requirement:") - For each requirement: - Search codebase for keywords related to the requirement diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index 9c2f798c7..49ffe7f5b 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -30,43 +30,43 @@ import { import { generateSkillContent } from '../../../src/core/shared/skill-generation.js'; const EXPECTED_FUNCTION_HASHES: Record<string, string> = { - getExploreSkillTemplate: '3f73b4d7ab189ef6367fccc9d99308bee35c6a89dae4c8044582a01cb01b335b', - getNewChangeSkillTemplate: '5989672758eccf54e3bb554ab97f2c129a192b12bbb7688cc1ffcf6bccb1ae9d', - getContinueChangeSkillTemplate: 'f2e413f0333dfd6641cc2bd1a189273fdea5c399eecdde98ef528b5216f097b3', - getApplyChangeSkillTemplate: '6238712ba8cd2fd099c4f3bac13436f758fc6ac776fb8be19547f2b195240bfd', - getFfChangeSkillTemplate: 'a7332fb14c8dc3f9dec71f5d332790b4a8488191e7db4ab6132ccbefecf9ded9', - getSyncSpecsSkillTemplate: 'bded184e4c345619148de2c0ad80a5b527d4ffe45c87cc785889b9329e0f465b', - getOnboardSkillTemplate: 'c9e719a02d2ae7f74a0e978f9ad4e767c1921248a9e3724c3321c58a15c38ba9', - getOpsxExploreCommandTemplate: 'b421b88c7a532385f7b1404736d7893eb35a05573b4a04a96f72379ac1bbf148', - getOpsxNewCommandTemplate: '62eee32d6d81a376e7be845d0891e28e6262ad07482f9bfe6af12a9f0366c364', - getOpsxContinueCommandTemplate: '8bbaedcc95287f9e822572608137df4f49ad54cedfb08d3342d0d1c4e9716caa', - getOpsxApplyCommandTemplate: 'f59cfe9482a1b29f64b9cd7396397991a2f00a5cb1abde4ab8b4757acf1678b9', - getOpsxFfCommandTemplate: 'cdebe872cc8e0fcc25c8864b98ffd66a93484c0657db94bd1285b8113092702a', - getArchiveChangeSkillTemplate: '6f8ca383fdb5a4eb9872aca81e07bf0ba7f25e4de8617d7a047ca914ca7f14b9', - getBulkArchiveChangeSkillTemplate: '8049897ce1ddb2ff6c0d4b72e22636f9ecfd083b5f2c2a30cf3bb1cb828a2f93', - getOpsxSyncCommandTemplate: '378d035fe7cc30be3e027b66dcc4b8afc78ef1c8369c39479c9b05a582fb5ccf', - getVerifyChangeSkillTemplate: '40dde29051a0ba204295b74e49e87b6e9ff30c8b89ff0e791b4f955b4595de59', - getOpsxArchiveCommandTemplate: 'b44cc9748109f61687f9f596604b037bc3ea803abc143b22f09a76aebd98b493', - getOpsxOnboardCommandTemplate: 'fce531f952e939ee85a41848fc21e4cc720b0f3eb62737adc3a51ee6ad2dfc57', - getOpsxBulkArchiveCommandTemplate: '0d77c82de43840a28c74f5181cb21e33b9a9d00454adf4bc92bdc9e69817d6f5', - getOpsxVerifyCommandTemplate: 'd7c0444863faabb16abb091bc40ee56d985ae4bfa9a4db1e622ca8ba03c32fed', - getOpsxProposeSkillTemplate: 'd67f937d44650e9c61d2158c865309fbab23cb3f50a3d4868a640a97776e3999', - getOpsxProposeCommandTemplate: '41ad59b37eafd7a161bab5c6e41997a37368f9c90b194451295ede5cd42e4d46', + getExploreSkillTemplate: 'e2765fae6c2e960f4ce07058cfdaa547ff3435d454eacd5e924e38139e97ad52', + getNewChangeSkillTemplate: 'b0c26f0b65380062e586505c08c72230e59dccea89e6acca7b673f01cba70d5a', + getContinueChangeSkillTemplate: 'fbc6c379ed3dd39f59f52b10584b8df5b1dc08b5422bcf1c6d6255a944d22a11', + getApplyChangeSkillTemplate: 'e746f230c2513a5fd40842bde494bb3cdb3c5f7c1bcece101f92090983d4ff55', + getFfChangeSkillTemplate: '50e68fbb49b76d2690b614bffa9e6210e45539fb74419fc2e4311158b6d38485', + getSyncSpecsSkillTemplate: '9f02b41227db70875b89eefeb275c769142607dc5b2593f4e606794aed2fdbad', + getOnboardSkillTemplate: '4f4b60fea6e3fc7d2185815b2808fad51535fdd00cd4401b32d1536f32fa2b6d', + getOpsxExploreCommandTemplate: '4d5e64e3ede6703113cf2fd23b797371ef2407b702478b4f7240fc81cbf2d3a5', + getOpsxNewCommandTemplate: '757f72e2d9a1a6794b2188704fd39dd2ab65428899b4b361c76cc15a5e4f2ccc', + getOpsxContinueCommandTemplate: '62f8863edda2bfe4e210f8bc3095fd4369aaaaf7772a5cba9602d0f0bca1d0c9', + getOpsxApplyCommandTemplate: '812feefd32a4d9d468e03e456d06e3d2d08d1118d29cce4911f0be59cdd30bfc', + getOpsxFfCommandTemplate: 'f775b242bcfd56594c431c7f31a0129208a1bacfdb2427074d412543072ef7ca', + getArchiveChangeSkillTemplate: 'bdf022ae2cdef1feef4d641a068bef3a7fc5d98a323f7ce9f77ac578fe8d20c6', + getBulkArchiveChangeSkillTemplate: 'fdb1715804e86de85be96222b8efeb9d5b350c6d5c19e343e244655deff8e62b', + getOpsxSyncCommandTemplate: '4c8118afaea79ff4fed3d946c88e6a7abbba904a5fbf643e4372da1e3735a467', + getVerifyChangeSkillTemplate: '3c5dda8b49ba00f50b5bae7f04763dd00cc00a05e5f1d8a2068ad7fb701d8165', + getOpsxArchiveCommandTemplate: '5181ec2f59c9f0f3376e61d952ed4be976cbd01595b6b0d5e67466c8bd6bac6d', + getOpsxOnboardCommandTemplate: '57c1f3e2590bda8f47818bab1d528456c1b8a9a7501f63ab9e2115e0cfaf6f35', + getOpsxBulkArchiveCommandTemplate: 'b76c421023ccb5a12867c349f27cdb186234b692c1811980fb94127567bdabda', + getOpsxVerifyCommandTemplate: '9a7a3f9e5bc3d0c0878b1a4493efbbb38729597d9b9be78f63284cc2da7c20c3', + getOpsxProposeSkillTemplate: 'bae22279f8c7f711a8d5c5289551551d48197ddf5a99b695d96fff5339e08a49', + getOpsxProposeCommandTemplate: '870ab824c2aeb825fe3fe161a1f223633b4fff308ecaeb8197cbf309db2ddf02', getFeedbackSkillTemplate: 'd7d83c5f7fc2b92fe8f4588a5bf2d9cb315e4c73ec19bcd5ef28270906319a0d', }; const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record<string, string> = { - 'openspec-explore': '08e1ec9958eb04653707dd3e198c3fd69cf1b3acd3cf95a1022693cca83c60fc', - 'openspec-new-change': 'c324a7ace1f244aa3f534ac8e3370a2c11190d6d1b85a315f26a211398310f0f', - 'openspec-continue-change': '463cf0b980ec9c3c24774414ef2a3e48e9faa8577bc8748990f45ab3d5efe960', - 'openspec-apply-change': '38ad2cb645827eda555f20e1ac9d483e1d75bae4c817c0669474aaa8c12c0421', - 'openspec-ff-change': '672c3a5b8df152d959b15bd7ae2be7a75ab7b8eaa2ec1e0daa15c02479b27937', - 'openspec-sync-specs': 'b8859cf454379a19ca35dbf59eedca67306607f44a355327f9dc851114e50bde', - 'openspec-archive-change': 'f83c85452bd47de0dee6b8efbcea6a62534f8a175480e9044f3043f887cebf0f', - 'openspec-bulk-archive-change': '10477399bb07c7ba67f78e315bd68fb1901af8866720545baf4c62a6a679493b', - 'openspec-verify-change': 'b6dc1b87940be9d6125b834831c8619019aec9a9748995f72bf981b6f08b67f8', - 'openspec-onboard': 'c1444e026028210efd699110f7e9079bcb486d85ccf27f743213a81cb1084303', - 'openspec-propose': '20e36dabefb90e232bad0667292bd5007ec280f8fc4fc995dbc4282bf45a22e7', + 'openspec-explore': '28d900ef82b325beb65e69ee6435949adcfdf14a4314638e7006e6dc359b92d4', + 'openspec-new-change': 'c99989810f982d72eefc74a35f2282b71f1956f23f61b83aaa58fa3dd921716f', + 'openspec-continue-change': 'c00e2a60f79cd60197094cc59762babe5ee6a2dc1e859a0ede3f436a775ccecf', + 'openspec-apply-change': 'd849442efd925b9247651e254a5cd696945321610cca5a9432ad420430554548', + 'openspec-ff-change': '9d9b1995b6f4adb3da570676f7d11fee4cd1cf6c5df8ec83c033e02783a544df', + 'openspec-sync-specs': '2e0f67ec6fadffc6107b4b1a28eef23a99a6649e5fae706897ea1dd9deb852a8', + 'openspec-archive-change': '8d14af2c8b2e4358308ac9fc14f75db42a4b41a07e175825035852a82479793e', + 'openspec-bulk-archive-change': '16207683996b1952559cd4e33463f28fb097761f2c5d912107733d01a90d3f2f', + 'openspec-verify-change': 'a2acecd0c2b4e57080a314e5e7a093e0688293c37e446eb45d378f5050058550', + 'openspec-onboard': 'b924ea3c97543ebb7ee82c5f194afe7ce87a521c32b85616f445240ab33a02ab', + 'openspec-propose': '56aa526fe1e9fac956ad3ad570a3a259d27f54b05086940d85af136a62069292', }; function stableStringify(value: unknown): string { From 1870c82d26d929bf0ec2b682a872b58903d7e20d Mon Sep 17 00:00:00 2001 From: TabishB <tabishbidiwale@gmail.com> Date: Thu, 14 May 2026 03:04:26 +1000 Subject: [PATCH 08/14] Add workspace planning verification coverage --- docs/cli.md | 43 +++++- docs/concepts.md | 8 +- .../manual-acceptance.md | 74 ++++++++++ .../workspace-change-planning/tasks.md | 34 ++--- src/cli/index.ts | 9 +- src/commands/workspace.ts | 8 ++ src/commands/workspace/selection.ts | 8 +- src/core/completions/command-registry.ts | 2 +- src/core/planning-home.ts | 16 ++- src/core/workspace/skills.ts | 26 ++-- test/commands/artifact-workflow.test.ts | 126 ++++++++++++++++++ test/commands/workspace.test.ts | 52 ++++++++ test/core/planning-home.test.ts | 29 ++++ .../templates/skill-templates-parity.test.ts | 19 +++ test/core/workspace/skills.test.ts | 10 ++ 15 files changed, 427 insertions(+), 37 deletions(-) create mode 100644 openspec/changes/workspace-change-planning/manual-acceptance.md create mode 100644 test/core/planning-home.test.ts diff --git a/docs/cli.md b/docs/cli.md index 81753560a..f48d1bc28 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -7,7 +7,7 @@ The OpenSpec CLI (`openspec`) provides terminal commands for project setup, vali | Category | Commands | Purpose | |----------|----------|---------| | **Setup** | `init`, `update` | Initialize and update OpenSpec in your project | -| **Workspaces (beta)** | `workspace setup`, `workspace list`, `workspace ls`, `workspace link`, `workspace relink`, `workspace doctor`, `workspace open` | Set up planning across linked repos or folders | +| **Workspaces (beta)** | `workspace setup`, `workspace list`, `workspace ls`, `workspace link`, `workspace relink`, `workspace doctor`, `workspace update`, `workspace open` | Set up planning across linked repos or folders | | **Browsing** | `list`, `view`, `show` | Explore changes and specs | | **Validation** | `validate` | Check changes and specs for issues | | **Lifecycle** | `archive` | Finalize completed changes | @@ -52,6 +52,7 @@ These commands support `--json` output for programmatic use by AI agents and scr | `openspec workspace link` | Link a repo or folder | `--json` for structured link output | | `openspec workspace relink` | Repair a linked path | `--json` for structured link output | | `openspec workspace doctor` | Check one workspace | `--json` for structured status output | +| `openspec workspace update` | Refresh workspace-local agent skills | `--tools` selects agents; profile selects workflows | --- @@ -187,6 +188,7 @@ openspec workspace setup [options] | `--link <path>` | Link an existing repo or folder and infer the link name from the folder name | | `--link <name>=<path>` | Link an existing repo or folder with an explicit link name | | `--opener <id>` | Store a preferred opener during non-interactive setup: `codex`, `claude`, `github-copilot`, or `editor` | +| `--tools <tools>` | Install workspace-local OpenSpec skills for agents. Use `all`, `none`, or comma-separated tool IDs | | `--no-interactive` | Disable prompts; requires `--name` and at least one `--link` | | `--json` | Output JSON; requires `--no-interactive` | @@ -196,10 +198,13 @@ openspec workspace setup [options] openspec workspace setup openspec workspace setup --no-interactive --name platform --link /repos/api --link web=/repos/web openspec workspace setup --no-interactive --name platform --link /repos/api --opener codex +openspec workspace setup --no-interactive --name platform --link /repos/api --tools codex,claude openspec workspace setup --no-interactive --json --name checkout --link /repos/platform/apps/checkout ``` -Interactive setup asks for a preferred opener and stores it in machine-local workspace state. Non-interactive setup stores a preferred opener only when `--opener` is provided; otherwise `workspace open` prompts later in interactive terminals when a supported opener is available, or asks scripts to pass `--agent <tool>` or `--editor`. +Interactive setup asks for a preferred opener and can install workspace-local OpenSpec skills for selected agents. Non-interactive setup stores a preferred opener only when `--opener` is provided; otherwise `workspace open` prompts later in interactive terminals when a supported opener is available, or asks scripts to pass `--agent <tool>` or `--editor`. + +Workspace skill installation is skills-only in this beta slice: even if global delivery is `commands` or `both`, workspace setup writes agent skill folders in the workspace root and does not create slash command files. The active global profile chooses which workflow skills are installed; `--tools` chooses which agents receive them. If `--tools` is omitted in non-interactive setup, no skills are installed and `workspace update --tools <ids>` can add them later. ### `openspec workspace list` @@ -262,6 +267,36 @@ Commands that need one workspace use the current workspace when run from inside JSON responses use typed objects plus `status` arrays. Primary data lives in `workspace`, `workspaces`, or `link`; warnings and errors live in `status`. +### `openspec workspace update` + +Refresh workspace-local OpenSpec skills from the active global profile. + +```bash +openspec workspace update [name] [options] +``` + +**Options:** + +| Option | Description | +|--------|-------------| +| `--workspace <name>` | Select a known workspace from the local registry | +| `--tools <tools>` | Select agents for workspace skills. Use `all`, `none`, or comma-separated tool IDs | +| `--json` | Output JSON | +| `--no-interactive` | Disable workspace picker prompts | + +**Examples:** + +```bash +openspec workspace update +openspec workspace update platform +openspec workspace update --workspace platform --tools codex,claude +openspec workspace update --workspace platform --tools none +``` + +`workspace update` 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 planning home redirects to `openspec workspace update`; run `openspec update` inside repo-local projects when you want repo-owned tool files updated. + ### `openspec workspace open` Open a workspace working set through the stored preferred opener, a one-session agent override, or VS Code editor mode. @@ -958,9 +993,9 @@ openspec config profile core - Keep current settings (exit) If you keep current settings, no changes are written and no update prompt is shown. -If there are no config changes but the current project files are out of sync with your global profile/delivery, OpenSpec will show a warning and suggest running `openspec update`. +If there are no config changes but the current project or workspace files are out of sync with your global profile/delivery, OpenSpec will show a warning and suggest `openspec update` for repo-local projects or `openspec workspace update` for workspace-local skills. Pressing `Ctrl+C` also cancels the flow cleanly (no stack trace) and exits with code `130`. -In the workflow checklist, `[x]` means the workflow is selected in global config. To apply those selections to project files, run `openspec update` (or choose `Apply changes to this project now?` when prompted inside a project). +In the workflow checklist, `[x]` means the workflow is selected in global config. To apply those selections to project files, run `openspec update` (or choose `Apply changes to this project now?` when prompted inside a project). From inside a workspace, use `openspec workspace update` to refresh workspace-local skills; this remains skills-only and does not generate workspace slash commands. **Interactive examples:** diff --git a/docs/concepts.md b/docs/concepts.md index a923b6a8e..490e964a4 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -160,13 +160,19 @@ openspec workspace relink api-service /new/path/to/api openspec workspace doctor openspec workspace doctor --workspace platform +# Refresh workspace-local agent skills from the active global profile +openspec workspace update +openspec workspace update --workspace platform --tools codex,claude + # Open the linked working set openspec workspace open openspec workspace open platform --agent github-copilot openspec workspace open --editor ``` -`workspace setup` always creates the workspace in the standard workspace location, records it in the local registry, shows the workspace location, and requires at least one linked repo or folder. Interactive setup asks for a preferred opener. Non-interactive setup stores one only when `--opener codex`, `--opener claude`, `--opener github-copilot`, or `--opener editor` is provided. +`workspace setup` always creates the workspace in the standard workspace location, records it in the local registry, shows the workspace location, and requires at least one linked repo or folder. Interactive setup asks for a preferred opener and can install OpenSpec skills for selected agents. Non-interactive setup stores one only when `--opener codex`, `--opener claude`, `--opener github-copilot`, or `--opener editor` is provided. + +Workspace skills are installed only in the workspace root. The active global profile selects which workflow skills are generated; `--tools` selects which agents receive them. Workspace setup and update are skills-only in this beta slice, so they do not create slash command files even when global delivery includes commands. Run `openspec workspace update` after changing the global profile to refresh, add, or remove managed workspace-local skill directories without editing linked repos or folders. OpenSpec also maintains root workspace open files: an OpenSpec-managed guidance block in `AGENTS.md`, a machine-local `<workspace-name>.code-workspace` file for VS Code and GitHub Copilot-in-VS-Code opens, and a specific ignore entry for that maintained `.code-workspace` file. User-authored `*.code-workspace` files remain trackable because the ignore rule targets only the maintained file. diff --git a/openspec/changes/workspace-change-planning/manual-acceptance.md b/openspec/changes/workspace-change-planning/manual-acceptance.md new file mode 100644 index 000000000..45340d2ef --- /dev/null +++ b/openspec/changes/workspace-change-planning/manual-acceptance.md @@ -0,0 +1,74 @@ +# Manual Acceptance Evidence + +Change: `workspace-change-planning` +Date: 2026-05-14 + +## Automated Verification + +Expected: targeted coverage exercises workspace setup, update, config profile apply, workspace change creation, workspace planning context, guarded workflow skills, cross-platform path helpers, docs/completion coverage, and strict OpenSpec validation. + +Actual: + +```bash +pnpm run build +pnpm vitest run test/commands/workspace.test.ts test/commands/artifact-workflow.test.ts test/core/workspace/skills.test.ts test/core/planning-home.test.ts test/core/templates/skill-templates-parity.test.ts +node dist/cli/index.js validate workspace-change-planning --strict +git diff --check +``` + +Observed: +- Build completed successfully. +- Targeted tests passed: workspace command, artifact workflow, workspace skill helpers, planning-home paths, and workflow skill template parity. +- Strict validation reported `Change 'workspace-change-planning' is valid`. +- `git diff --check` reported no whitespace errors. + +## Clean Workspace Rerun + +Expected: from a clean temporary workspace, setup records links, installs skills only in the workspace root, profile changes can be applied to workspace-local skills, workspace change creation stays in the coordination root, status/instructions expose workspace planning context, linked repos remain untouched, and unsupported workflows stop rather than falling back to repo-local edits. + +Actual commands: + +```bash +tmp=$(mktemp -d) +mkdir -p "$tmp/config/openspec" "$tmp/data" "$tmp/api" "$tmp/web" +# write global config: custom profile, commands delivery, workflows ["apply"] +XDG_CONFIG_HOME="$tmp/config" XDG_DATA_HOME="$tmp/data" node dist/cli/index.js workspace setup --no-interactive --json --name final-manual --link api="$tmp/api" --link web="$tmp/web" --tools codex +cd "$workspace" && node dist/cli/index.js workspace doctor --json +cd "$workspace" && node dist/cli/index.js config profile core +cd "$workspace/changes" && node dist/cli/index.js update +cd "$workspace" && node dist/cli/index.js new change cross-workspace-login --goal "Unify login across API and web" --areas api,web +mkdir -p "$workspace/changes/cross-workspace-login/specs/api/login" +# write specs/api/login/spec.md +cd "$workspace" && node dist/cli/index.js status --change cross-workspace-login --json +cd "$workspace" && node dist/cli/index.js instructions specs --change cross-workspace-login --json +find "$tmp/api" "$tmp/web" -maxdepth 3 -print | sort +rg -n 'actionContext.mode: "workspace-planning"|resolvedOutputPath|Do not fall back to repo-local paths|openspec/changes/<name>' "$workspace/.codex/skills" +``` + +Observed: +- Setup JSON reported `profile: custom`, `delivery: commands`, `workflow_ids: ["apply"]`, `selected_agents: ["codex"]`, `skills_only: true`, and the skills-only delivery notice. +- `workspace doctor --json` showed registered links `api` and `web` before any change was created. +- `config profile core` from the workspace printed `Config updated. Run \`openspec workspace update\` to apply it to workspace-local skills.` +- Running `openspec update` from the workspace `changes/` directory redirected to workspace update and refreshed Codex to the core workflows without a false registry warning. +- `new change cross-workspace-login --goal ... --areas api,web` created a workspace change at `changes/cross-workspace-login/` with schema `workspace-planning`. +- Status JSON reported: + - `schemaName: workspace-planning` + - `planningHome.kind: workspace` + - `affectedAreas.known: ["api", "web"]` + - `actionContext.mode: workspace-planning` + - `actionContext.allowedEditRoots: []` + - `artifactPaths.specs.existingOutputPaths` preserved `specs/api/login/spec.md`. +- Instructions JSON for `specs` reported `resolvedOutputPath` as `changes/cross-workspace-login/specs/**/*.md` and preserved the nested existing spec path. +- `find "$tmp/api" "$tmp/web"` showed only the linked folder roots, with no repo-local OpenSpec artifacts created. +- Generated workflow skills contained `actionContext.mode: "workspace-planning"`, `resolvedOutputPath`, and explicit "do not fall back to repo-local paths" guardrails where relevant; no generated skill contained `openspec/changes/<name>`. + +## UX Review + +Expected: command wording should make the project/workspace distinction clear, tell users which command applies profile changes, and avoid implying linked repos are edited during workspace planning. + +Actual: +- Setup/update output says workspace setup installs skills only and workspace command generation is not part of this slice. +- Workspace change creation says "workspace change", prints affected areas, and points to `status` for planning artifacts. +- Status/instructions JSON now provide enough path and action context that a separate artifact-context command is not needed. +- Unsupported workspace apply, sync, archive, bulk archive, and verify workflows are intentionally guarded. They stop and ask for an explicit affected-area implementation workflow instead of editing linked repos. +- Fresh-agent rerun was not available in this session; as fallback, the clean temporary workspace rerun above was performed after the final path and registry fixes. diff --git a/openspec/changes/workspace-change-planning/tasks.md b/openspec/changes/workspace-change-planning/tasks.md index a36fe2979..0d607c2c3 100644 --- a/openspec/changes/workspace-change-planning/tasks.md +++ b/openspec/changes/workspace-change-planning/tasks.md @@ -91,20 +91,20 @@ User-testable outcome: A user can inspect regenerated workflow skills and verify User-testable outcome: A user or reviewer can run the full manual checklist from a clean workspace and compare expected versus actual evidence for every earlier phase. -- [ ] 7.1 Add tests that workspace setup installs skills in the workspace root and leaves linked repos unchanged. -- [ ] 7.2 Add tests that workspace update refreshes, adds, and removes only managed workspace skill directories. -- [ ] 7.3 Add tests that workspace setup/update use the current global profile for workflow skill selection while keeping workspace delivery skills-only. -- [ ] 7.4 Add tests that `openspec config profile` inside a workspace can apply changes through `openspec workspace update`. -- [ ] 7.5 Add tests for stored workspace skill agent selection, omitted-`--tools` behavior, and profile drift reporting. -- [ ] 7.6 Add tests that `openspec update` from a workspace planning home redirects to `openspec workspace update`. -- [ ] 7.7 Add tests that unsupported workspace workflow skills are guarded and do not instruct repo-local fallback edits. -- [ ] 7.8 Add tests that registered repos are visible before change creation. -- [ ] 7.9 Add tests that workspace change creation does not imply repo-local artifact creation. -- [ ] 7.10 Add tests that the workspace-planning schema resolves nested `specs/<area-or-repo>/<capability>/spec.md` files as workspace-scoped specs. -- [ ] 7.11 Add cross-platform path tests for workspace-root skill paths and workspace change paths. -- [ ] 7.12 Update CLI docs, command help, and shell completion coverage for `workspace update`, `--tools`, profile behavior, and workspace skills-only delivery. -- [ ] 7.13 Run `openspec validate workspace-change-planning --strict`. -- [ ] 7.14 Run the full manual acceptance checklist across setup, update, config profile, change creation, planning context, and workflow skills before marking the change complete. -- [ ] 7.15 Complete a final UX review across the whole workflow and record any follow-up fixes or intentional deferrals. -- [ ] 7.16 Before implementation sign-off, record the manual commands or interaction paths, expected observations, and actual observations for each phase. -- [ ] 7.17 Have a separate reviewer or fresh agent context rerun the manual acceptance and UX checklist when available; otherwise rerun it from a clean temporary workspace and report the evidence. +- [x] 7.1 Add tests that workspace setup installs skills in the workspace root and leaves linked repos unchanged. +- [x] 7.2 Add tests that workspace update refreshes, adds, and removes only managed workspace skill directories. +- [x] 7.3 Add tests that workspace setup/update use the current global profile for workflow skill selection while keeping workspace delivery skills-only. +- [x] 7.4 Add tests that `openspec config profile` inside a workspace can apply changes through `openspec workspace update`. +- [x] 7.5 Add tests for stored workspace skill agent selection, omitted-`--tools` behavior, and profile drift reporting. +- [x] 7.6 Add tests that `openspec update` from a workspace planning home redirects to `openspec workspace update`. +- [x] 7.7 Add tests that unsupported workspace workflow skills are guarded and do not instruct repo-local fallback edits. +- [x] 7.8 Add tests that registered repos are visible before change creation. +- [x] 7.9 Add tests that workspace change creation does not imply repo-local artifact creation. +- [x] 7.10 Add tests that the workspace-planning schema resolves nested `specs/<area-or-repo>/<capability>/spec.md` files as workspace-scoped specs. +- [x] 7.11 Add cross-platform path tests for workspace-root skill paths and workspace change paths. +- [x] 7.12 Update CLI docs, command help, and shell completion coverage for `workspace update`, `--tools`, profile behavior, and workspace skills-only delivery. +- [x] 7.13 Run `openspec validate workspace-change-planning --strict`. +- [x] 7.14 Run the full manual acceptance checklist across setup, update, config profile, change creation, planning context, and workflow skills before marking the change complete. +- [x] 7.15 Complete a final UX review across the whole workflow and record any follow-up fixes or intentional deferrals. +- [x] 7.16 Before implementation sign-off, record the manual commands or interaction paths, expected observations, and actual observations for each phase. +- [x] 7.17 Have a separate reviewer or fresh agent context rerun the manual acceptance and UX checklist when available; otherwise rerun it from a clean temporary workspace and report the evidence. diff --git a/src/cli/index.ts b/src/cli/index.ts index d40fdf29c..5a8a47052 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -16,7 +16,8 @@ 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 } from '../commands/workspace.js'; +import { registerWorkspaceCommand, runWorkspaceUpdate } from '../commands/workspace.js'; +import { findWorkspaceRoot } from '../core/workspace/index.js'; import { statusCommand, instructionsCommand, @@ -161,6 +162,12 @@ program .action(async (targetPath = '.', options?: { force?: boolean }) => { try { const resolvedPath = path.resolve(targetPath); + const workspaceRoot = await findWorkspaceRoot(resolvedPath); + if (workspaceRoot) { + await runWorkspaceUpdate(undefined, {}); + return; + } + const updateCommand = new UpdateCommand({ force: options?.force }); await updateCommand.execute(resolvedPath); } catch (error) { diff --git a/src/commands/workspace.ts b/src/commands/workspace.ts index 246649e32..7c7846850 100644 --- a/src/commands/workspace.ts +++ b/src/commands/workspace.ts @@ -1054,6 +1054,14 @@ class WorkspaceCommand { } } +export async function runWorkspaceUpdate( + positionalName: string | undefined, + options: WorkspaceUpdateOptions = {} +): Promise<void> { + const workspaceCommand = new WorkspaceCommand(); + await workspaceCommand.update(positionalName, options); +} + function collectOption(value: string, previous: string[]): string[] { return [...previous, value]; } diff --git a/src/commands/workspace/selection.ts b/src/commands/workspace/selection.ts index 06487be76..16a228aaf 100644 --- a/src/commands/workspace/selection.ts +++ b/src/commands/workspace/selection.ts @@ -14,9 +14,11 @@ import { } from './types.js'; function normalizeRegistryRootForComparison(workspaceRoot: string): string { - return process.platform === 'win32' - ? FileSystemUtils.canonicalizeExistingPath(workspaceRoot) - : workspaceRoot; + try { + return FileSystemUtils.canonicalizeExistingPath(workspaceRoot); + } catch { + return workspaceRoot; + } } export async function selectWorkspaceForCommand( diff --git a/src/core/completions/command-registry.ts b/src/core/completions/command-registry.ts index 545d5d061..9b629b5f7 100644 --- a/src/core/completions/command-registry.ts +++ b/src/core/completions/command-registry.ts @@ -282,7 +282,7 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [ }, { name: 'tools', - description: 'Select agents for workspace skills; global profile selects workflows', + description: 'Select agents for workspace skills-only delivery; global profile selects workflows', takesValue: true, }, COMMON_FLAGS.json, diff --git a/src/core/planning-home.ts b/src/core/planning-home.ts index b0fef2ae6..5c181aa7e 100644 --- a/src/core/planning-home.ts +++ b/src/core/planning-home.ts @@ -95,6 +95,18 @@ function countPathSegments(candidatePath: string): number { return path.resolve(candidatePath).split(path.sep).filter(Boolean).length; } +function isWindowsLikePath(candidatePath: string): boolean { + return /^[A-Za-z]:[\\/]/.test(candidatePath) || candidatePath.startsWith('\\\\'); +} + +function relativePlanningPath(fromPath: string, toPath: string): string { + if (isWindowsLikePath(fromPath) || isWindowsLikePath(toPath)) { + return path.win32.relative(path.win32.normalize(fromPath), path.win32.normalize(toPath)); + } + + return path.posix.relative(fromPath.replace(/\\/g, '/'), toPath.replace(/\\/g, '/')); +} + function readWorkspaceSharedStateSync(workspaceRoot: string): WorkspaceSharedState | null { try { return parseWorkspaceSharedState( @@ -155,11 +167,11 @@ export function resolveCurrentPlanningHomeSync( } export function getChangeDir(planningHome: PlanningHome, changeName: string): string { - return path.join(planningHome.changesDir, changeName); + return FileSystemUtils.joinPath(planningHome.changesDir, changeName); } export function formatChangeLocation(planningHome: PlanningHome, changeName: string): string { const changeDir = getChangeDir(planningHome, changeName); - const relative = path.relative(planningHome.root, changeDir); + const relative = relativePlanningPath(planningHome.root, changeDir); return relative.length > 0 ? relative : changeDir; } diff --git a/src/core/workspace/skills.ts b/src/core/workspace/skills.ts index 469d3ac5f..08837c24c 100644 --- a/src/core/workspace/skills.ts +++ b/src/core/workspace/skills.ts @@ -1,4 +1,3 @@ -import * as path from 'node:path'; import * as nodeFs from 'node:fs'; import { createRequire } from 'node:module'; @@ -229,6 +228,17 @@ function getWorkspaceSkillTool(toolId: string): WorkspaceSkillCapableTool { return tool; } +function getWorkspaceSkillDirectoryForTool( + workspaceRoot: string, + tool: WorkspaceSkillCapableTool +): string { + return FileSystemUtils.joinPath(workspaceRoot, tool.skillsDir, 'skills'); +} + +export function getWorkspaceSkillDirectory(workspaceRoot: string, toolId: string): string { + return getWorkspaceSkillDirectoryForTool(workspaceRoot, getWorkspaceSkillTool(toolId)); +} + function makeAgentResult( workspaceRoot: string, tool: WorkspaceSkillCapableTool, @@ -237,7 +247,7 @@ function makeAgentResult( return { tool_id: tool.value, name: tool.name, - skills_path: path.join(workspaceRoot, tool.skillsDir, 'skills'), + skills_path: getWorkspaceSkillDirectoryForTool(workspaceRoot, tool), workflow_ids: workflowIds, }; } @@ -262,7 +272,7 @@ async function removeManagedWorkflowSkillDirs( reason: WorkspaceSkillRemovedResult['reason'] ): Promise<WorkspaceSkillRemovedResult | null> { const desiredSet = new Set(desiredWorkflowIds); - const skillsDir = path.join(workspaceRoot, tool.skillsDir, 'skills'); + const skillsDir = getWorkspaceSkillDirectoryForTool(workspaceRoot, tool); const removedWorkflowIds: string[] = []; for (const { workflowId, dirName } of getManagedWorkspaceSkillEntries()) { @@ -270,7 +280,7 @@ async function removeManagedWorkflowSkillDirs( continue; } - const skillDir = path.join(skillsDir, dirName); + const skillDir = FileSystemUtils.joinPath(skillsDir, dirName); if (!(await pathExists(skillDir))) { continue; } @@ -324,12 +334,12 @@ export async function generateWorkspaceAgentSkills( const wasConfigured = getToolSkillStatus(workspaceRoot, tool.value).configured; try { - const skillsDir = path.join(workspaceRoot, tool.skillsDir, 'skills'); + const skillsDir = getWorkspaceSkillDirectoryForTool(workspaceRoot, tool); const transformer = tool.value === 'opencode' || tool.value === 'pi' ? transformToHyphenCommands : undefined; for (const { template, dirName } of skillTemplates) { - const skillFile = path.join(skillsDir, dirName, 'SKILL.md'); + const skillFile = FileSystemUtils.joinPath(skillsDir, dirName, 'SKILL.md'); const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer); await FileSystemUtils.writeFile(skillFile, skillContent); } @@ -436,12 +446,12 @@ export async function updateWorkspaceAgentSkills( const tool = getWorkspaceSkillTool(toolId); try { - const skillsDir = path.join(workspaceRoot, tool.skillsDir, 'skills'); + const skillsDir = getWorkspaceSkillDirectoryForTool(workspaceRoot, tool); const transformer = tool.value === 'opencode' || tool.value === 'pi' ? transformToHyphenCommands : undefined; for (const { template, dirName } of skillTemplates) { - const skillFile = path.join(skillsDir, dirName, 'SKILL.md'); + const skillFile = FileSystemUtils.joinPath(skillsDir, dirName, 'SKILL.md'); const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer); await FileSystemUtils.writeFile(skillFile, skillContent); } diff --git a/test/commands/artifact-workflow.test.ts b/test/commands/artifact-workflow.test.ts index 613e37f98..e5753535a 100644 --- a/test/commands/artifact-workflow.test.ts +++ b/test/commands/artifact-workflow.test.ts @@ -342,6 +342,132 @@ describe('artifact-workflow CLI commands', () => { expect(stat.isDirectory()).toBe(true); }); + it('creates workspace-planning changes under the workspace root without touching linked repos', async () => { + const workspaceEnv = { + XDG_DATA_HOME: path.join(tempDir, 'data'), + XDG_CONFIG_HOME: path.join(tempDir, 'config'), + OPEN_SPEC_INTERACTIVE: '0', + OPENSPEC_TELEMETRY: '0', + }; + const api = path.join(tempDir, 'linked-api'); + await fs.mkdir(path.join(api, 'openspec', 'specs'), { recursive: true }); + const apiEntriesBefore = (await fs.readdir(api)).sort(); + + const setup = await runCLI( + [ + 'workspace', + 'setup', + '--no-interactive', + '--json', + '--name', + 'platform', + '--link', + `api=${api}`, + ], + { cwd: tempDir, env: workspaceEnv } + ); + expect(setup.exitCode).toBe(0); + const workspaceRoot = JSON.parse(setup.stdout).workspace.root; + + const create = await runCLI( + [ + 'new', + 'change', + 'cross-repo-login', + '--goal', + 'Unify login across API and web', + '--areas', + 'api', + ], + { cwd: workspaceRoot, env: workspaceEnv } + ); + expect(create.exitCode).toBe(0); + const createOutput = getOutput(create); + expect(createOutput).toContain('workspace change'); + expect(createOutput).toContain('changes/cross-repo-login'); + + const changeDir = path.join(workspaceRoot, 'changes', 'cross-repo-login'); + const metadata = await fs.readFile(path.join(changeDir, '.openspec.yaml'), 'utf-8'); + expect(metadata).toContain('schema: workspace-planning'); + expect(metadata).toContain('goal: Unify login across API and web'); + expect(metadata).toContain('affected_areas:'); + expect(metadata).toContain('- api'); + expect((await fs.readdir(api)).sort()).toEqual(apiEntriesBefore); + await expect(fs.stat(path.join(api, 'openspec', 'changes'))).rejects.toMatchObject({ + code: 'ENOENT', + }); + }); + + it('resolves nested workspace-planning specs as workspace-scoped paths', async () => { + const workspaceEnv = { + XDG_DATA_HOME: path.join(tempDir, 'data'), + XDG_CONFIG_HOME: path.join(tempDir, 'config'), + OPEN_SPEC_INTERACTIVE: '0', + OPENSPEC_TELEMETRY: '0', + }; + const api = path.join(tempDir, 'linked-api'); + await fs.mkdir(api, { recursive: true }); + + const setup = await runCLI( + [ + 'workspace', + 'setup', + '--no-interactive', + '--json', + '--name', + 'platform', + '--link', + `api=${api}`, + ], + { cwd: tempDir, env: workspaceEnv } + ); + expect(setup.exitCode).toBe(0); + const workspaceRoot = JSON.parse(setup.stdout).workspace.root; + + const create = await runCLI( + ['new', 'change', 'nested-workspace-spec', '--goal', 'Plan API login', '--areas', 'api'], + { cwd: workspaceRoot, env: workspaceEnv } + ); + expect(create.exitCode).toBe(0); + + const changeDir = path.join(workspaceRoot, 'changes', 'nested-workspace-spec'); + const specPath = path.join(changeDir, 'specs', 'api', 'login', 'spec.md'); + await fs.mkdir(path.dirname(specPath), { recursive: true }); + await fs.writeFile( + specPath, + '## ADDED Requirements\n\n### Requirement: API login\n\n#### Scenario: Valid login\n- **WHEN** credentials are valid\n- **THEN** login succeeds\n' + ); + + const status = await runCLI(['status', '--change', 'nested-workspace-spec', '--json'], { + cwd: workspaceRoot, + env: workspaceEnv, + }); + expect(status.exitCode).toBe(0); + const statusJson = JSON.parse(status.stdout); + expect(statusJson.schemaName).toBe('workspace-planning'); + expect(statusJson.planningHome.kind).toBe('workspace'); + expect(statusJson.affectedAreas.known).toEqual(['api']); + expect(statusJson.actionContext).toEqual( + expect.objectContaining({ + mode: 'workspace-planning', + allowedEditRoots: [], + }) + ); + expect(statusJson.artifactPaths.specs.existingOutputPaths).toEqual([canonical(specPath)]); + + const instructions = await runCLI( + ['instructions', 'specs', '--change', 'nested-workspace-spec', '--json'], + { cwd: workspaceRoot, env: workspaceEnv } + ); + expect(instructions.exitCode).toBe(0); + const instructionsJson = JSON.parse(instructions.stdout); + expect(instructionsJson.planningHome.kind).toBe('workspace'); + expect(normalizePaths(instructionsJson.resolvedOutputPath)).toContain( + 'changes/nested-workspace-spec/specs/**/*.md' + ); + expect(instructionsJson.existingOutputPaths).toEqual([canonical(specPath)]); + }); + it('creates README.md when --description is provided', async () => { const result = await runCLI( ['new', 'change', 'described-feature', '--description', 'This is a test feature'], diff --git a/test/commands/workspace.test.ts b/test/commands/workspace.test.ts index b5abb35f3..10ed14cb0 100644 --- a/test/commands/workspace.test.ts +++ b/test/commands/workspace.test.ts @@ -234,6 +234,16 @@ describe('workspace command', () => { status: [], }), ]); + + const doctor = await runCLI(['workspace', 'doctor', '--workspace', 'platform', '--json'], { + cwd: tempDir, + env, + }); + expect(doctor.exitCode).toBe(0); + expect(parseJson(doctor).workspace.links).toEqual([ + expect.objectContaining({ name: 'api', path: expectedApi, status: [] }), + expect.objectContaining({ name: 'checkout', path: expectedCheckout, status: [] }), + ]); }); it('keeps non-interactive setup compatible by skipping skills when --tools is omitted', async () => { @@ -453,6 +463,38 @@ describe('workspace command', () => { ); }); + it('redirects openspec update from a workspace planning home to workspace update', async () => { + const api = mkdir('repos/api'); + const linkedEntriesBefore = fs.readdirSync(api).sort(); + writeGlobalConfig({ + profile: 'custom', + delivery: 'commands', + workflows: ['apply'], + }); + const setup = await setupWorkspace('update-redirect', [`api=${api}`], ['--tools', 'codex']); + const workspaceRoot = setup.workspace.root; + expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-apply-change', 'SKILL.md'))).toBe(true); + expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-propose', 'SKILL.md'))).toBe(false); + + writeGlobalConfig({ + profile: 'core', + delivery: 'commands', + }); + + const update = await runCLI(['update'], { + cwd: path.join(workspaceRoot, WORKSPACE_CHANGES_DIR_NAME), + env, + }); + expect(update.exitCode).toBe(0); + expect(update.stdout).toContain('Workspace update complete'); + expect(update.stdout).toContain('update-redirect'); + expect(update.stdout).not.toContain('not recorded in the local workspace registry'); + expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-propose', 'SKILL.md'))).toBe(true); + expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-sync-specs', 'SKILL.md'))).toBe(true); + expect(fs.readdirSync(api).sort()).toEqual(linkedEntriesBefore); + expect(fs.existsSync(path.join(api, '.codex'))).toBe(false); + }); + it('supports named and flag-selected workspace updates with explicit agent changes', async () => { const api = mkdir('repos/api'); writeGlobalConfig({ @@ -1553,6 +1595,9 @@ preferred_opener: 'open', ]); expect(setup?.flags?.some((flag) => flag.name === 'opener')).toBe(true); + expect(setup?.flags?.find((flag) => flag.name === 'tools')?.description).toContain( + 'Install OpenSpec skills' + ); expect(setup?.flags?.find((flag) => flag.name === 'opener')?.values).toEqual([ 'codex', 'claude', @@ -1576,6 +1621,13 @@ preferred_opener: 'json', 'no-interactive', ]); + expect(update?.description).toContain('active global profile'); + expect(update?.flags?.find((flag) => flag.name === 'tools')?.description).toContain( + 'global profile selects workflows' + ); + expect(update?.flags?.find((flag) => flag.name === 'tools')?.description).toContain( + 'skills-only' + ); expect(open?.positionals).toEqual([ { name: 'name', optional: true }, ]); diff --git a/test/core/planning-home.test.ts b/test/core/planning-home.test.ts new file mode 100644 index 000000000..d64d3edd4 --- /dev/null +++ b/test/core/planning-home.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest'; + +import { + type PlanningHome, + formatChangeLocation, + getChangeDir, +} from '../../src/core/planning-home.js'; + +describe('planning home paths', () => { + it('builds workspace change paths with the planning home path style', () => { + const workspacePlanningHome: PlanningHome = { + kind: 'workspace', + root: 'D:\\repos\\platform-workspace', + changesDir: 'D:\\repos\\platform-workspace\\changes', + defaultSchema: 'workspace-planning', + workspace: { + name: 'platform', + links: ['api', 'web'], + }, + }; + + expect(getChangeDir(workspacePlanningHome, 'cross-repo-login')).toBe( + 'D:\\repos\\platform-workspace\\changes\\cross-repo-login' + ); + expect(formatChangeLocation(workspacePlanningHome, 'cross-repo-login')).toBe( + 'changes\\cross-repo-login' + ); + }); +}); diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index 49ffe7f5b..f851082e5 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -150,4 +150,23 @@ describe('skill templates split parity', () => { expect(actualHashes).toEqual(EXPECTED_GENERATED_SKILL_CONTENT_HASHES); }); + + it('guards unsupported workspace workflows from repo-local fallback edits', () => { + const guardedSkills: Array<[string, () => SkillTemplate, string]> = [ + ['openspec-apply-change', getApplyChangeSkillTemplate, 'full workspace apply is not supported'], + ['openspec-sync-specs', getSyncSpecsSkillTemplate, 'workspace spec sync is not supported'], + ['openspec-archive-change', getArchiveChangeSkillTemplate, 'workspace archive is not supported'], + ['openspec-bulk-archive-change', getBulkArchiveChangeSkillTemplate, 'workspace bulk archive is not supported'], + ['openspec-verify-change', getVerifyChangeSkillTemplate, 'full workspace implementation verification is not supported'], + ]; + + for (const [dirName, createTemplate, guardText] of guardedSkills) { + const content = generateSkillContent(createTemplate(), 'PARITY-BASELINE'); + + expect(content, dirName).toContain('actionContext.mode: "workspace-planning"'); + expect(content, dirName).toContain(guardText); + expect(content, dirName).not.toContain('openspec/changes/<name>'); + expect(content, dirName).not.toContain('mv openspec/changes'); + } + }); }); diff --git a/test/core/workspace/skills.test.ts b/test/core/workspace/skills.test.ts index 5fae5b80b..bed5b046c 100644 --- a/test/core/workspace/skills.test.ts +++ b/test/core/workspace/skills.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { + getWorkspaceSkillDirectory, getWorkspaceSkillToolIds, parseWorkspaceSkillToolsValue, } from '../../../src/core/workspace/skills.js'; @@ -17,4 +18,13 @@ describe('workspace skill helpers', () => { expect(() => parseWorkspaceSkillToolsValue('all,codex')).toThrow(/Cannot combine/); expect(() => parseWorkspaceSkillToolsValue('codex,missing')).toThrow(/missing/); }); + + it('builds workspace-root skill paths with the workspace path style', () => { + expect(getWorkspaceSkillDirectory('/repos/platform-workspace', 'codex')).toBe( + '/repos/platform-workspace/.codex/skills' + ); + expect(getWorkspaceSkillDirectory('D:\\repos\\platform-workspace', 'codex')).toBe( + 'D:\\repos\\platform-workspace\\.codex\\skills' + ); + }); }); From c6c2b358addcc588390be6b51896b2fa6f338f75 Mon Sep 17 00:00:00 2001 From: TabishB <tabishbidiwale@gmail.com> Date: Thu, 14 May 2026 03:21:40 +1000 Subject: [PATCH 09/14] Fix workspace update review issues --- .../workspace-planning/templates/design.md | 4 +- .../workspace-planning/templates/proposal.md | 6 +- src/cli/index.ts | 7 +- src/commands/workspace.ts | 132 ++++++++++++------ src/commands/workspace/selection.ts | 82 ++++++++--- src/core/workspace/skills.ts | 10 ++ test/commands/workspace.test.ts | 89 ++++++++++++ 7 files changed, 258 insertions(+), 72 deletions(-) diff --git a/schemas/workspace-planning/templates/design.md b/schemas/workspace-planning/templates/design.md index d8a491ef3..2a6194648 100644 --- a/schemas/workspace-planning/templates/design.md +++ b/schemas/workspace-planning/templates/design.md @@ -5,7 +5,7 @@ Summarize the workspace planning context, relevant linked areas, and constraints ## Goals / Non-Goals **Goals:** -- +- **Non-Goals:** - Creating repo-local implementation artifacts before an affected area is selected. @@ -30,4 +30,4 @@ Alternative considered: <alternative and why it was not chosen> ## Open Questions -- +- diff --git a/schemas/workspace-planning/templates/proposal.md b/schemas/workspace-planning/templates/proposal.md index b8c018276..d79448883 100644 --- a/schemas/workspace-planning/templates/proposal.md +++ b/schemas/workspace-planning/templates/proposal.md @@ -4,7 +4,7 @@ Describe the shared product goal, problem, or opportunity that makes this worksp ## What Changes -- +- ## Affected Areas @@ -15,11 +15,11 @@ Describe the shared product goal, problem, or opportunity that makes this worksp ### New Capabilities -- +- ### Modified Capabilities -- +- ## Impact diff --git a/src/cli/index.ts b/src/cli/index.ts index 5a8a47052..186e13eb9 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -16,7 +16,10 @@ 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, runWorkspaceUpdate } from '../commands/workspace.js'; +import { + registerWorkspaceCommand, + runWorkspaceUpdateForRoot, +} from '../commands/workspace.js'; import { findWorkspaceRoot } from '../core/workspace/index.js'; import { statusCommand, @@ -164,7 +167,7 @@ program const resolvedPath = path.resolve(targetPath); const workspaceRoot = await findWorkspaceRoot(resolvedPath); if (workspaceRoot) { - await runWorkspaceUpdate(undefined, {}); + await runWorkspaceUpdateForRoot(workspaceRoot, {}); return; } diff --git a/src/commands/workspace.ts b/src/commands/workspace.ts index 7c7846850..28d3c43d2 100644 --- a/src/commands/workspace.ts +++ b/src/commands/workspace.ts @@ -37,7 +37,10 @@ import { validateLinkNameForCommand, validateWorkspaceNameForSetup, } from './workspace/operations.js'; -import { selectWorkspaceForCommand } from './workspace/selection.js'; +import { + selectWorkspaceForCommand, + selectWorkspaceRootForCommand, +} from './workspace/selection.js'; import { assertWorkspaceOpenerAvailable, buildWorkspaceOpenCommandForState, @@ -52,6 +55,7 @@ import { WorkspaceListOptions, WorkspaceOpenOptions, WorkspaceOutput, + SelectedWorkspace, WorkspaceSetupOptions, WorkspaceStatus, WorkspaceUpdateOptions, @@ -539,6 +543,16 @@ function printWorkspaceSkillReportHuman(report: WorkspaceSkillInstallationReport } } +function hasWorkspaceSkillFailures(report: WorkspaceSkillInstallationReport): boolean { + return report.failed.length > 0; +} + +function setWorkspaceSkillFailureExitCode(report: WorkspaceSkillInstallationReport): void { + if (hasWorkspaceSkillFailures(report)) { + process.exitCode = 1; + } +} + async function writeWorkspaceSkillState( workspaceRoot: string, selectedAgentIds: string[], @@ -786,7 +800,7 @@ class WorkspaceCommand { ) : await generateWorkspaceAgentSkills(workspace.root, selectedWorkspaceSkillAgents); - if (selectedWorkspaceSkillAgents !== undefined) { + if (selectedWorkspaceSkillAgents !== undefined && !hasWorkspaceSkillFailures(skillReport)) { await writeWorkspaceSkillState(workspace.root, selectedWorkspaceSkillAgents, skillReport); } @@ -803,6 +817,7 @@ class WorkspaceCommand { workspace_skills: skillReport, status: doctorResult.status, }); + setWorkspaceSkillFailureExitCode(skillReport); return; } @@ -821,6 +836,8 @@ class WorkspaceCommand { console.log(` openspec workspace doctor --workspace ${workspace.name}`); console.log(` openspec workspace update --workspace ${workspace.name} --tools <ids>`); console.log(' openspec workspace list'); + + setWorkspaceSkillFailureExitCode(skillReport); } catch (error) { this.handleFailure(options.json, { workspace: null, status: [] }, error); } @@ -939,54 +956,73 @@ class WorkspaceCommand { 'update', { preferPositionalName: Boolean(positionalName) } ); - const { localState } = await readWorkspaceForMutation(selected); - const hasExplicitToolSelection = options.tools !== undefined; - const selectedAgentIds = hasExplicitToolSelection - ? parseUpdateToolsOption(options.tools ?? '') - : localState.workspace_skills?.selected_agents ?? []; - const previousSkillState = - hasExplicitToolSelection - ? localState.workspace_skills ?? { selected_agents: [] } - : localState.workspace_skills; - const skillReport = await updateWorkspaceAgentSkills( - selected.root, - selectedAgentIds, - previousSkillState - ); - const shouldStoreSelection = hasExplicitToolSelection || Boolean(localState.workspace_skills); + await this.updateSelected(selected, options); + } catch (error) { + this.handleFailure(options.json, { workspace: null, workspace_skills: null, status: [] }, error); + } + } - if (shouldStoreSelection) { - await writeWorkspaceSkillState(selected.root, selectedAgentIds, skillReport); - await recordSelectedWorkspaceAfterMutation(selected); - } + 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); + } + } - const doctorResult = await loadWorkspaceForDoctor(selected); + private async updateSelected( + selected: SelectedWorkspace, + options: WorkspaceUpdateOptions + ): Promise<void> { + const { localState } = await readWorkspaceForMutation(selected); + const hasExplicitToolSelection = options.tools !== undefined; + const selectedAgentIds = hasExplicitToolSelection + ? parseUpdateToolsOption(options.tools ?? '') + : localState.workspace_skills?.selected_agents ?? []; + const previousSkillState = + hasExplicitToolSelection + ? localState.workspace_skills ?? { selected_agents: [] } + : localState.workspace_skills; + const skillReport = await updateWorkspaceAgentSkills( + selected.root, + selectedAgentIds, + previousSkillState + ); + const shouldStoreSelection = hasExplicitToolSelection || Boolean(localState.workspace_skills); - if (options.json) { - printJson({ - workspace: doctorResult.workspace, - workspace_skills: skillReport, - status: doctorResult.status, - }); - return; - } + if (shouldStoreSelection && !hasWorkspaceSkillFailures(skillReport)) { + await writeWorkspaceSkillState(selected.root, selectedAgentIds, skillReport); + await recordSelectedWorkspaceAfterMutation(selected); + } - console.log(chalk.green('Workspace update complete')); - console.log(`Workspace: ${doctorResult.workspace.name}`); - console.log(`Location: ${doctorResult.workspace.root}`); - console.log(''); - printStatusLines(doctorResult.status); - if (doctorResult.status.length > 0) { - console.log(''); - } - printWorkspaceSkillReportHuman(skillReport); + const doctorResult = await loadWorkspaceForDoctor(selected); + + if (options.json) { + printJson({ + workspace: doctorResult.workspace, + workspace_skills: skillReport, + status: doctorResult.status, + }); + setWorkspaceSkillFailureExitCode(skillReport); + return; + } + + console.log(chalk.green('Workspace update complete')); + console.log(`Workspace: ${doctorResult.workspace.name}`); + console.log(`Location: ${doctorResult.workspace.root}`); + console.log(''); + printStatusLines(doctorResult.status); + if (doctorResult.status.length > 0) { console.log(''); - console.log('Next useful commands:'); - console.log(` openspec workspace doctor --workspace ${doctorResult.workspace.name}`); - console.log(` openspec workspace update --workspace ${doctorResult.workspace.name} --tools <ids>`); - } catch (error) { - this.handleFailure(options.json, { workspace: null, workspace_skills: null, status: [] }, error); } + printWorkspaceSkillReportHuman(skillReport); + console.log(''); + console.log('Next useful commands:'); + console.log(` openspec workspace doctor --workspace ${doctorResult.workspace.name}`); + console.log(` openspec workspace update --workspace ${doctorResult.workspace.name} --tools <ids>`); + + setWorkspaceSkillFailureExitCode(skillReport); } async open( @@ -1062,6 +1098,14 @@ 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); +} + function collectOption(value: string, previous: string[]): string[] { return [...previous, value]; } diff --git a/src/commands/workspace/selection.ts b/src/commands/workspace/selection.ts index 16a228aaf..05dfa9dbd 100644 --- a/src/commands/workspace/selection.ts +++ b/src/commands/workspace/selection.ts @@ -10,6 +10,7 @@ import { SelectedWorkspace, WorkspaceCliError, WorkspaceSelectionOptions, + WorkspaceStatus, makeStatus, } from './types.js'; @@ -21,6 +22,65 @@ function normalizeRegistryRootForComparison(workspaceRoot: string): string { } } +function workspaceNotInRegistryWarning(): WorkspaceStatus { + return makeStatus( + 'warning', + 'workspace_not_in_local_registry', + 'This workspace is not recorded in the local workspace registry.', + { + target: 'workspace.root', + fix: 'Run a mutating workspace command from this workspace, such as workspace link or workspace relink, to record it locally.', + } + ); +} + +function isRegisteredWorkspaceRoot( + registryRoot: string | undefined, + currentWorkspaceRoot: string +): boolean { + return ( + registryRoot !== undefined && + normalizeRegistryRootForComparison(registryRoot) === + normalizeRegistryRootForComparison(currentWorkspaceRoot) + ); +} + +async function selectedWorkspaceFromRoot( + currentWorkspaceRoot: string, + registry: Awaited<ReturnType<typeof readRegistry>> +): Promise<SelectedWorkspace> { + const sharedState = await readWorkspaceSharedState(currentWorkspaceRoot); + const registeredRoot = registry.workspaces[sharedState.name]; + const isRegistered = isRegisteredWorkspaceRoot(registeredRoot, currentWorkspaceRoot); + + return { + name: sharedState.name, + root: currentWorkspaceRoot, + status: isRegistered ? [] : [workspaceNotInRegistryWarning()], + unregisteredCurrentWorkspace: !isRegistered, + }; +} + +export async function selectWorkspaceRootForCommand( + workspaceRoot: string +): Promise<SelectedWorkspace> { + const registry = await readRegistry(); + 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, registry); +} + export async function selectWorkspaceForCommand( options: WorkspaceSelectionOptions, commandName: string, @@ -54,27 +114,7 @@ export async function selectWorkspaceForCommand( const currentWorkspaceRoot = await findWorkspaceRoot(process.cwd()); if (currentWorkspaceRoot) { - const sharedState = await readWorkspaceSharedState(currentWorkspaceRoot); - const registeredRoot = registry.workspaces[sharedState.name]; - const isRegistered = - registeredRoot !== undefined && - normalizeRegistryRootForComparison(registeredRoot) === currentWorkspaceRoot; - const warning = makeStatus( - 'warning', - 'workspace_not_in_local_registry', - 'This workspace is not recorded in the local workspace registry.', - { - target: 'workspace.root', - fix: 'Run a mutating workspace command from this workspace, such as workspace link or workspace relink, to record it locally.', - } - ); - - return { - name: sharedState.name, - root: currentWorkspaceRoot, - status: isRegistered ? [] : [warning], - unregisteredCurrentWorkspace: !isRegistered, - }; + return selectedWorkspaceFromRoot(currentWorkspaceRoot, registry); } const entries = listWorkspaceRegistryEntries(registry); diff --git a/src/core/workspace/skills.ts b/src/core/workspace/skills.ts index 08837c24c..db8b0793e 100644 --- a/src/core/workspace/skills.ts +++ b/src/core/workspace/skills.ts @@ -11,6 +11,7 @@ import { getSkillTemplates, getToolSkillStatus, getToolsWithSkillsDir, + extractGeneratedByVersion, } from '../shared/index.js'; import type { WorkspaceLocalState, WorkspaceSkillState } from './foundation.js'; @@ -265,6 +266,11 @@ async function pathExists(targetPath: string): Promise<boolean> { } } +function isOpenSpecManagedSkillDir(skillDir: string): boolean { + const skillFile = FileSystemUtils.joinPath(skillDir, 'SKILL.md'); + return extractGeneratedByVersion(skillFile) !== null; +} + async function removeManagedWorkflowSkillDirs( workspaceRoot: string, tool: WorkspaceSkillCapableTool, @@ -285,6 +291,10 @@ async function removeManagedWorkflowSkillDirs( continue; } + if (!isOpenSpecManagedSkillDir(skillDir)) { + continue; + } + await fs.rm(skillDir, { recursive: true, force: true }); removedWorkflowIds.push(workflowId); } diff --git a/test/commands/workspace.test.ts b/test/commands/workspace.test.ts index 10ed14cb0..4554729d6 100644 --- a/test/commands/workspace.test.ts +++ b/test/commands/workspace.test.ts @@ -495,6 +495,35 @@ describe('workspace command', () => { expect(fs.existsSync(path.join(api, '.codex'))).toBe(false); }); + it('updates the workspace passed to openspec update even when another workspace is known', async () => { + const firstApi = mkdir('repos/first-api'); + const secondApi = mkdir('repos/second-api'); + writeGlobalConfig({ + profile: 'custom', + delivery: 'commands', + workflows: ['apply'], + }); + const first = await setupWorkspace('target-first', [`api=${firstApi}`], ['--tools', 'codex']); + const second = await setupWorkspace('target-second', [`api=${secondApi}`], ['--tools', 'codex']); + + writeGlobalConfig({ + profile: 'core', + delivery: 'commands', + }); + + const update = await runCLI( + ['update', path.join(first.workspace.root, WORKSPACE_CHANGES_DIR_NAME)], + { cwd: tempDir, env } + ); + + expect(update.exitCode).toBe(0); + expect(update.stdout).toContain('Workspace update complete'); + expect(update.stdout).toContain('target-first'); + expect(update.stdout).not.toContain('Multiple OpenSpec workspaces are known'); + expect(fs.existsSync(path.join(first.workspace.root, '.codex', 'skills', 'openspec-propose', 'SKILL.md'))).toBe(true); + expect(fs.existsSync(path.join(second.workspace.root, '.codex', 'skills', 'openspec-propose', 'SKILL.md'))).toBe(false); + }); + it('supports named and flag-selected workspace updates with explicit agent changes', async () => { const api = mkdir('repos/api'); writeGlobalConfig({ @@ -544,6 +573,66 @@ describe('workspace command', () => { expect(readLocalState(workspaceRoot).workspace_skills?.selected_agents).toEqual(['claude']); }); + it('does not remove unmanaged skill directories that collide with OpenSpec workflow names', async () => { + const api = mkdir('repos/api'); + writeGlobalConfig({ + profile: 'custom', + delivery: 'skills', + workflows: ['verify'], + }); + const setup = await setupWorkspace('unmanaged-collision', [`api=${api}`], ['--tools', 'codex']); + const workspaceRoot = setup.workspace.root; + const collidingSkillDir = path.join(workspaceRoot, '.codex', 'skills', 'openspec-verify-change'); + fs.writeFileSync(path.join(collidingSkillDir, 'SKILL.md'), 'name: user-owned-verify\n'); + + const update = await runCLI( + ['workspace', 'update', '--workspace', 'unmanaged-collision', '--tools', 'none', '--json'], + { cwd: tempDir, env } + ); + + expect(update.exitCode).toBe(0); + expect(parseJson(update).workspace_skills.removed).toEqual([]); + expect(fs.existsSync(path.join(collidingSkillDir, 'SKILL.md'))).toBe(true); + expect(readLocalState(workspaceRoot).workspace_skills?.selected_agents).toEqual([]); + }); + + it('does not record workspace skills as applied when an update fails', async () => { + const api = mkdir('repos/api'); + writeGlobalConfig({ + profile: 'custom', + delivery: 'skills', + workflows: ['apply'], + }); + const setup = await setupWorkspace('failed-update-state', [`api=${api}`], ['--tools', 'codex']); + const workspaceRoot = setup.workspace.root; + const blockingSkillPath = path.join(workspaceRoot, '.codex', 'skills', 'openspec-propose'); + fs.writeFileSync(blockingSkillPath, 'blocks generated skill directory\n'); + + writeGlobalConfig({ + profile: 'core', + delivery: 'skills', + }); + + const update = await runCLI( + ['workspace', 'update', '--workspace', 'failed-update-state', '--json'], + { cwd: tempDir, env } + ); + + expect(update.exitCode).toBe(1); + expect(parseJson(update).workspace_skills.failed).toEqual([ + expect.objectContaining({ + tool_id: 'codex', + }), + ]); + expect(readLocalState(workspaceRoot).workspace_skills).toEqual( + expect.objectContaining({ + selected_agents: ['codex'], + last_applied_profile: 'custom', + last_applied_workflow_ids: ['apply'], + }) + ); + }); + it('reports a no-op workspace update when no stored skill selection exists', async () => { const api = mkdir('repos/api'); const setup = await setupWorkspace('no-stored-skills', [`api=${api}`]); From 7320b115810515ff3a15484aceb6c0043a773515 Mon Sep 17 00:00:00 2001 From: TabishB <tabishbidiwale@gmail.com> Date: Thu, 14 May 2026 15:24:58 +1000 Subject: [PATCH 10/14] Fix workspace skill drift comparison --- src/core/workspace/skills.ts | 12 +++++++-- test/core/workspace/skills.test.ts | 39 ++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/core/workspace/skills.ts b/src/core/workspace/skills.ts index db8b0793e..dca3167ac 100644 --- a/src/core/workspace/skills.ts +++ b/src/core/workspace/skills.ts @@ -99,11 +99,19 @@ export function getCurrentWorkspaceSkillProfileSelection(): { } function arraysEqual(left: readonly string[] | undefined, right: readonly string[]): boolean { - if (!left || left.length !== right.length) { + const leftValues = left ?? []; + if (leftValues.length !== right.length) { return false; } - return left.every((value, index) => value === right[index]); + const leftSet = new Set(leftValues); + const rightSet = new Set(right); + + if (leftSet.size !== rightSet.size) { + return false; + } + + return [...leftSet].every((value) => rightSet.has(value)); } export function hasWorkspaceSkillProfileDrift( diff --git a/test/core/workspace/skills.test.ts b/test/core/workspace/skills.test.ts index bed5b046c..c776ff851 100644 --- a/test/core/workspace/skills.test.ts +++ b/test/core/workspace/skills.test.ts @@ -1,10 +1,34 @@ +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; + import { describe, expect, it } from 'vitest'; import { getWorkspaceSkillDirectory, getWorkspaceSkillToolIds, + hasWorkspaceSkillProfileDrift, parseWorkspaceSkillToolsValue, } from '../../../src/core/workspace/skills.js'; +import { CORE_WORKFLOWS } from '../../../src/core/profiles.js'; + +function withDefaultGlobalConfig<T>(callback: () => T): T { + const previousConfigHome = process.env.XDG_CONFIG_HOME; + const configHome = fs.mkdtempSync(path.join(os.tmpdir(), 'openspec-workspace-skills-')); + + process.env.XDG_CONFIG_HOME = configHome; + + try { + return callback(); + } finally { + if (previousConfigHome === undefined) { + delete process.env.XDG_CONFIG_HOME; + } else { + process.env.XDG_CONFIG_HOME = previousConfigHome; + } + fs.rmSync(configHome, { recursive: true, force: true }); + } +} describe('workspace skill helpers', () => { it('parses workspace --tools values using the skill-capable tool set', () => { @@ -27,4 +51,19 @@ describe('workspace skill helpers', () => { 'D:\\repos\\platform-workspace\\.codex\\skills' ); }); + + it('does not report profile drift when workflow IDs match in a different order', () => { + withDefaultGlobalConfig(() => { + expect( + hasWorkspaceSkillProfileDrift({ + workspace_skills: { + selected_agents: ['codex'], + last_applied_profile: 'core', + last_applied_delivery: 'both', + last_applied_workflow_ids: [...CORE_WORKFLOWS].reverse(), + }, + }) + ).toBe(false); + }); + }); }); From d07e321de36f12b9410a87b48d5215f0380fc14e Mon Sep 17 00:00:00 2001 From: TabishB <tabishbidiwale@gmail.com> Date: Thu, 14 May 2026 15:46:30 +1000 Subject: [PATCH 11/14] Clean up workspace change planning artifacts --- .../candidate-requirements.md | 178 -- .../deep-dive-requirements.html | 1660 ----------------- .../workspace-change-planning/design.md | 11 + .../exploration-summary.md | 337 ---- .../manual-acceptance.md | 74 - .../changes/workspace-change-planning/prd.md | 275 --- .../review-candidate-requirements.html | 868 --------- .../workspace-change-planning/tasks.md | 23 + 8 files changed, 34 insertions(+), 3392 deletions(-) delete mode 100644 openspec/changes/workspace-change-planning/candidate-requirements.md delete mode 100644 openspec/changes/workspace-change-planning/deep-dive-requirements.html delete mode 100644 openspec/changes/workspace-change-planning/exploration-summary.md delete mode 100644 openspec/changes/workspace-change-planning/manual-acceptance.md delete mode 100644 openspec/changes/workspace-change-planning/prd.md delete mode 100644 openspec/changes/workspace-change-planning/review-candidate-requirements.html diff --git a/openspec/changes/workspace-change-planning/candidate-requirements.md b/openspec/changes/workspace-change-planning/candidate-requirements.md deleted file mode 100644 index d1a3b62af..000000000 --- a/openspec/changes/workspace-change-planning/candidate-requirements.md +++ /dev/null @@ -1,178 +0,0 @@ -# Candidate Requirements - -Date: 2026-05-11 - -Status: exploratory requirements notes. These capture requirements that emerged from discussion, but they are not yet finalized OpenSpec spec deltas. - -## Requirement: Clear Workspace Planning Framing - -Workspace change planning should be framed around helping users and agents plan work across linked repos or folders. - -The user should understand: - -- where to stand -- what the workspace can see -- where the plan will be captured -- when implementation begins - -The feature should be described by the planning experience it enables, not primarily by the POC behavior it avoids. - -## Requirement: Workspace As Planning Home - -A workspace change should capture the shared plan for a cross-area effort. - -The workspace should provide a planning home for: - -- goals -- decisions -- coordination tasks -- affected areas -- cross-area risks and dependencies - -Repo-local execution should begin only when the user or agent moves into an implementation workflow for a selected area. - -## Requirement: Preserve The Familiar OpenSpec Workflow - -Workspace mode should preserve the familiar OpenSpec workflow vocabulary. - -Users should still be able to think in terms of: - -- explore -- propose -- apply -- verify -- archive - -Workspace context should change paths, scope, and allowed actions, not require a separate user-facing workflow family. - -## Requirement: Avoid Workspace-Specific Skill Duplication - -OpenSpec should not require separate workspace-specific versions of every workflow skill. - -OpenSpec should maintain one conceptual workflow skill per workflow, such as: - -- `openspec-propose` -- `openspec-explore` -- `openspec-apply-change` -- `openspec-verify-change` -- `openspec-archive-change` - -Each workflow skill should discover whether it is operating in repo-local or workspace mode instead of being duplicated by mode. - -Workspace-specific behavior should come from CLI-reported context, not from a parallel set of workspace-only skills. - -## Requirement: Separate Agent Affordances From Workflow Semantics - -OpenSpec workflow instructions should describe the OpenSpec product workflow. - -Agent-specific instructions should describe how the current coding agent performs common actions, such as: - -- asking the user questions -- tracking todos -- delegating to subagents -- attaching directories -- handling agent-specific session constraints - -Workflow skills should reference or use an agent affordance layer instead of repeating agent-specific instructions in every workflow. - -## Requirement: Status JSON Provides Agent Context - -`openspec status --change <id> --json` should provide enough context for an agent to understand the current change and act safely. - -Status JSON should tell the agent: - -- whether the change is repo-local or workspace-scoped -- where the change artifacts live -- which artifact paths are relevant -- what should happen next -- which areas are affected or unresolved -- which edit roots are allowed for implementation -- whether the next action needs an area selection - -Agents should not need to assume that changes always live under `openspec/changes/<id>/`. - -## Requirement: Use Plain Action Language - -OpenSpec should use simple terms for agent-facing action context. - -Preferred terms: - -- `nextSteps`: what should happen next -- `actionContext`: paths, areas, and constraints needed to act - -Avoid exposing implementation-oriented terms such as "workflow affordances" in user-facing docs or JSON fields. - -## Requirement: Apply Means Implement - -`/apply` should start or continue implementation work for an already planned change. - -In repo-local mode, `/apply` should: - -- read the repo-local change artifacts -- identify pending implementation tasks -- implement the pending tasks in that repo -- update task progress as implementation work is completed - -In workspace mode, `/apply` should: - -- select or confirm one affected area when needed -- read the workspace planning context for that area -- identify the implementation root for that area -- implement only inside the allowed repo or folder -- update the relevant task progress as work is completed - -If a workspace change has multiple affected areas and the user did not specify one, `/apply` should ask which area to implement. - -## Requirement: Slice Means Delivery Increment - -`slice` should refer to a delivery increment inside a larger change. - -A slice should help users split a large effort into smaller planned parts that can be: - -- designed -- implemented -- reviewed -- verified -- shipped - -Repo or folder ownership should use a different term so delivery breakdown and ownership boundaries remain distinct. - -## Requirement: Area Means Ownership Or Execution Boundary - -`area` should describe where ownership or implementation happens. - -Examples of areas: - -- repo -- package -- folder -- service -- app -- docs site - -A small workspace change may have several areas and one implicit slice. - -A large workspace change may have several slices, and each slice may affect one or more areas. - -OpenSpec should be able to report affected areas without forcing users to think of those areas as delivery slices. - -## Requirement: Keep Unsettled Direction In Exploratory Notes - -Open questions and unsettled direction should live in exploratory notes until they are ready to become proposal, design, or spec content. - -`proposal.md` should be updated when the scope and intended behavior are clear enough to propose. - -`design.md` should be created or updated when technical decisions are selected, not while the team is still comparing models. - -## Requirement: Preserve Exploration Context - -The workspace change planning exploration should preserve: - -- key findings -- candidate requirements -- open questions -- candidate terminology -- POC lessons -- rejected or risky directions - -Future work on `workspace-change-planning` should be able to use these notes without mistaking them for final requirements. diff --git a/openspec/changes/workspace-change-planning/deep-dive-requirements.html b/openspec/changes/workspace-change-planning/deep-dive-requirements.html deleted file mode 100644 index 0f1ea53ba..000000000 --- a/openspec/changes/workspace-change-planning/deep-dive-requirements.html +++ /dev/null @@ -1,1660 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> -<head> -<meta charset="UTF-8" /> -<title>Deep Dive: Workspace Change Planning Requirements - - - - - - -
      -
      -

      Workspace Change Planning — Deep Dive

      -
      openspec/changes/workspace-change-planning/candidate-requirements.md · 12 requirements
      -
      - -
      - -
      - - -
      -
      -
      -
      Requirement 1 of 12
      -

      -
      -
      - -
      - - - - - - - -
      - -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
        -
        -
        -
          -
          -
          -
          -
          -
          -
          -
          Acceptance Signal
          -
          -
          -
          -
          -
          -
          - -
          -
          - Your read -
          - - - - -
          - - - ← → nav · 1-7 tabs · esc to clear note -
          -
          - -
          Copied!
          - - - - diff --git a/openspec/changes/workspace-change-planning/design.md b/openspec/changes/workspace-change-planning/design.md index 225aa0923..7a297ed0d 100644 --- a/openspec/changes/workspace-change-planning/design.md +++ b/openspec/changes/workspace-change-planning/design.md @@ -215,6 +215,17 @@ When practical, a separate reviewer or fresh agent context should run the manual Alternative considered: rely on automated tests plus the implementer's final review. Automated tests are necessary, but this change is workflow-heavy and agent-facing, so independent evidence is more useful than confidence alone. +## Deferred Direction + +The earlier product notes pointed at a richer workspace model than this slice ships. Keep that direction as follow-up material, not competing current scope. + +- Full workspace apply should select or confirm one work focus before implementation. The first work focus should be an affected area with an allowed edit root; later work may add an optional delivery phase when a large change needs sequencing. Until that model exists, workspace apply/verify/archive skills remain guarded. +- Workspace verify and archive should wait for a clear model of partial area completion, final whole-change completion, and how workspace-scoped specs become repo-local canonical specs. +- Scoped plan files may eventually attach at the change, phase, affected-area, or work-focus level. This slice intentionally keeps the first workspace schema close to normal OpenSpec artifacts: proposal, specs, design, and tasks. +- Affected areas can start as registered workspace link names, but future flows may refine or derive them from planning artifacts. That derivation should avoid reintroducing target-first or repo-slice language. +- Workflow skills may later separate generic OpenSpec workflow semantics from agent-specific affordances such as asking questions, tracking todos, or delegating work. This slice only makes generated workflow skills path-agnostic. +- OpenSpec may need a named exploratory-notes convention for preserving unsettled thinking before it is promoted into proposal, design, specs, or tasks. This cleanup keeps the current change folder focused on standard artifacts. + ## Risks / Trade-offs - Skill generation logic may drift from `init/update` → share the same template generation and tool validation helpers where practical. diff --git a/openspec/changes/workspace-change-planning/exploration-summary.md b/openspec/changes/workspace-change-planning/exploration-summary.md deleted file mode 100644 index 96d117578..000000000 --- a/openspec/changes/workspace-change-planning/exploration-summary.md +++ /dev/null @@ -1,337 +0,0 @@ -# Workspace Change Planning Exploration Summary - -Date: 2026-05-11 - -Status: exploratory notes. This file records open thinking from the session. It does not represent a final product direction. - -## Why This Discussion Happened - -The active `workspace-change-planning` proposal still contains framing from the workspace POC era, especially the phrase "without immediately materializing repo-local artifacts." That framing is confusing in the current reimplementation because workspace setup/open already separates repo visibility from implementation. - -The real problem space is broader: - -- users need to plan changes that may span linked repos or folders -- agents need to explore across the workspace before committing to a plan shape -- OpenSpec should avoid making workspace mode feel like a second product with duplicated workflow skills -- large changes may need to be broken down into smaller parts without becoming many unrelated changes - -## Workspace POC Findings - -The workspace POC branch in PR #1006 changed `openspec new change` to behave differently inside a workspace. - -Outside a workspace, the command behaved like the normal repo-local flow: - -```bash -openspec new change add-auth -``` - -created: - -```text -openspec/changes/add-auth/ - .openspec.yaml -``` - -Inside a workspace, the POC required explicit targets: - -```bash -openspec new change add-3ds --targets api,web -``` - -created: - -```text -workspace/ - changes/add-3ds/ - .openspec.yaml # schema, created, targets - proposal.md - design.md - tasks/ - coordination.md - targets/ - api/ - tasks.md - specs/ - web/ - tasks.md - specs/ -``` - -Important POC behavior: - -- workspace changes lived at top-level `changes//`, not repo-local `openspec/changes//` -- `--targets` was required inside a workspace -- `--targets` was rejected outside a workspace -- target aliases had to be registered workspace repos -- no files were created inside registered repos during change creation -- `openspec apply --change --repo ` later materialized one target's planning artifacts into a repo-local change -- normal OpenSpec skills were not updated to understand this new structure - -The POC behavior was documented on the POC branch in: - -- `WORKSPACE_POC_PRD.md` -- `WORKSPACE_POC_DECISION_RECORD.md` -- `WORKSPACE_POC_FOLLOWUP_NOTES.md` -- `docs/workspace.md` -- `docs/workspace-demo.md` -- `notes/workspace-poc/phase-05-targeted-change-create/SUMMARY.md` -- `notes/workspace-poc/phase-10-materialization-contract-research/DECISION.md` - -The most important follow-up note was that the POC proved central workspace planning can work, but it also pushed target selection and materialization too early. - -## Skill Complexity Concern - -A major concern is avoiding a combinatorial explosion in generated skills. - -Bad direction: - -```text -workflow x agent x workspace-mode -``` - -For example, if OpenSpec has several workflow skills, several coding-agent-specific instruction variants, and then adds workspace-specific variants on top, the number of skill files grows quickly. - -The preferred architectural pressure is: - -```text -workflow skill + agent affordance profile + OpenSpec-provided context -``` - -Meaning: - -- keep one conceptual `openspec-propose`, `openspec-apply-change`, `openspec-verify-change`, etc. -- keep agent-specific affordances separate, such as how to ask user questions, use todos, use subagents, or attach directories -- have the OpenSpec CLI tell the skill whether it is in repo-local mode or workspace mode, where artifacts live, what areas are relevant, and what action is safe next - -Current skills that would likely need updates if workspace planning proceeds: - -- `openspec-propose` -- `openspec-explore` -- `openspec-apply-change` -- `openspec-verify-change` -- `openspec-archive-change` -- `openspec-bulk-archive-change` later, or keep it repo-local until workspace archive semantics are stable - -The key instruction these skills may need: - -```text -Run `openspec status --change --json`. -Use the returned paths, areas, next steps, and action context. -Do not assume changes live under `openspec/changes/`. -``` - -## `openspec status --json` As Agent Context - -One idea that seemed promising was to use `openspec status --change --json` as the existing CLI surface that tells agents how to act. - -Instead of introducing a separate context command immediately, status could report: - -- whether the current change is repo-local or workspace-scoped -- where the change artifacts live -- what the next steps are -- which areas are affected or unresolved -- which task/spec/design files should be read or updated -- what edit roots are allowed for implementation -- whether apply needs an area selection - -Use simpler language: - -- `nextSteps`: what should happen next -- `actionContext`: paths, areas, and constraints needed to act - -Avoid terms such as "workflow affordances" in user-facing docs or JSON fields. - -## Open Question: Should Targets Exist? - -The session questioned whether users need to explicitly set targets at all. - -Reason to have targets: - -- focused `workspace open --change ` -- per-area status roll-up -- apply or implementation of one area at a time -- unresolved path reporting -- avoiding accidental edits outside the intended area -- future verify/archive behavior - -Reason to avoid explicit target-setting: - -- target selection can be premature before exploration -- it adds bookkeeping for agents -- the plan itself should reveal the affected areas - -Potential direction under discussion: - -```text -derive affected areas from planning artifacts instead of requiring a separate target-setting step -``` - -Possible derivation sources: - -- structured area folders -- structured slice/area folders -- explicit sections in tasks or design -- optional metadata as a cache or confirmation marker, validated against artifacts - -Important caution: Markdown task headings alone are probably too ambiguous to be the only source of truth. - -## Terminology Under Discussion - -The session identified a naming conflict around "slice." - -There are two different concepts: - -```text -delivery breakdown = a smaller planned part of a large change -ownership/execution boundary = a repo, folder, package, or system area affected by the change -``` - -Calling both of these "slices" would make the model confusing. - -Working vocabulary under discussion: - -```text -change = the overall user-visible planning boundary -slice = a delivery increment inside a large change -area = an ownership or execution boundary, such as a repo or folder -unit = one area inside one slice, when implementation needs that precision -artifact = a proposal, design, task list, spec delta, or note attached to a scope -``` - -Examples: - -```text -Small repo-local work: - change only - implicit area = current repo - implicit slice = main - -Small workspace work: - change = add-3ds - areas = contracts, billing, web - slice = main - -Large workspace work: - change = workspace-reimplementation - slices = setup, open, change-planning, apply, verify-archive - areas = cli, agent-skills, docs - units = setup/cli, open/agent-skills, etc. -``` - -This vocabulary is not decided. It is a candidate model for avoiding confusion. - -## Artifact Scoping Problem - -A fixed tree where every change, slice, and area gets every artifact would be too heavy. - -Artifacts should attach to the scope where they are useful. - -Proposal: - -- usually belongs at the change level -- summarizes why the work exists and what outcome is intended -- most areas or slices do not need their own proposal -- a slice-level proposal might exist only if that slice needs independent approval or has a materially distinct "why" - -Design: - -- should live at the broadest scope where the decision applies -- change-level design for cross-area architecture, sequencing, contracts, and tradeoffs -- slice-level design for one independently complex delivery increment -- area-level design for local implementation choices -- unit-level design only when one area within one slice is complex enough - -Tasks: - -- can exist at multiple scopes -- change-level tasks for coordination, decisions, rollout, and review gates -- slice-level tasks for a delivery increment -- area or unit tasks for implementation work an agent can actually complete - -Specs: - -- should live closest to ownership -- area or unit specs for normal behavior owned by a repo or folder -- change-level draft specs only when ownership is unresolved - -This matters because OpenSpec allows users to select which artifacts a change contains. Workspace/slice/area support should not assume every artifact type exists at every scope. - -## Possible File Shapes - -No file shape was decided. - -A future-friendly, fully structured shape might look like: - -```text -changes// - proposal.md - design.md - tasks.md - slices/ - / - tasks.md - design.md - areas/ - / - tasks.md - specs/ -``` - -This may be too much structure for a first implementation. - -A simpler intermediate shape for large changes could be: - -```text -changes// - proposal.md - design.md - tasks.md - slices/ - workspace-open.md - workspace-change-planning.md -``` - -If a slice grows, it could become a folder later. - -For smaller cross-area changes, an area-oriented shape might be enough: - -```text -changes// - proposal.md - design.md - tasks.md - areas/ - api/ - tasks.md - specs/ - web/ - tasks.md - specs/ -``` - -Again, this is exploratory. The point is that artifact scope should be explicit enough for tools and agents, without forcing unnecessary files. - -## Important UX Principles - -The discussion kept returning to these UX principles: - -- workspace mode should preserve the familiar OpenSpec mental model where possible -- users should not need to understand target metadata before they can start planning -- repo visibility is not change commitment -- change creation should not be a transport mechanism just to attach repos -- workspace-specific behavior should not require duplicate skill families -- `/apply` should mean implement, not materialize planning files -- status should help agents know what to do next without hardcoding paths -- large changes should support breakdown into slices, but simple changes should stay simple - -## Open Questions - -- Should `openspec new change ` inside a workspace create only `.openspec.yaml`, or also seed minimal planning artifacts? -- Should `--targets` exist as an optional fast path, compatibility flag, or not at all? -- What structure should represent affected areas without overfitting to repo aliases? -- Should affected areas be derived from folders, metadata, artifact contents, or a combination? -- What is the smallest useful representation of a delivery slice? -- How should artifact schemas express which scopes an artifact type can attach to? -- How should `status --json` expose scoped artifact paths without becoming too large or unstable? -- Which workspace archive semantics are needed before archive skills become workspace-aware? -- How should `apply` select one area and expose allowed edit roots if `/apply` means implementation? diff --git a/openspec/changes/workspace-change-planning/manual-acceptance.md b/openspec/changes/workspace-change-planning/manual-acceptance.md deleted file mode 100644 index 45340d2ef..000000000 --- a/openspec/changes/workspace-change-planning/manual-acceptance.md +++ /dev/null @@ -1,74 +0,0 @@ -# Manual Acceptance Evidence - -Change: `workspace-change-planning` -Date: 2026-05-14 - -## Automated Verification - -Expected: targeted coverage exercises workspace setup, update, config profile apply, workspace change creation, workspace planning context, guarded workflow skills, cross-platform path helpers, docs/completion coverage, and strict OpenSpec validation. - -Actual: - -```bash -pnpm run build -pnpm vitest run test/commands/workspace.test.ts test/commands/artifact-workflow.test.ts test/core/workspace/skills.test.ts test/core/planning-home.test.ts test/core/templates/skill-templates-parity.test.ts -node dist/cli/index.js validate workspace-change-planning --strict -git diff --check -``` - -Observed: -- Build completed successfully. -- Targeted tests passed: workspace command, artifact workflow, workspace skill helpers, planning-home paths, and workflow skill template parity. -- Strict validation reported `Change 'workspace-change-planning' is valid`. -- `git diff --check` reported no whitespace errors. - -## Clean Workspace Rerun - -Expected: from a clean temporary workspace, setup records links, installs skills only in the workspace root, profile changes can be applied to workspace-local skills, workspace change creation stays in the coordination root, status/instructions expose workspace planning context, linked repos remain untouched, and unsupported workflows stop rather than falling back to repo-local edits. - -Actual commands: - -```bash -tmp=$(mktemp -d) -mkdir -p "$tmp/config/openspec" "$tmp/data" "$tmp/api" "$tmp/web" -# write global config: custom profile, commands delivery, workflows ["apply"] -XDG_CONFIG_HOME="$tmp/config" XDG_DATA_HOME="$tmp/data" node dist/cli/index.js workspace setup --no-interactive --json --name final-manual --link api="$tmp/api" --link web="$tmp/web" --tools codex -cd "$workspace" && node dist/cli/index.js workspace doctor --json -cd "$workspace" && node dist/cli/index.js config profile core -cd "$workspace/changes" && node dist/cli/index.js update -cd "$workspace" && node dist/cli/index.js new change cross-workspace-login --goal "Unify login across API and web" --areas api,web -mkdir -p "$workspace/changes/cross-workspace-login/specs/api/login" -# write specs/api/login/spec.md -cd "$workspace" && node dist/cli/index.js status --change cross-workspace-login --json -cd "$workspace" && node dist/cli/index.js instructions specs --change cross-workspace-login --json -find "$tmp/api" "$tmp/web" -maxdepth 3 -print | sort -rg -n 'actionContext.mode: "workspace-planning"|resolvedOutputPath|Do not fall back to repo-local paths|openspec/changes/' "$workspace/.codex/skills" -``` - -Observed: -- Setup JSON reported `profile: custom`, `delivery: commands`, `workflow_ids: ["apply"]`, `selected_agents: ["codex"]`, `skills_only: true`, and the skills-only delivery notice. -- `workspace doctor --json` showed registered links `api` and `web` before any change was created. -- `config profile core` from the workspace printed `Config updated. Run \`openspec workspace update\` to apply it to workspace-local skills.` -- Running `openspec update` from the workspace `changes/` directory redirected to workspace update and refreshed Codex to the core workflows without a false registry warning. -- `new change cross-workspace-login --goal ... --areas api,web` created a workspace change at `changes/cross-workspace-login/` with schema `workspace-planning`. -- Status JSON reported: - - `schemaName: workspace-planning` - - `planningHome.kind: workspace` - - `affectedAreas.known: ["api", "web"]` - - `actionContext.mode: workspace-planning` - - `actionContext.allowedEditRoots: []` - - `artifactPaths.specs.existingOutputPaths` preserved `specs/api/login/spec.md`. -- Instructions JSON for `specs` reported `resolvedOutputPath` as `changes/cross-workspace-login/specs/**/*.md` and preserved the nested existing spec path. -- `find "$tmp/api" "$tmp/web"` showed only the linked folder roots, with no repo-local OpenSpec artifacts created. -- Generated workflow skills contained `actionContext.mode: "workspace-planning"`, `resolvedOutputPath`, and explicit "do not fall back to repo-local paths" guardrails where relevant; no generated skill contained `openspec/changes/`. - -## UX Review - -Expected: command wording should make the project/workspace distinction clear, tell users which command applies profile changes, and avoid implying linked repos are edited during workspace planning. - -Actual: -- Setup/update output says workspace setup installs skills only and workspace command generation is not part of this slice. -- Workspace change creation says "workspace change", prints affected areas, and points to `status` for planning artifacts. -- Status/instructions JSON now provide enough path and action context that a separate artifact-context command is not needed. -- Unsupported workspace apply, sync, archive, bulk archive, and verify workflows are intentionally guarded. They stop and ask for an explicit affected-area implementation workflow instead of editing linked repos. -- Fresh-agent rerun was not available in this session; as fallback, the clean temporary workspace rerun above was performed after the final path and registry fixes. diff --git a/openspec/changes/workspace-change-planning/prd.md b/openspec/changes/workspace-change-planning/prd.md deleted file mode 100644 index 44c6ad47e..000000000 --- a/openspec/changes/workspace-change-planning/prd.md +++ /dev/null @@ -1,275 +0,0 @@ -# Workspace Change Planning PRD - -## Summary - -OpenSpec should help people plan one change across one or more repos or folders without making workspace mode feel like a second product. - -The product should center on a simple idea: a change has one shared plan, and that plan can point to the places where work will happen. - -For a normal repo, the plan and the work usually live in the same repo. For a workspace, the plan lives in the workspace and the work happens in one or more linked repos or folders. - -## Problem - -OpenSpec currently treats a change as something that lives under one repo-local `openspec/changes/` folder. - -That works well for a single repo, but it becomes awkward when the user is planning across several repos or folders. The user needs one place to capture the goal, decisions, affected places, risks, and coordination tasks. The user should not have to create repo-local change folders before the plan is clear. - -The current workspace notes also use terms like targets, slices, scopes, and materialization. Those terms describe implementation details more than the user experience. - -## Product Direction - -OpenSpec should make the familiar change workflow work in more places. - -A user should still think in terms of creating a change, exploring it, writing a proposal, applying the work, verifying it, and archiving it. - -The difference is where the plan lives and where implementation is allowed to happen. - -In a repo, the planning home is the repo. In a workspace, the planning home is the workspace. In both cases, the user is still working with a change. - -## What Already Exists - -OpenSpec already has several pieces needed for this direction. - -Workspaces already have a real root folder and a planning path at `changes/`. - -Workspaces can already link repos or folders, and those links already have stable names. - -Workspace state already separates shared workspace information from local machine paths. - -Workspace open already makes linked repos and folders visible to agents before a change exists. - -Workflow schemas already define plan file types, templates, order, and apply rules. - -`openspec status --change --json` already exists. - -`openspec instructions --json` already exists. - -Generated skills already call `status` and `instructions` in important places. - -This means the first step is mostly connection work, not a full new product. - -## What Is Missing - -OpenSpec does not yet have one shared way to answer this question: given this command, which planning home and change should it use? - -That missing layer is the main gap. - -Workspace links are close to affected areas, but changes do not record or report affected areas yet. - -Plan file paths exist in instructions, but only for repo-local change folders. - -Status output exists, but it does not yet explain the planning home, affected areas, allowed edit roots, or next steps clearly enough for agents. - -Apply exists, but it does not yet pick a work focus before editing. - -Workspace verify and archive are not ready yet. - -## Product Language - -| Product term | Meaning | Avoid using | -| --- | --- | --- | -| Change | The overall goal and plan for a feature, fix, project, or other piece of work. | Change Plan | -| Planning home | The place where the shared plan lives. This can be a repo or a workspace. | Planning Surface | -| Affected area | A repo, folder, package, app, service, or docs site touched by the change. | Target, repo slice | -| Phase | A delivery step inside a larger change. Most changes do not need phases. | Slice | -| Work focus | The one affected area, and optional phase, currently being implemented or verified. | Work Unit | -| Plan file | A proposal, design, task list, spec draft, or note attached to the right part of the change. | Scoped Artifact | -| Next step guidance | The CLI output that tells an agent what to read, where to write, and what not to edit. | Action Context | - -## User Experience - -Maya opens a workspace that links `api`, `web`, and `billing`. - -She creates a change called `add-3ds`. - -OpenSpec creates one shared plan in the workspace. The plan describes the goal, the affected areas, the risks, the decisions, and the coordination tasks. - -No files are written inside `api`, `web`, or `billing` just because the shared plan exists. - -Later, Maya says she wants to apply the change to `api`. - -OpenSpec reads the shared plan, confirms the work focus is `api`, and tells the agent that implementation edits are allowed only inside the `api` checkout. - -When `api` is done, Maya can move the work focus to `web` or `billing`. - -When all affected areas are done, OpenSpec can verify and archive the whole change. - -## Requirements - -### A Change Has One Shared Plan - -A workspace change should capture the shared goal once. - -The shared plan should hold the goal, decisions, affected areas, risks, and coordination tasks. - -Creating the shared plan should not create files inside linked repos or folders. - -### Repo Changes Stay Simple - -A repo-local change should keep working as it does today. - -A repo-local change should be treated as the simplest version of the same model, with one planning home and one affected area. - -Existing `openspec/changes/` folders should keep working. - -### Workspaces Add Reach, Not A New Workflow - -Workspace mode should not create separate workflow commands or separate workflow skills. - -Users should still use the same workflow words: explore, propose, apply, verify, and archive. - -The CLI should provide enough next step guidance that agent skills do not need to hardcode where change files live. - -### Affected Areas Come From The Plan - -OpenSpec should support affected areas as first-class plan information. - -An affected area may be a linked repo, a linked folder, a package, an app, a service, or a docs site. - -Workspace links are a strong starting point for affected areas, because they already provide stable names and local paths. - -The product should not force users to pick all affected areas before they have explored the change. - -### Phases Are Optional - -Most changes should not need phases. - -Large changes may use phases when the work needs to be split into delivery steps. - -Phases should describe delivery order, not repo ownership. - -### Plan Files Attach Where They Make Sense - -A proposal usually belongs to the whole change. - -A design should live at the broadest level where the decision applies. - -Tasks may belong to the whole change, one phase, one affected area, or one work focus. - -Specs should live as close as possible to the place that owns the behavior. - -OpenSpec should not force every affected area or phase to have every kind of plan file. - -### Apply Means Start Work - -Applying a change should mean starting or continuing implementation work. - -In a repo-local change, apply should work through the repo-local tasks. - -In a workspace change, apply should pick or confirm a work focus first. - -After the work focus is selected, OpenSpec should tell the agent which plan files to read and which repo or folder may be edited. - -### Status Should Guide Agents - -`openspec status --change --json` should become the main machine-readable guide for agents. - -It should say where the plan lives, which affected areas exist, which plan files matter, what should happen next, and which edit roots are allowed. - -The JSON should use plain names such as `nextSteps` and `actionContext`. - -Agent skills should use this output instead of assuming that every change lives under `openspec/changes/`. - -## Configuration And Extension - -OpenSpec already has a useful extension point through workflow schemas. - -That should remain the main extension path. - -Schemas should grow carefully so they can describe where plan files may live, what kinds of plan files exist, and how apply should find its tasks. - -Project config should continue to provide shared context and rules. - -Custom validators, custom plan file types, and custom scope kinds should come later, after the first end-to-end workspace flow is working. - -OpenSpec should not add a broad workspace-specific config layer first. - -## Implementation Shape - -The first implementation should keep the current repo-local layout working. - -The first workspace implementation should create changes under the workspace `changes/` folder. - -The system should introduce a small shared planning-home resolver. - -The resolver should answer where the plan lives, where the change lives, which files matter, which affected areas are known, and which edit roots are allowed. - -The system should introduce a richer change model that can record affected areas, optional phases, plan file locations, and the current work focus. - -The first version of this model should stay small. It should not add custom validators, custom scope kinds, deep folder trees, or workspace archive behavior. - -The system should enrich status and instruction output before changing archive behavior. - -Archive and spec sync should come later because they are the highest-risk parts of the current system. - -## Rollout - -### Step 1: Connect Existing Pieces - -Add the small planning-home resolver. - -Keep repo-local behavior unchanged. - -Use the existing workspace root, workspace links, workflow schema, status command, and instructions command. - -The goal of this step is to stop spreading workspace checks through individual commands. - -### Step 2: Product Model And Status - -Update the change model and status JSON while keeping current repo-local storage. - -The goal of this step is to let agents stop guessing paths. - -Status should return the planning home, change path, plan file paths, affected areas when known, next steps, and allowed edit roots when relevant. - -### Step 3: Workspace Change Creation - -Allow a workspace to create one shared change under `changes/`. - -This step should not write into linked repos or folders. - -### Step 4: Affected Areas - -Use workspace links as the first source of affected areas. - -Let the plan confirm or refine which linked repos or folders are affected. - -### Step 5: Work Focus Apply - -Teach apply to select one affected area before implementation starts. - -The CLI should return the allowed edit root for that selected work focus. - -### Step 6: Scoped Plan Files - -Allow selected plan files to live at the level where they make sense. - -Start with simple paths before introducing a deep folder tree. - -### Step 7: Verify And Archive - -Add per-area verification and final whole-change archive once planning and apply are stable. - -## Risks - -The biggest risk is changing archive too early. - -Archive currently assumes one repo-local change and one repo-local specs folder. Workspace archive needs a clearer model for partial completion, per-area progress, and final completion. - -The second biggest risk is exposing too many new terms to users. - -The product should keep the visible workflow simple and push richer structure into CLI JSON for agents. - -The third biggest risk is overbuilding the file tree. - -OpenSpec should avoid creating a folder for every possible phase and affected area unless there is a real plan file to put there. - -## Decision - -The change should be reframed from workspace-specific commands to shared change planning. - -Workspace support should become one use case of a more general model where a change has a planning home, affected areas, optional phases, plan files, and one current work focus. - -The user-facing workflow should stay familiar. - -The internal model should become richer enough for agents to act safely without hardcoded paths. diff --git a/openspec/changes/workspace-change-planning/review-candidate-requirements.html b/openspec/changes/workspace-change-planning/review-candidate-requirements.html deleted file mode 100644 index 67417d726..000000000 --- a/openspec/changes/workspace-change-planning/review-candidate-requirements.html +++ /dev/null @@ -1,868 +0,0 @@ - - - - -Review: candidate-requirements.md - - - -
          -
          -

          Review: candidate-requirements.md

          -
          openspec/changes/workspace-change-planning/candidate-requirements.md
          -
          -
          - 0 total - 0 pending - 0 approved - 0 rejected -
          -
          - -
          -
          -
          -
          - - - - -
          -
          -
          -
          - -
          -
          -

          Prompt output

          - -
          -
          Approve suggestions or add comments to build a prompt.
          -
          - - - - diff --git a/openspec/changes/workspace-change-planning/tasks.md b/openspec/changes/workspace-change-planning/tasks.md index 0d607c2c3..6710c2d2c 100644 --- a/openspec/changes/workspace-change-planning/tasks.md +++ b/openspec/changes/workspace-change-planning/tasks.md @@ -108,3 +108,26 @@ User-testable outcome: A user or reviewer can run the full manual checklist from - [x] 7.15 Complete a final UX review across the whole workflow and record any follow-up fixes or intentional deferrals. - [x] 7.16 Before implementation sign-off, record the manual commands or interaction paths, expected observations, and actual observations for each phase. - [x] 7.17 Have a separate reviewer or fresh agent context rerun the manual acceptance and UX checklist when available; otherwise rerun it from a clean temporary workspace and report the evidence. + +## Verification Evidence + +Completion evidence was recorded on 2026-05-14. + +Automated checks: + +```bash +pnpm run build +pnpm vitest run test/commands/workspace.test.ts test/commands/artifact-workflow.test.ts test/core/workspace/skills.test.ts test/core/planning-home.test.ts test/core/templates/skill-templates-parity.test.ts +node dist/cli/index.js validate workspace-change-planning --strict +git diff --check +``` + +Clean workspace rerun covered non-interactive workspace setup, workspace doctor, config profile update guidance, workspace update redirection, workspace change creation with `--areas api,web`, status/instructions JSON for nested workspace specs, linked repo cleanliness, and guarded unsupported workflow skills. + +Observed results: + +- Build, targeted tests, strict validation, and whitespace checks passed. +- Workspace setup/update generated skills only in the workspace root and left linked repos untouched. +- Workspace change creation used schema `workspace-planning`, reported affected areas `api` and `web`, preserved nested `specs/api/login/spec.md`, and kept `actionContext.allowedEditRoots` empty during planning. +- Generated workflow skills used CLI-reported paths and workspace guards rather than hardcoded `openspec/changes/` paths. +- Fresh-agent rerun was not available; the clean temporary workspace rerun served as the fallback independent acceptance pass. From 2063d2386c87ad02c836fddbd405c89dd3d52b69 Mon Sep 17 00:00:00 2001 From: TabishB Date: Thu, 14 May 2026 23:07:15 +1000 Subject: [PATCH 12/14] Archive workspace change planning --- WORKSPACE_REIMPLEMENTATION_START_HERE.md | 5 +- .../design.md | 0 .../proposal.md | 0 .../specs/artifact-graph/spec.md | 0 .../specs/change-creation/spec.md | 0 .../specs/cli-artifact-workflow/spec.md | 0 .../specs/cli-config/spec.md | 0 .../specs/cli-update/spec.md | 0 .../specs/openspec-conventions/spec.md | 0 .../specs/schema-resolution/spec.md | 0 .../specs/workspace-change-planning/spec.md | 0 .../specs/workspace-links/spec.md | 0 .../tasks.md | 0 .../workspace-agent-guidance/.openspec.yaml | 2 + .../workspace-agent-guidance/design.md | 69 ++++++++ .../workspace-agent-guidance/proposal.md | 33 ++++ .../specs/change-creation/spec.md | 15 ++ .../specs/cli-artifact-workflow/spec.md | 30 ++++ .../specs/workspace-links/spec.md | 21 +++ .../changes/workspace-agent-guidance/tasks.md | 34 ++++ .../README.md | 7 +- .../proposal.md | 4 +- openspec/specs/artifact-graph/spec.md | 36 +++- openspec/specs/change-creation/spec.md | 41 +++++ openspec/specs/cli-artifact-workflow/spec.md | 100 ++++++++++- openspec/specs/cli-config/spec.md | 54 ++++++ openspec/specs/cli-update/spec.md | 20 +++ openspec/specs/openspec-conventions/spec.md | 31 ++++ openspec/specs/schema-resolution/spec.md | 25 ++- .../specs/workspace-change-planning/spec.md | 70 ++++++++ openspec/specs/workspace-links/spec.md | 163 +++++++++++++++++- 31 files changed, 751 insertions(+), 9 deletions(-) rename openspec/changes/{workspace-change-planning => archive/2026-05-14-workspace-change-planning}/design.md (100%) rename openspec/changes/{workspace-change-planning => archive/2026-05-14-workspace-change-planning}/proposal.md (100%) rename openspec/changes/{workspace-change-planning => archive/2026-05-14-workspace-change-planning}/specs/artifact-graph/spec.md (100%) rename openspec/changes/{workspace-change-planning => archive/2026-05-14-workspace-change-planning}/specs/change-creation/spec.md (100%) rename openspec/changes/{workspace-change-planning => archive/2026-05-14-workspace-change-planning}/specs/cli-artifact-workflow/spec.md (100%) rename openspec/changes/{workspace-change-planning => archive/2026-05-14-workspace-change-planning}/specs/cli-config/spec.md (100%) rename openspec/changes/{workspace-change-planning => archive/2026-05-14-workspace-change-planning}/specs/cli-update/spec.md (100%) rename openspec/changes/{workspace-change-planning => archive/2026-05-14-workspace-change-planning}/specs/openspec-conventions/spec.md (100%) rename openspec/changes/{workspace-change-planning => archive/2026-05-14-workspace-change-planning}/specs/schema-resolution/spec.md (100%) rename openspec/changes/{workspace-change-planning => archive/2026-05-14-workspace-change-planning}/specs/workspace-change-planning/spec.md (100%) rename openspec/changes/{workspace-change-planning => archive/2026-05-14-workspace-change-planning}/specs/workspace-links/spec.md (100%) rename openspec/changes/{workspace-change-planning => archive/2026-05-14-workspace-change-planning}/tasks.md (100%) create mode 100644 openspec/changes/workspace-agent-guidance/.openspec.yaml create mode 100644 openspec/changes/workspace-agent-guidance/design.md create mode 100644 openspec/changes/workspace-agent-guidance/proposal.md create mode 100644 openspec/changes/workspace-agent-guidance/specs/change-creation/spec.md create mode 100644 openspec/changes/workspace-agent-guidance/specs/cli-artifact-workflow/spec.md create mode 100644 openspec/changes/workspace-agent-guidance/specs/workspace-links/spec.md create mode 100644 openspec/changes/workspace-agent-guidance/tasks.md create mode 100644 openspec/specs/workspace-change-planning/spec.md diff --git a/WORKSPACE_REIMPLEMENTATION_START_HERE.md b/WORKSPACE_REIMPLEMENTATION_START_HERE.md index b0066504f..6f3c8f7e3 100644 --- a/WORKSPACE_REIMPLEMENTATION_START_HERE.md +++ b/WORKSPACE_REIMPLEMENTATION_START_HERE.md @@ -39,8 +39,9 @@ Implement these flat OpenSpec changes in order: 2. `workspace-create-and-register-repos` 3. `workspace-open-agent-context` 4. `workspace-change-planning` -5. `workspace-apply-repo-slice` -6. `workspace-verify-and-archive` +5. `workspace-agent-guidance` +6. `workspace-apply-repo-slice` +7. `workspace-verify-and-archive` `workspace-reimplementation-roadmap` is the continuity and reference container for the plan. diff --git a/openspec/changes/workspace-change-planning/design.md b/openspec/changes/archive/2026-05-14-workspace-change-planning/design.md similarity index 100% rename from openspec/changes/workspace-change-planning/design.md rename to openspec/changes/archive/2026-05-14-workspace-change-planning/design.md diff --git a/openspec/changes/workspace-change-planning/proposal.md b/openspec/changes/archive/2026-05-14-workspace-change-planning/proposal.md similarity index 100% rename from openspec/changes/workspace-change-planning/proposal.md rename to openspec/changes/archive/2026-05-14-workspace-change-planning/proposal.md diff --git a/openspec/changes/workspace-change-planning/specs/artifact-graph/spec.md b/openspec/changes/archive/2026-05-14-workspace-change-planning/specs/artifact-graph/spec.md similarity index 100% rename from openspec/changes/workspace-change-planning/specs/artifact-graph/spec.md rename to openspec/changes/archive/2026-05-14-workspace-change-planning/specs/artifact-graph/spec.md diff --git a/openspec/changes/workspace-change-planning/specs/change-creation/spec.md b/openspec/changes/archive/2026-05-14-workspace-change-planning/specs/change-creation/spec.md similarity index 100% rename from openspec/changes/workspace-change-planning/specs/change-creation/spec.md rename to openspec/changes/archive/2026-05-14-workspace-change-planning/specs/change-creation/spec.md diff --git a/openspec/changes/workspace-change-planning/specs/cli-artifact-workflow/spec.md b/openspec/changes/archive/2026-05-14-workspace-change-planning/specs/cli-artifact-workflow/spec.md similarity index 100% rename from openspec/changes/workspace-change-planning/specs/cli-artifact-workflow/spec.md rename to openspec/changes/archive/2026-05-14-workspace-change-planning/specs/cli-artifact-workflow/spec.md diff --git a/openspec/changes/workspace-change-planning/specs/cli-config/spec.md b/openspec/changes/archive/2026-05-14-workspace-change-planning/specs/cli-config/spec.md similarity index 100% rename from openspec/changes/workspace-change-planning/specs/cli-config/spec.md rename to openspec/changes/archive/2026-05-14-workspace-change-planning/specs/cli-config/spec.md diff --git a/openspec/changes/workspace-change-planning/specs/cli-update/spec.md b/openspec/changes/archive/2026-05-14-workspace-change-planning/specs/cli-update/spec.md similarity index 100% rename from openspec/changes/workspace-change-planning/specs/cli-update/spec.md rename to openspec/changes/archive/2026-05-14-workspace-change-planning/specs/cli-update/spec.md diff --git a/openspec/changes/workspace-change-planning/specs/openspec-conventions/spec.md b/openspec/changes/archive/2026-05-14-workspace-change-planning/specs/openspec-conventions/spec.md similarity index 100% rename from openspec/changes/workspace-change-planning/specs/openspec-conventions/spec.md rename to openspec/changes/archive/2026-05-14-workspace-change-planning/specs/openspec-conventions/spec.md diff --git a/openspec/changes/workspace-change-planning/specs/schema-resolution/spec.md b/openspec/changes/archive/2026-05-14-workspace-change-planning/specs/schema-resolution/spec.md similarity index 100% rename from openspec/changes/workspace-change-planning/specs/schema-resolution/spec.md rename to openspec/changes/archive/2026-05-14-workspace-change-planning/specs/schema-resolution/spec.md diff --git a/openspec/changes/workspace-change-planning/specs/workspace-change-planning/spec.md b/openspec/changes/archive/2026-05-14-workspace-change-planning/specs/workspace-change-planning/spec.md similarity index 100% rename from openspec/changes/workspace-change-planning/specs/workspace-change-planning/spec.md rename to openspec/changes/archive/2026-05-14-workspace-change-planning/specs/workspace-change-planning/spec.md diff --git a/openspec/changes/workspace-change-planning/specs/workspace-links/spec.md b/openspec/changes/archive/2026-05-14-workspace-change-planning/specs/workspace-links/spec.md similarity index 100% rename from openspec/changes/workspace-change-planning/specs/workspace-links/spec.md rename to openspec/changes/archive/2026-05-14-workspace-change-planning/specs/workspace-links/spec.md diff --git a/openspec/changes/workspace-change-planning/tasks.md b/openspec/changes/archive/2026-05-14-workspace-change-planning/tasks.md similarity index 100% rename from openspec/changes/workspace-change-planning/tasks.md rename to openspec/changes/archive/2026-05-14-workspace-change-planning/tasks.md diff --git a/openspec/changes/workspace-agent-guidance/.openspec.yaml b/openspec/changes/workspace-agent-guidance/.openspec.yaml new file mode 100644 index 000000000..66dd08a95 --- /dev/null +++ b/openspec/changes/workspace-agent-guidance/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-14 diff --git a/openspec/changes/workspace-agent-guidance/design.md b/openspec/changes/workspace-agent-guidance/design.md new file mode 100644 index 000000000..7cc2e7622 --- /dev/null +++ b/openspec/changes/workspace-agent-guidance/design.md @@ -0,0 +1,69 @@ +## Context + +`workspace-change-planning` deliberately kept workflow skills generic and path-agnostic. That was the right first step: the same skill can now ask the CLI where a change lives and avoid hardcoded `openspec/changes/` assumptions. + +The next problem is intent. The generated skills do not yet behave differently when they are installed into a workspace root. In particular, `openspec-new-change`, `openspec-propose`, and `openspec-ff-change` still create changes with: + +```bash +openspec new change "" +``` + +That works, but it loses the workspace-specific metadata this slice just introduced. It also relies on general schema instructions to teach workspace planning after the change is created, instead of telling the agent how to approach workspace planning up front. + +## Goals / Non-Goals + +**Goals:** +- Give workspace-installed agents explicit workspace planning guidance. +- Keep the guidance layered on top of existing workflow skills instead of creating an unrelated workflow family. +- Teach change-starting skills to use `--goal` for the product goal when creating workspace changes. +- Teach change-starting skills to use `--areas` only for known registered workspace link names. +- Preserve the ability to create a workspace change before all affected areas are known. +- Keep linked repos and folders read-only during planning unless an explicit implementation workflow provides an allowed edit root. + +**Non-Goals:** +- Implement workspace apply, verify, or archive semantics. +- Add workspace slash command generation. +- Require agents to fully infer affected areas before creating a proposal. +- Add another required area manifest outside normal workspace planning artifacts. +- Replace the current `status --json` and `instructions --json` context contract. + +## Decisions + +### Layer Workspace Guidance Onto Existing Skills + +Workspace setup/update should continue selecting normal workflow skills from the active global profile. The workspace-specific part should be an installed guidance layer or generation transform that augments those skills when they are written into a workspace root. + +Alternative considered: create separate `openspec-workspace-*` skills. That would make workspace behavior obvious, but it risks duplicating every workflow and making repo-local and workspace flows diverge too early. + +### Make Change-Starting Skills Workspace-Aware + +The `new`, `propose`, and `ff` workflow skills should detect workspace context before creating a change. In workspace context, they should derive: + +- a kebab-case change name +- a concise product goal for `--goal` +- a list of confident affected areas for `--areas`, using registered workspace link names only + +If areas remain unclear, the skills should omit `--areas`, create the workspace change, and keep the unresolved area question in the proposal/specs/tasks. + +Alternative considered: always omit `--areas` and rely on artifact content. That preserves flexibility but wastes the affected-area metadata and makes status less helpful immediately after creation. + +### Keep Goal Capture Lightweight + +The goal captured by `--goal` should remain lightweight metadata, not a substitute for `proposal.md`. The generated proposal should still explain the goal in normal product language. + +Alternative considered: have `--goal` prefill proposal content. That may be useful later, but this change should first make the agent use the existing flag consistently. + +### Treat Metadata Flags As Workspace-Scoped + +`--areas` is already rejected outside workspace-scoped change creation. `--goal` should either follow that same workspace-scoped rule or the CLI should clearly document any repo-local meaning before keeping it generic. The preferred direction is to make both flags workspace planning metadata so users and skills have one clear mental model. + +### Keep Guards For Unsupported Workspace Workflows + +Apply, verify, archive, sync, and bulk archive should continue inspecting `actionContext`. If workspace status reports no `allowedEditRoots`, skills should stop before implementation edits. This change should improve planning guidance without loosening those safety boundaries. + +## Risks / Trade-offs + +- Skill content can become too conditional -> keep workspace-specific guidance short and action-oriented. +- Agents may over-infer affected areas -> require `--areas` only for confident registered link names. +- `--goal` repo-local behavior may already be observable -> decide whether to reject it outside workspaces or document it before implementation. +- Duplicated instructions across skills can drift -> use a shared helper or generation transform where practical. diff --git a/openspec/changes/workspace-agent-guidance/proposal.md b/openspec/changes/workspace-agent-guidance/proposal.md new file mode 100644 index 000000000..d86a2c617 --- /dev/null +++ b/openspec/changes/workspace-agent-guidance/proposal.md @@ -0,0 +1,33 @@ +## Why + +Workspace change planning can now create a shared planning home and install OpenSpec workflow skills into that home, but the installed skills still behave mostly like repo-local workflow skills. They are path-aware and guarded after a change exists, yet they do not give agents a strong workspace-native operating model before and during planning. + +This leaves a gap right after `workspace-change-planning`: an agent opened in a workspace should know how to explore linked repos, create a workspace change with the captured product goal, use known affected areas, and keep linked repos read-only until an explicit implementation workflow selects an allowed edit root. + +## What Changes + +- Add workspace-native guidance to workspace-local agent skill installation and refresh. +- Teach change-starting workflow skills how to recognize workspace planning context. +- In workspace planning homes, have generated skills pass `--goal` and known `--areas` when creating workspace changes. +- Keep unresolved affected areas visible when the agent cannot determine them confidently. +- Clarify that workspace planning metadata flags are workspace-scoped and should not be treated as generic repo-local change metadata. +- Preserve the existing path-agnostic status/instructions pattern and unsupported-workflow guards. + +## Capabilities + +### New Capabilities + +- + +### Modified Capabilities + +- `workspace-links`: Workspace-local skill installation includes workspace-native agent guidance. +- `cli-artifact-workflow`: Generated workflow skills start workspace changes with workspace planning context. +- `change-creation`: Workspace planning metadata flags are treated as workspace-scoped change creation inputs. + +## Impact + +- Skill template content for workspace setup/update. +- Workspace-local skill generation and update behavior. +- Tests for generated skill content in workspace mode. +- CLI help/docs if flag semantics or workspace skill behavior become clearer to users. diff --git a/openspec/changes/workspace-agent-guidance/specs/change-creation/spec.md b/openspec/changes/workspace-agent-guidance/specs/change-creation/spec.md new file mode 100644 index 000000000..74e04c730 --- /dev/null +++ b/openspec/changes/workspace-agent-guidance/specs/change-creation/spec.md @@ -0,0 +1,15 @@ +## ADDED Requirements + +### Requirement: Workspace planning metadata flags +OpenSpec SHALL treat workspace planning metadata flags as inputs for workspace-scoped change creation. + +#### Scenario: Storing a workspace product goal +- **GIVEN** the command runs from an OpenSpec workspace planning home +- **WHEN** the user creates a change with `--goal ` +- **THEN** OpenSpec SHALL store the text as workspace change planning metadata +- **AND** it SHALL not treat the metadata value as a replacement for `proposal.md` + +#### Scenario: Rejecting metadata flags with unclear scope +- **WHEN** a metadata flag is intended only for workspace planning +- **THEN** OpenSpec SHALL either reject that flag outside workspace-scoped change creation or document its repo-local behavior explicitly +- **AND** generated workflow skills SHALL follow the documented scope diff --git a/openspec/changes/workspace-agent-guidance/specs/cli-artifact-workflow/spec.md b/openspec/changes/workspace-agent-guidance/specs/cli-artifact-workflow/spec.md new file mode 100644 index 000000000..0b40dfcb8 --- /dev/null +++ b/openspec/changes/workspace-agent-guidance/specs/cli-artifact-workflow/spec.md @@ -0,0 +1,30 @@ +## ADDED Requirements + +### Requirement: Workspace-aware change-starting skills +Generated change-starting workflow skills SHALL create workspace changes with workspace planning context when they are operating from a workspace planning home. + +#### Scenario: Capturing the product goal when starting a workspace change +- **GIVEN** an agent is using a generated change-starting skill from a workspace planning home +- **WHEN** the agent creates a workspace change from the user's product goal +- **THEN** the skill guidance SHALL instruct the agent to pass the concise product goal with `--goal` +- **AND** it SHALL still create or update `proposal.md` as the human-readable planning artifact + +#### Scenario: Passing known affected areas +- **GIVEN** an agent is using a generated change-starting skill from a workspace planning home +- **AND** the agent can identify affected areas that match registered workspace link names +- **WHEN** the agent creates the workspace change +- **THEN** the skill guidance SHALL instruct the agent to pass those link names with `--areas` +- **AND** it SHALL not pass exploratory or uncertain area names as `--areas` + +#### Scenario: Deferring unresolved affected areas +- **GIVEN** an agent is using a generated change-starting skill from a workspace planning home +- **AND** affected areas are unclear +- **WHEN** the agent creates the workspace change +- **THEN** the skill guidance SHALL allow the agent to omit `--areas` +- **AND** it SHALL tell the agent to keep unresolved affected-area questions visible in workspace planning artifacts + +#### Scenario: Preserving repo-local change creation +- **GIVEN** an agent is using a generated change-starting skill from a repo-local planning home +- **WHEN** the agent creates a new change +- **THEN** the skill guidance SHALL preserve normal repo-local change creation behavior +- **AND** it SHALL not instruct the agent to use workspace-only metadata flags for repo-local changes diff --git a/openspec/changes/workspace-agent-guidance/specs/workspace-links/spec.md b/openspec/changes/workspace-agent-guidance/specs/workspace-links/spec.md new file mode 100644 index 000000000..7af6524a6 --- /dev/null +++ b/openspec/changes/workspace-agent-guidance/specs/workspace-links/spec.md @@ -0,0 +1,21 @@ +## ADDED Requirements + +### Requirement: Workspace-local skill guidance +Workspace-local OpenSpec skills SHALL include guidance that helps agents operate from the workspace planning home. + +#### Scenario: Installing workspace guidance with skills +- **WHEN** workspace setup or workspace update installs OpenSpec skills into a workspace root +- **THEN** the installed skills SHALL tell agents they are operating from a workspace planning home +- **AND** they SHALL describe linked repos and folders as exploration context during planning +- **AND** they SHALL preserve the rule that implementation edits require an explicit implementation workflow and allowed edit root + +#### Scenario: Keeping profile workflow selection +- **GIVEN** global config resolves to a workflow profile +- **WHEN** workspace setup or workspace update installs workspace-local skills +- **THEN** OpenSpec SHALL continue installing the workflows selected by the profile +- **AND** it SHALL layer workspace guidance onto those workflow skills without requiring a separate workspace workflow family + +#### Scenario: Refreshing workspace guidance +- **WHEN** workspace update refreshes existing workspace-local skills +- **THEN** OpenSpec SHALL refresh the workspace guidance along with the selected workflow skill content +- **AND** it SHALL continue removing only known OpenSpec-managed workflow skill directories diff --git a/openspec/changes/workspace-agent-guidance/tasks.md b/openspec/changes/workspace-agent-guidance/tasks.md new file mode 100644 index 000000000..e3d48c352 --- /dev/null +++ b/openspec/changes/workspace-agent-guidance/tasks.md @@ -0,0 +1,34 @@ +## 1. Workspace Guidance Model + +- [ ] 1.1 Decide whether workspace guidance is injected through a generation transform, a small shared template block, or a dedicated workspace guidance skill. +- [ ] 1.2 Keep workspace setup/update installing profile-selected workflow skills rather than creating a separate workspace workflow family. +- [ ] 1.3 Define the workspace-mode guidance agents need before creating a change: inspect links, keep implementation read-only, identify likely affected areas, and preserve unresolved questions. + +## 2. Change-Starting Skill Updates + +- [ ] 2.1 Update `openspec-new-change` skill guidance for workspace planning homes. +- [ ] 2.2 Update `openspec-propose` skill guidance for workspace planning homes. +- [ ] 2.3 Update `openspec-ff-change` skill guidance for workspace planning homes. +- [ ] 2.4 In workspace mode, instruct agents to pass `--goal ""` when creating the change. +- [ ] 2.5 In workspace mode, instruct agents to pass `--areas ` only for known registered workspace link names. +- [ ] 2.6 In workspace mode, instruct agents to omit `--areas` and record unresolved area questions in artifacts when areas are unclear. + +## 3. Flag Semantics + +- [ ] 3.1 Decide whether `--goal` should be rejected outside workspace-scoped change creation or explicitly documented for repo-local changes. +- [ ] 3.2 Align CLI help, tests, and generated skill instructions with the chosen `--goal` semantics. +- [ ] 3.3 Add tests for `--goal` and `--areas` behavior from workspace and repo-local planning homes. + +## 4. Workspace Skill Verification + +- [ ] 4.1 Add tests that workspace setup writes skills with workspace-native planning guidance. +- [ ] 4.2 Add tests that workspace update refreshes the workspace-native guidance. +- [ ] 4.3 Add tests that generated change-starting skills include the `--goal` / `--areas` workspace creation path. +- [ ] 4.4 Verify unsupported workspace workflows still guard against repo-local fallback edits. + +## 5. Documentation And Review + +- [ ] 5.1 Update CLI/docs text where users need to understand workspace-local skill behavior. +- [ ] 5.2 Run targeted tests for skill generation, workspace setup/update, and artifact workflow templates. +- [ ] 5.3 Run `openspec validate workspace-agent-guidance --strict`. +- [ ] 5.4 Manually inspect generated workspace-local skills from a clean workspace and record the observed guidance. diff --git a/openspec/changes/workspace-reimplementation-roadmap/README.md b/openspec/changes/workspace-reimplementation-roadmap/README.md index b53925c7e..3708e4103 100644 --- a/openspec/changes/workspace-reimplementation-roadmap/README.md +++ b/openspec/changes/workspace-reimplementation-roadmap/README.md @@ -33,8 +33,9 @@ Implement the flat sibling changes in this order: 2. `workspace-create-and-register-repos` 3. `workspace-open-agent-context` 4. `workspace-change-planning` -5. `workspace-apply-repo-slice` -6. `workspace-verify-and-archive` +5. `workspace-agent-guidance` +6. `workspace-apply-repo-slice` +7. `workspace-verify-and-archive` OpenSpec currently discovers active changes as immediate directories under `openspec/changes/`, and change names are kebab-case identifiers. Keep these changes as flat siblings until formal change-stacking metadata is available. @@ -48,6 +49,8 @@ OpenSpec currently discovers active changes as immediate directories under `open `workspace-change-planning` creates the workspace-level planning commitment and identifies target repo slices. +`workspace-agent-guidance` makes workspace-local workflow skills use the planning model deliberately: inspect linked context, seed workspace changes with goal and known affected areas, and preserve linked repos as read-only planning context until apply selects an edit root. + `workspace-apply-repo-slice` treats apply as implementation of one selected repo slice, not materialization of workspace planning files. `workspace-verify-and-archive` makes cross-repo progress visible and separates partial repo completion from final workspace completion. diff --git a/openspec/changes/workspace-reimplementation-roadmap/proposal.md b/openspec/changes/workspace-reimplementation-roadmap/proposal.md index d943b2f07..028a8b823 100644 --- a/openspec/changes/workspace-reimplementation-roadmap/proposal.md +++ b/openspec/changes/workspace-reimplementation-roadmap/proposal.md @@ -20,6 +20,7 @@ Add a lightweight roadmap for reimplementing workspace support as a stack of fla - `workspace-create-and-register-repos` - `workspace-open-agent-context` - `workspace-change-planning` +- `workspace-agent-guidance` - `workspace-apply-repo-slice` - `workspace-verify-and-archive` @@ -32,6 +33,7 @@ workspace-foundation -> workspace-create-and-register-repos -> workspace-open-agent-context -> workspace-change-planning + -> workspace-agent-guidance -> workspace-apply-repo-slice -> workspace-verify-and-archive ``` @@ -49,5 +51,5 @@ workspace-foundation ## Impact - Planning only in this PR. -- Future changes will affect workspace metadata, workspace CLI flows, agent context construction, workspace change planning, repo-slice application, verification, and archive behavior. +- Future changes will affect workspace metadata, workspace CLI flows, agent context construction, workspace change planning, workspace-local agent guidance, repo-slice application, verification, and archive behavior. - No runtime behavior changes are introduced by this roadmap proposal. diff --git a/openspec/specs/artifact-graph/spec.md b/openspec/specs/artifact-graph/spec.md index 4f6fd82b5..fb9627ca3 100644 --- a/openspec/specs/artifact-graph/spec.md +++ b/openspec/specs/artifact-graph/spec.md @@ -2,7 +2,6 @@ ## Purpose Define the artifact graph model, dependency validation, and completion-state logic used by schema-driven workflows. - ## Requirements ### Requirement: Schema Loading The system SHALL load artifact graph definitions from YAML schema files within schema directories. @@ -129,3 +128,38 @@ The system SHALL support self-contained schema directories with co-located templ - **WHEN** listing schemas - **THEN** the system returns schema names from both user and package directories +### Requirement: Workspace planning schema +The artifact graph SHALL provide a built-in workspace planning schema for workspace-scoped changes. + +#### Scenario: Built-in workspace planning schema is available +- **WHEN** schemas are resolved from package built-ins +- **THEN** a schema named `workspace-planning` SHALL be available +- **AND** it SHALL describe the artifact structure for workspace-scoped planning + +#### Scenario: Workspace planning schema artifacts +- **WHEN** the `workspace-planning` schema is loaded +- **THEN** it SHALL include the normal planning artifacts for a shared proposal, workspace-scoped specs, cross-area design, and coordination tasks +- **AND** it SHALL not require an additional area manifest outside those normal planning artifacts + +#### Scenario: Workspace planning schema supports nested specs +- **WHEN** the `workspace-planning` schema defines its specs artifact +- **THEN** the specs artifact SHALL resolve workspace-scoped spec files under `specs/**/*.md` +- **AND** schema guidance SHALL describe `specs///spec.md` as the default convention for area-specific requirements + +#### Scenario: Workspace planning schema templates +- **WHEN** artifact instructions are requested for the `workspace-planning` schema +- **THEN** the schema SHALL provide templates that guide agents to write workspace-level planning content +- **AND** those templates SHALL avoid instructing agents to create repo-local implementation artifacts +- **AND** specs instructions SHALL support organizing area-specific requirements under workspace-scoped `specs/` paths + +#### Scenario: Workspace nested spec paths stay workspace-scoped +- **GIVEN** a workspace change has spec files under `specs///spec.md` +- **WHEN** OpenSpec reports status or artifact instructions for the workspace change +- **THEN** it SHALL preserve the concrete nested workspace spec paths +- **AND** it SHALL not treat those files as repo-local specs to sync or archive without an explicit affected-area implementation context + +#### Scenario: Workspace planning apply readiness +- **WHEN** the `workspace-planning` schema defines apply readiness +- **THEN** it SHALL require coordination tasks before implementation begins +- **AND** the apply guidance SHALL direct agents to select an affected area before making implementation edits + diff --git a/openspec/specs/change-creation/spec.md b/openspec/specs/change-creation/spec.md index 3e85719f5..1e2cb1a9d 100644 --- a/openspec/specs/change-creation/spec.md +++ b/openspec/specs/change-creation/spec.md @@ -65,3 +65,44 @@ The system SHALL validate change names follow kebab-case conventions. - **WHEN** a change name like `add--auth` is validated - **THEN** validation returns `{ valid: false, error: "..." }` +### Requirement: Workspace-aware change creation +Change creation SHALL support both repo-local and workspace planning homes. + +#### Scenario: Creating a change from a workspace root +- **GIVEN** the command runs from an OpenSpec workspace root +- **WHEN** the user creates a new change +- **THEN** OpenSpec SHALL create the change under the workspace planning path +- **AND** it SHALL not create the change under a linked repo's `openspec/changes/` directory +- **AND** it SHALL use the `workspace-planning` schema when no explicit schema is provided + +#### Scenario: Creating a change from inside a workspace +- **GIVEN** the command runs from a subdirectory of an OpenSpec workspace planning home +- **WHEN** the user creates a new change +- **THEN** OpenSpec SHALL resolve the current workspace as the planning home +- **AND** it SHALL create the change under that workspace's planning path +- **AND** it SHALL use the `workspace-planning` schema when no explicit schema is provided + +#### Scenario: Creating a change from inside a linked repo +- **GIVEN** a repo or folder is registered as a workspace link +- **AND** the command runs from inside that linked repo or folder rather than from the workspace planning home +- **WHEN** the user creates a new change without explicitly selecting a workspace +- **THEN** OpenSpec SHALL preserve repo-local change creation behavior for that location +- **AND** it SHALL not create a workspace-scoped change merely because the location is registered as a workspace link + +#### Scenario: Preserving repo-local change creation +- **GIVEN** the command runs outside an OpenSpec workspace +- **WHEN** the user creates a new change in a repo-local OpenSpec project +- **THEN** OpenSpec SHALL continue to create the change under `openspec/changes/` + +#### Scenario: Rejecting invalid workspace affected areas +- **GIVEN** a workspace change creation request includes affected area names +- **WHEN** one or more names are not registered workspace links +- **THEN** OpenSpec SHALL reject those invalid affected areas +- **AND** it SHALL list the valid workspace link names + +#### Scenario: Creating without affected areas +- **GIVEN** the user is still exploring scope +- **WHEN** the user creates a workspace change without affected areas +- **THEN** OpenSpec SHALL create the workspace change +- **AND** it SHALL allow affected areas to be identified later + diff --git a/openspec/specs/cli-artifact-workflow/spec.md b/openspec/specs/cli-artifact-workflow/spec.md index 6f2e38743..60e43295d 100644 --- a/openspec/specs/cli-artifact-workflow/spec.md +++ b/openspec/specs/cli-artifact-workflow/spec.md @@ -2,7 +2,6 @@ ## Purpose Define artifact workflow CLI behavior (`status`, `instructions`, `templates`, and setup flows) for scaffolded and active changes. - ## Requirements ### Requirement: Status Command @@ -298,3 +297,102 @@ The setup command SHALL display clear output about what was generated. - **WHEN** command generation is skipped due to missing adapter - **THEN** output includes message: "Command generation skipped - no adapter for " + +### Requirement: Status JSON provides planning context +The status command SHALL provide machine-readable planning context for repo-local and workspace changes. + +#### Scenario: Reporting planning home +- **WHEN** a user runs `openspec status --change --json` +- **THEN** the output SHALL identify whether the change is repo-local or workspace-scoped +- **AND** it SHALL include the planning home root and change root + +#### Scenario: Reporting concrete artifact paths +- **WHEN** a user runs `openspec status --change --json` +- **THEN** the output SHALL include concrete paths for existing artifacts +- **AND** agents SHALL be able to read those paths without assuming `openspec/changes//` +- **AND** workspace-scoped nested spec paths SHALL be reported without flattening the area or capability path + +#### Scenario: Reporting workspace affected areas +- **GIVEN** the change is workspace-scoped +- **WHEN** a user runs `openspec status --change --json` +- **THEN** the output SHALL include known affected areas +- **AND** it SHALL indicate when affected areas remain unresolved without requiring an additional area manifest artifact + +#### Scenario: Reporting next steps +- **WHEN** a user runs `openspec status --change --json` +- **THEN** the output SHALL include next step guidance for agents +- **AND** the guidance SHALL use plain action language + +### Requirement: Status JSON action context +The status command SHALL expose action context that lets agents act without hardcoded filesystem assumptions. + +#### Scenario: Planning action context +- **WHEN** a workspace change is still in planning +- **THEN** status JSON SHALL identify the planning artifacts agents may read or update +- **AND** it SHALL indicate that linked repos and folders are context for exploration + +#### Scenario: Implementation action context +- **WHEN** a workspace change has a selected affected area for implementation +- **THEN** status JSON SHALL include the allowed edit root for that area +- **AND** it SHALL avoid authorizing edits outside that selected area + +#### Scenario: Repo-local action context +- **GIVEN** the change is repo-local +- **WHEN** a user runs `openspec status --change --json` +- **THEN** status JSON SHALL preserve existing artifact status behavior +- **AND** it SHALL report a repo-local planning home for agents that use action context + +### Requirement: Instructions use resolved planning paths +Artifact and apply instructions SHALL use resolved planning paths rather than hardcoded repo-local change paths. + +#### Scenario: Workspace artifact instructions +- **GIVEN** the change is workspace-scoped +- **WHEN** a user runs `openspec instructions --change --json` +- **THEN** instruction output SHALL point to the artifact path under the workspace change root +- **AND** it SHALL not instruct the agent to write under a linked repo unless an explicit implementation context allows it + +#### Scenario: Repo-local artifact instructions +- **GIVEN** the change is repo-local +- **WHEN** a user runs `openspec instructions --change --json` +- **THEN** instruction output SHALL preserve existing repo-local paths + +### Requirement: Workflow skills use CLI artifact context +Generated workflow skills SHALL use OpenSpec CLI output as the source of truth for artifact locations. + +#### Scenario: Skills inspect status before artifact work +- **WHEN** a generated workflow skill needs to inspect or create artifacts for a change +- **THEN** it SHALL instruct the agent to run `openspec status --change --json` +- **AND** it SHALL use returned planning context and artifact paths rather than assuming a repo-local change path + +#### Scenario: Skills use instructions before writing artifacts +- **WHEN** a generated workflow skill is about to create or update an artifact +- **THEN** it SHALL instruct the agent to run `openspec instructions --change --json` +- **AND** it SHALL write to the resolved artifact path returned by the command + +#### Scenario: Skills avoid hardcoded repo-local paths +- **WHEN** generated workflow skills describe artifact locations +- **THEN** they SHALL avoid hardcoded examples that require changes to live under `openspec/changes//` +- **AND** any examples SHALL defer to CLI-reported paths for repo-local and workspace-scoped changes + +#### Scenario: Skills guard unsupported workspace workflows +- **GIVEN** a generated workflow skill is selected by the global profile +- **AND** the workflow does not yet have full workspace-scoped behavior in this slice +- **WHEN** the skill is used for a workspace-scoped change +- **THEN** it SHALL tell the agent that the workspace action is not supported yet +- **AND** it SHALL not instruct the agent to fall back to repo-local paths or edit linked repos without an explicit allowed edit root + +### Requirement: Workspace schema instructions +Workflow commands SHALL use the workspace planning schema instructions for workspace-scoped changes that use that schema. + +#### Scenario: Workspace planning artifact order +- **GIVEN** a workspace-scoped change uses schema `workspace-planning` +- **WHEN** a user runs `openspec status --change --json` +- **THEN** the artifact list SHALL reflect the workspace planning schema +- **AND** it SHALL include the normal proposal, specs, design, and tasks artifacts + +#### Scenario: Workspace specs instructions +- **GIVEN** a workspace-scoped change uses schema `workspace-planning` +- **WHEN** a user requests instructions for the specs artifact +- **THEN** instruction output SHALL guide the agent to organize area-specific requirements under workspace-scoped `specs/` paths +- **AND** it SHALL not require all affected areas to be finalized before planning can continue +- **AND** it SHALL not instruct the agent to create repo-local spec files while the change is still in workspace planning diff --git a/openspec/specs/cli-config/spec.md b/openspec/specs/cli-config/spec.md index 8b87d110a..f3c9a11fa 100644 --- a/openspec/specs/cli-config/spec.md +++ b/openspec/specs/cli-config/spec.md @@ -262,3 +262,57 @@ The config command SHALL reserve the `--scope` flag for future extensibility. - **WHEN** user executes `openspec config --scope project ` - **THEN** display error message: "Project-local config is not yet implemented" - **AND** exit with code 1 + +### Requirement: Config profile applies to current workspace +The `openspec config profile` command SHALL remain global while offering an explicit workspace apply path when run from inside an OpenSpec workspace. + +#### Scenario: Config profile run inside a workspace +- **GIVEN** the command runs from inside an OpenSpec workspace +- **WHEN** the user changes profile or delivery settings with interactive `openspec config profile` +- **THEN** OpenSpec SHALL save the global config changes +- **AND** it SHALL prompt: `Apply changes to this workspace now?` + +#### Scenario: User confirms workspace apply +- **GIVEN** `openspec config profile` changed global profile or delivery settings inside a workspace +- **WHEN** the user confirms the workspace apply prompt +- **THEN** OpenSpec SHALL run `openspec workspace update` for the current workspace +- **AND** it SHALL not run repo-local `openspec update` unless the current planning home is repo-local + +#### Scenario: User declines workspace apply +- **GIVEN** `openspec config profile` changed global profile or delivery settings inside a workspace +- **WHEN** the user declines the workspace apply prompt +- **THEN** OpenSpec SHALL explain that global config was updated +- **AND** it SHALL tell the user to run `openspec workspace update` later to apply the profile to workspace-local skills +- **AND** it SHALL not modify workspace skill files + +#### Scenario: No-op inside workspace +- **GIVEN** the command runs from inside an OpenSpec workspace +- **WHEN** `openspec config profile` exits with no effective config changes +- **THEN** OpenSpec SHALL not prompt to apply changes +- **AND** it SHALL warn if workspace-local skills are out of sync with the current global profile +- **AND** the warning SHALL suggest `openspec workspace update` + +#### Scenario: Core preset shortcut inside a workspace +- **GIVEN** the command runs from inside an OpenSpec workspace +- **WHEN** the user runs `openspec config profile core` +- **THEN** OpenSpec SHALL save the global config change without prompting to apply immediately +- **AND** it SHALL tell the user to run `openspec workspace update` to apply the profile to workspace-local skills + +#### Scenario: Core preset shortcut inside a repo project +- **GIVEN** the command runs from inside a repo-local OpenSpec project +- **WHEN** the user runs `openspec config profile core` +- **THEN** OpenSpec SHALL preserve existing repo-local shortcut behavior +- **AND** it SHALL tell the user to run `openspec update` to apply the profile to project files + +#### Scenario: Workspace planning home wins over linked repo project +- **GIVEN** the command runs in a path under a workspace planning home where a repo-local OpenSpec project could also be detected +- **WHEN** OpenSpec decides which apply prompt to show +- **THEN** the nearest current planning home SHALL determine whether to offer `openspec workspace update` or repo-local `openspec update` +- **AND** OpenSpec SHALL not apply profile changes to a linked repo when the current planning home is the workspace + +#### Scenario: Linked repo keeps repo-local profile behavior +- **GIVEN** a repo-local OpenSpec project is registered as a workspace link +- **AND** the command runs from inside that linked repo rather than from the workspace planning home +- **WHEN** OpenSpec decides which apply prompt or guidance to show +- **THEN** OpenSpec SHALL preserve repo-local `openspec update` behavior for that repo +- **AND** it SHALL not offer `openspec workspace update` unless the workspace is explicitly selected diff --git a/openspec/specs/cli-update/spec.md b/openspec/specs/cli-update/spec.md index 3de91356e..6e848751a 100644 --- a/openspec/specs/cli-update/spec.md +++ b/openspec/specs/cli-update/spec.md @@ -166,6 +166,26 @@ The archive slash command template SHALL support optional change ID arguments fo - **AND** wrap it in a clear structure like `\n $ARGUMENTS\n` 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 +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 +- **GIVEN** the command runs from an OpenSpec workspace root +- **WHEN** the user runs `openspec update` +- **THEN** OpenSpec SHALL not generate repo-local project files in the workspace root +- **AND** it SHALL tell the user to run `openspec workspace update` + +#### Scenario: Running update from inside a workspace planning directory +- **GIVEN** the command runs from a subdirectory of an OpenSpec workspace planning home +- **WHEN** the user runs `openspec update` +- **THEN** OpenSpec SHALL not run repo-local update behavior +- **AND** it SHALL tell the user to run `openspec workspace update` + +#### Scenario: Running update from a repo-local project +- **GIVEN** the command runs from inside a repo-local OpenSpec project +- **WHEN** the user runs `openspec update` +- **THEN** OpenSpec SHALL preserve existing repo-local update behavior + ## Edge Cases ### Error Handling diff --git a/openspec/specs/openspec-conventions/spec.md b/openspec/specs/openspec-conventions/spec.md index 31be93e8e..f1360cf2e 100644 --- a/openspec/specs/openspec-conventions/spec.md +++ b/openspec/specs/openspec-conventions/spec.md @@ -273,6 +273,37 @@ OpenSpec conventions SHALL describe coordination workspaces in user-facing produ - **THEN** conventions SHALL allow those changes to remain flat siblings under `openspec/changes/` - **AND** dependency order MAY be documented in proposal prose until formal change stacking metadata is available +### Requirement: Workspace planning vocabulary +OpenSpec conventions SHALL distinguish workspace planning concepts using user-facing product language. + +#### Scenario: Naming affected areas +- **WHEN** documentation or generated guidance refers to repos, folders, packages, services, apps, or docs sites touched by a workspace change +- **THEN** it SHALL call them affected areas +- **AND** it SHALL avoid using "target repo" or "repo slice" as the primary user-facing term + +#### Scenario: Naming delivery slices +- **WHEN** documentation or generated guidance refers to delivery increments inside a larger change +- **THEN** it SHALL call them slices or phases only when delivery sequencing is the subject +- **AND** it SHALL not use slice as a synonym for repo, folder, or affected area + +### Requirement: Workspace planning and implementation boundary +OpenSpec conventions SHALL distinguish workspace-level planning from repo-local implementation ownership. + +#### Scenario: Workspace as shared planning home +- **WHEN** a change spans linked repos or folders +- **THEN** conventions SHALL describe the workspace as the shared planning home +- **AND** repo-local implementation homes SHALL retain ownership of their code and canonical behavior + +#### Scenario: Avoiding materialization-first language +- **WHEN** documentation explains workspace change creation +- **THEN** it SHALL describe the user outcome in terms of shared planning and affected areas +- **AND** it SHALL avoid making users understand implementation terms such as materialization before they can plan + +#### Scenario: Preserving familiar workflow verbs +- **WHEN** workspace guidance describes OpenSpec workflows +- **THEN** it SHALL keep the familiar verbs explore, propose, apply, verify, and archive +- **AND** it SHALL explain that workspace context changes paths, scope, and allowed edit roots rather than creating a separate workflow family + ## Core Principles The system SHALL follow these principles: diff --git a/openspec/specs/schema-resolution/spec.md b/openspec/specs/schema-resolution/spec.md index b8c0caace..cacf5f74b 100644 --- a/openspec/specs/schema-resolution/spec.md +++ b/openspec/specs/schema-resolution/spec.md @@ -2,7 +2,6 @@ ## Purpose Define project-local schema resolution behavior, including precedence order (project-local, then user override, then package built-in) and backward-compatible fallback when `projectRoot` is not provided. - ## Requirements ### Requirement: Project-local schema resolution @@ -170,3 +169,27 @@ The system SHALL continue to work with existing changes that do not have project #### Scenario: Existing change with config added later - **WHEN** config file is added to project with existing changes - **THEN** existing changes continue to use their bound schema from `.openspec.yaml` + +### Requirement: Workspace planning schema resolution +Schema resolution SHALL support the built-in workspace planning schema. + +#### Scenario: Listing workspace planning schema +- **WHEN** a user runs `openspec schemas` +- **THEN** the output SHALL include `workspace-planning` +- **AND** it SHALL identify it as a package-provided schema unless overridden by a higher-precedence schema + +#### Scenario: Resolving workspace planning schema by name +- **WHEN** a workflow command requests schema `workspace-planning` +- **THEN** schema resolution SHALL resolve it using the normal project, user, then package precedence order + +#### Scenario: Workspace default schema for new changes +- **GIVEN** the command creates a change in a workspace planning home +- **AND** the user did not pass an explicit `--schema` +- **WHEN** OpenSpec resolves the schema for the new change +- **THEN** it SHALL use `workspace-planning` as the default schema + +#### Scenario: Explicit schema override for workspace change +- **GIVEN** the command creates a change in a workspace planning home +- **WHEN** the user passes an explicit `--schema ` +- **THEN** OpenSpec SHALL use the explicitly requested schema +- **AND** it SHALL validate that schema using normal schema resolution diff --git a/openspec/specs/workspace-change-planning/spec.md b/openspec/specs/workspace-change-planning/spec.md new file mode 100644 index 000000000..59c640a92 --- /dev/null +++ b/openspec/specs/workspace-change-planning/spec.md @@ -0,0 +1,70 @@ +# workspace-change-planning Specification + +## Purpose +TBD - created by archiving change workspace-change-planning. Update Purpose after archive. +## Requirements +### Requirement: Workspace change planning home +OpenSpec SHALL support workspace-level changes whose shared plan lives in the workspace planning home. + +#### Scenario: Creating a workspace change +- **GIVEN** the command runs from an OpenSpec workspace +- **WHEN** the user creates a change for workspace planning +- **THEN** OpenSpec SHALL create the change under the workspace planning path +- **AND** it SHALL treat the workspace as the planning home for that change +- **AND** it SHALL use the workspace planning schema when no explicit schema is provided + +#### Scenario: Workspace planning artifact structure +- **GIVEN** a workspace change uses the workspace planning schema +- **WHEN** OpenSpec reports or creates planning artifacts for that change +- **THEN** it SHALL use workspace-level artifacts for proposal, specs, cross-area design, and coordination tasks +- **AND** those artifacts SHALL live under the workspace change root +- **AND** it SHALL not require an additional area manifest outside those normal planning artifacts + +#### Scenario: Capturing the shared goal once +- **WHEN** a workspace change is proposed +- **THEN** OpenSpec SHALL capture the product goal at the workspace change level +- **AND** it SHALL avoid requiring separate repo-local proposals before the affected areas are understood + +#### Scenario: Preserving linked repos during change creation +- **WHEN** OpenSpec creates a workspace-level change +- **THEN** it SHALL not create repo-local OpenSpec change directories inside linked repos or folders +- **AND** it SHALL not edit implementation files in linked repos or folders + +### Requirement: Workspace affected areas +OpenSpec SHALL represent ownership or implementation boundaries in a workspace change as affected areas. + +#### Scenario: Using registered workspace links as areas +- **GIVEN** a workspace has linked repos or folders +- **WHEN** a workspace change identifies affected areas by registered link name +- **THEN** OpenSpec SHALL validate those area names against the workspace links +- **AND** it SHALL report invalid area names clearly + +#### Scenario: Planning before all areas are known +- **WHEN** a user is still exploring a workspace change +- **THEN** OpenSpec SHALL allow the shared plan to exist before all affected areas are finalized +- **AND** it SHALL keep unresolved affected area questions visible in the normal planning artifacts and status output + +#### Scenario: Organizing requirements by area +- **GIVEN** a workspace change has requirements owned by one or more affected areas +- **WHEN** OpenSpec reports or creates workspace-scoped specs +- **THEN** it SHALL allow area-specific requirements to be organized under `specs///spec.md` +- **AND** it SHALL not require separate area folders outside the normal `specs/` artifact tree +- **AND** it SHALL preserve the area-or-repo path segment as workspace planning context rather than flattening it into a repo-local capability name + +#### Scenario: Separating areas from delivery slices +- **WHEN** a workspace change reports affected areas +- **THEN** OpenSpec SHALL distinguish affected areas from delivery slices or phases +- **AND** it SHALL not require users to define delivery slices for a small cross-area change + +### Requirement: Workspace planning source of truth +OpenSpec SHALL keep the workspace change plan as the source of truth until implementation begins for a selected affected area. + +#### Scenario: Exploring before implementation +- **WHEN** an agent explores a workspace change +- **THEN** it SHALL use workspace-level planning artifacts as the shared planning source +- **AND** it SHALL treat linked repos and folders as available context rather than committed implementation targets + +#### Scenario: Deferring repo-local implementation +- **WHEN** repo-local implementation work is needed for a workspace change +- **THEN** OpenSpec SHALL require an explicit implementation workflow with a selected affected area +- **AND** it SHALL expose the allowed edit root for that selected area before implementation edits begin diff --git a/openspec/specs/workspace-links/spec.md b/openspec/specs/workspace-links/spec.md index f8abc488b..840d8e371 100644 --- a/openspec/specs/workspace-links/spec.md +++ b/openspec/specs/workspace-links/spec.md @@ -4,7 +4,6 @@ Define the direct workspace setup, discovery, linking, relinking, health check, and JSON-output behavior for managing OpenSpec workspaces across repos and folders. - ## Requirements ### Requirement: Guided Workspace Setup OpenSpec SHALL provide a guided setup flow for users starting workspace planning. @@ -360,3 +359,165 @@ OpenSpec SHALL provide JSON output for direct workspace setup commands. #### Scenario: Commands with JSON output - **WHEN** users run `workspace setup --no-interactive`, `workspace list`, `workspace link`, `workspace relink`, or `workspace doctor` - **THEN** each command SHALL support JSON output + +### Requirement: Workspace setup installs agent skills +OpenSpec SHALL let users install OpenSpec agent skills into a workspace during workspace setup. + +#### Scenario: Prompting for workspace agent skills +- **WHEN** interactive workspace setup reaches agent skill installation +- **THEN** OpenSpec SHALL ask which agents should get OpenSpec skills in this workspace +- **AND** the prompt SHALL use agent-skill language rather than "AI tools" language + +#### Scenario: Preselecting the preferred opener +- **GIVEN** the user selected a preferred opener that supports OpenSpec skill generation +- **WHEN** interactive workspace setup asks which agents should get skills +- **THEN** OpenSpec SHALL preselect the matching agent +- **AND** the user SHALL be able to select additional agents or deselect the preselected agent + +#### Scenario: Installing selected workspace skills +- **WHEN** workspace setup completes with one or more selected agents +- **THEN** OpenSpec SHALL generate or refresh OpenSpec skill files under the workspace root for each selected agent +- **AND** it SHALL report which agents received skills +- **AND** it SHALL store the selected agents in workspace-local machine state + +#### Scenario: Installing profile-selected workflows +- **GIVEN** global config resolves to a workflow profile +- **WHEN** workspace setup installs agent skills +- **THEN** OpenSpec SHALL install workspace-local skills for the workflows selected by that profile +- **AND** it SHALL treat `--tools` as agent selection, not workflow selection +- **AND** it SHALL record the last applied workflow IDs for drift detection + +#### Scenario: Installing skills only during setup +- **WHEN** workspace setup installs agent skills +- **THEN** OpenSpec SHALL generate skill files only +- **AND** it SHALL not generate slash command files or global command files as part of workspace setup + +#### Scenario: Ignoring command delivery for workspace setup +- **GIVEN** global config delivery is `commands` or `both` +- **WHEN** workspace setup installs agent skills +- **THEN** OpenSpec SHALL still generate workspace-local skills only +- **AND** it SHALL report that workspace command generation is not part of this slice + +#### Scenario: Preserving linked repos during skill installation +- **WHEN** workspace setup installs agent skills +- **THEN** OpenSpec SHALL leave linked repos and folders unchanged +- **AND** generated skills SHALL be scoped to the workspace planning home + +#### Scenario: Non-interactive setup tool selection +- **WHEN** non-interactive workspace setup receives `--tools all`, `--tools none`, or `--tools ` +- **THEN** OpenSpec SHALL use the selected tool set for workspace agent skill installation +- **AND** it SHALL validate tool IDs using the same supported tool IDs as skill generation for repo initialization + +#### Scenario: Non-interactive setup without tool selection +- **WHEN** non-interactive workspace setup omits `--tools` +- **THEN** OpenSpec SHALL create the workspace without installing agent skills +- **AND** it SHALL report that no workspace skills were installed +- **AND** it SHALL tell the user to run `openspec workspace update --tools ` to install skills later + +#### Scenario: Reporting setup skills in JSON output +- **WHEN** non-interactive workspace setup installs agent skills with JSON output enabled +- **THEN** OpenSpec SHALL include generated, refreshed, skipped, or failed skill installation results in machine-readable output + +### Requirement: Workspace update manages agent skills +OpenSpec SHALL provide a workspace update flow for refreshing agent skills after setup. + +#### Scenario: Updating the current workspace +- **GIVEN** the command runs from inside an OpenSpec workspace +- **WHEN** the user runs `openspec workspace update` +- **THEN** OpenSpec SHALL update that current workspace + +#### Scenario: Updating a named workspace +- **GIVEN** a workspace named `platform` is known locally +- **WHEN** the user runs `openspec workspace update platform` +- **THEN** OpenSpec SHALL update the `platform` workspace + +#### Scenario: Updating a workspace selected by flag +- **GIVEN** a workspace named `platform` is known locally +- **WHEN** the user runs `openspec workspace update --workspace platform` +- **THEN** OpenSpec SHALL update the `platform` workspace + +#### Scenario: Updating selected workspace skills +- **WHEN** workspace update completes with selected agents +- **THEN** OpenSpec SHALL refresh OpenSpec skills for selected agents +- **AND** it SHALL add skills for newly selected agents +- **AND** it SHALL remove OpenSpec-managed workflow skill directories for agents that are no longer selected +- **AND** it SHALL update the stored workspace-local selected agent list + +#### Scenario: Updating profile-selected workflows +- **GIVEN** global config resolves to a workflow profile +- **WHEN** workspace update refreshes workspace-local skills +- **THEN** OpenSpec SHALL sync the workspace-local skill workflow set to the workflows selected by that profile +- **AND** deselected workflow skill directories SHALL be removed only when they are known OpenSpec-managed workflow skill directories +- **AND** it SHALL update the last applied workflow IDs used for drift detection + +#### Scenario: Ignoring command delivery for workspace update +- **GIVEN** global config delivery is `commands` or `both` +- **WHEN** workspace update refreshes workspace-local skills +- **THEN** OpenSpec SHALL still update workspace-local skills only +- **AND** it SHALL not generate slash command files or global command files + +#### Scenario: Removing only managed skill directories +- **WHEN** workspace update removes skills for an unselected agent +- **THEN** OpenSpec SHALL remove only known OpenSpec-managed workflow skill directories +- **AND** it SHALL preserve unrelated files in the agent directory + +#### Scenario: Updating stored agent selection by flag +- **WHEN** workspace update receives `--tools ` or `--tools none` +- **THEN** OpenSpec SHALL replace the stored workspace-local selected agent list with that selection +- **AND** future workspace updates without `--tools` SHALL use the stored selection + +#### Scenario: Non-interactive update tool selection +- **WHEN** workspace update receives `--tools all`, `--tools none`, or `--tools ` +- **THEN** OpenSpec SHALL update workspace agent skills using that selected tool set +- **AND** it SHALL avoid prompting for agent selection + +#### Scenario: Non-interactive update without tool selection +- **GIVEN** workspace-local selected agents are stored +- **WHEN** non-interactive workspace update omits `--tools` +- **THEN** OpenSpec SHALL refresh the stored selected agents using the active global profile +- **AND** it SHALL avoid prompting for agent selection + +#### Scenario: Non-interactive update without stored selection +- **GIVEN** no workspace-local selected agents are stored +- **WHEN** non-interactive workspace update omits `--tools` +- **THEN** OpenSpec SHALL complete without installing agent skills +- **AND** it SHALL report a no-op with guidance to pass `--tools` + +#### Scenario: Reporting workspace skill drift +- **GIVEN** workspace-local skill state records last applied workflow IDs +- **AND** the active global profile resolves to a different workflow set +- **WHEN** OpenSpec reports workspace skill state +- **THEN** it SHALL report that workspace-local skills are out of sync with the global profile +- **AND** it SHALL suggest `openspec workspace update` + +#### Scenario: Reporting clean workspace skill sync +- **GIVEN** workspace-local skill state matches the active global profile and selected agents +- **WHEN** OpenSpec reports workspace skill state +- **THEN** it SHALL not report profile drift + +#### Scenario: Reporting workspace skill update results +- **WHEN** workspace update changes agent skill state +- **THEN** OpenSpec SHALL report which agents were refreshed, added, removed, skipped, or failed + +#### Scenario: Reporting workspace update results in JSON output +- **WHEN** workspace update runs with JSON output enabled +- **THEN** OpenSpec SHALL include refreshed, added, removed, skipped, or failed skill results in machine-readable output + +### Requirement: Workspace skill update surface is documented +OpenSpec SHALL expose workspace skill setup/update behavior in user-facing command surfaces. + +#### Scenario: Workspace update appears in help +- **WHEN** a user runs `openspec workspace --help` +- **THEN** OpenSpec SHALL list `workspace update` +- **AND** it SHALL describe it as refreshing workspace-local agent skills + +#### Scenario: Workspace update options appear in help +- **WHEN** a user runs `openspec workspace update --help` +- **THEN** OpenSpec SHALL document workspace selection options +- **AND** it SHALL document `--tools all|none|` +- **AND** it SHALL state that global profile selects workflows and `--tools` selects agents + +#### Scenario: Workspace update appears in completions +- **WHEN** shell completions are generated +- **THEN** the workspace command registry SHALL include `workspace update` +- **AND** it SHALL include relevant options such as `--workspace`, `--tools`, `--json`, and `--no-interactive` From a70de99eb84ac45c5810c6c059883e5f0f6adc2a Mon Sep 17 00:00:00 2001 From: TabishB Date: Thu, 14 May 2026 23:26:36 +1000 Subject: [PATCH 13/14] Fix archived workspace planning spec purpose --- openspec/specs/workspace-change-planning/spec.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openspec/specs/workspace-change-planning/spec.md b/openspec/specs/workspace-change-planning/spec.md index 59c640a92..0d8ea674f 100644 --- a/openspec/specs/workspace-change-planning/spec.md +++ b/openspec/specs/workspace-change-planning/spec.md @@ -1,7 +1,8 @@ # workspace-change-planning Specification ## Purpose -TBD - created by archiving change workspace-change-planning. Update Purpose after archive. +Define how OpenSpec creates, tracks, and guides workspace-level changes whose planning artifacts coordinate multiple linked repos or folders before implementation ownership is finalized. + ## Requirements ### Requirement: Workspace change planning home OpenSpec SHALL support workspace-level changes whose shared plan lives in the workspace planning home. From 829ef46c341a773c52259cd73f2881681ac7e391 Mon Sep 17 00:00:00 2001 From: TabishB Date: Thu, 14 May 2026 23:29:59 +1000 Subject: [PATCH 14/14] Address workspace planning review comments --- openspec/specs/schema-resolution/spec.md | 20 +++++++++++++------- openspec/specs/workspace-links/spec.md | 6 ++++++ src/cli/index.ts | 2 +- src/commands/workspace/types.ts | 1 + 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/openspec/specs/schema-resolution/spec.md b/openspec/specs/schema-resolution/spec.md index cacf5f74b..f243252ad 100644 --- a/openspec/specs/schema-resolution/spec.md +++ b/openspec/specs/schema-resolution/spec.md @@ -92,14 +92,14 @@ The `openspec schemas` command SHALL display the source of each schema. ### Requirement: Use config schema as default for new changes -The system SHALL use the schema field from `openspec/config.yaml` as the default when creating new changes without explicit `--schema` flag. +The system SHALL use the schema field from `openspec/config.yaml` as the default when creating new changes without explicit `--schema` flag and no planning-home default applies. #### Scenario: Create change without --schema flag and config exists -- **WHEN** user runs `openspec new change foo` and config contains `schema: "tdd"` +- **WHEN** user runs `openspec new change foo`, no planning-home default applies, and config contains `schema: "tdd"` - **THEN** system creates change with schema "tdd" #### Scenario: Create change without --schema flag and no config -- **WHEN** user runs `openspec new change foo` and no config file exists +- **WHEN** user runs `openspec new change foo`, no planning-home default applies, and no config file exists - **THEN** system creates change with default schema "spec-driven" #### Scenario: Create change with explicit --schema flag @@ -108,7 +108,7 @@ The system SHALL use the schema field from `openspec/config.yaml` as the default ### Requirement: Resolve schema with updated precedence order -The system SHALL resolve the schema for a change using the following precedence order: CLI flag, change metadata, project config, hardcoded default. +The system SHALL resolve the schema for a change using the following precedence order: CLI flag, change metadata, planning-home default, project config, hardcoded default. #### Scenario: CLI flag is provided - **WHEN** user runs command with `--schema custom` @@ -118,12 +118,16 @@ The system SHALL resolve the schema for a change using the following precedence - **WHEN** change has `.openspec.yaml` with `schema: bound` and config has `schema: tdd` - **THEN** system uses "bound" from change metadata +#### Scenario: Planning home default overrides project config +- **WHEN** no CLI flag or change metadata, the planning home provides default schema `workspace-planning`, and config has `schema: tdd` +- **THEN** system uses "workspace-planning" from the planning home default + #### Scenario: Only project config specifies schema -- **WHEN** no CLI flag or change metadata, but config has `schema: tdd` +- **WHEN** no CLI flag, change metadata, or planning-home default exists, but config has `schema: tdd` - **THEN** system uses "tdd" from project config #### Scenario: No schema specified anywhere -- **WHEN** no CLI flag, change metadata, or project config +- **WHEN** no CLI flag, change metadata, planning-home default, or project config - **THEN** system uses hardcoded default "spec-driven" ### Requirement: Support project-local schema names in config @@ -185,8 +189,10 @@ Schema resolution SHALL support the built-in workspace planning schema. #### Scenario: Workspace default schema for new changes - **GIVEN** the command creates a change in a workspace planning home - **AND** the user did not pass an explicit `--schema` +- **AND** no change metadata schema applies to the new change - **WHEN** OpenSpec resolves the schema for the new change -- **THEN** it SHALL use `workspace-planning` as the default schema +- **THEN** it SHALL use the planning-home default schema `workspace-planning` +- **AND** it SHALL use that planning-home default before any project or global config schema value #### Scenario: Explicit schema override for workspace change - **GIVEN** the command creates a change in a workspace planning home diff --git a/openspec/specs/workspace-links/spec.md b/openspec/specs/workspace-links/spec.md index 840d8e371..edbc2fea8 100644 --- a/openspec/specs/workspace-links/spec.md +++ b/openspec/specs/workspace-links/spec.md @@ -443,6 +443,12 @@ OpenSpec SHALL provide a workspace update flow for refreshing agent skills after - **AND** it SHALL remove OpenSpec-managed workflow skill directories for agents that are no longer selected - **AND** it SHALL update the stored workspace-local selected agent list +#### Scenario: Identifying managed workflow skill directories +- **WHEN** workspace update evaluates a workflow skill directory for removal +- **THEN** OpenSpec SHALL treat it as OpenSpec-managed only when the directory name matches a known generated workflow skill directory and its `SKILL.md` contains OpenSpec generated metadata +- **AND** generated metadata SHALL include the `generatedBy` marker written by OpenSpec skill generation +- **AND** OpenSpec SHALL not remove directories that are missing the generated metadata, even when their names match known workflow skill directory names + #### Scenario: Updating profile-selected workflows - **GIVEN** global config resolves to a workflow profile - **WHEN** workspace update refreshes workspace-local skills diff --git a/src/cli/index.ts b/src/cli/index.ts index 186e13eb9..baa3e48fa 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -167,7 +167,7 @@ program const resolvedPath = path.resolve(targetPath); const workspaceRoot = await findWorkspaceRoot(resolvedPath); if (workspaceRoot) { - await runWorkspaceUpdateForRoot(workspaceRoot, {}); + await runWorkspaceUpdateForRoot(workspaceRoot, { force: options?.force }); return; } diff --git a/src/commands/workspace/types.ts b/src/commands/workspace/types.ts index 5943c3a57..e680cc901 100644 --- a/src/commands/workspace/types.ts +++ b/src/commands/workspace/types.ts @@ -51,6 +51,7 @@ export type WorkspaceLinkOptions = WorkspaceSelectionOptions; export interface WorkspaceUpdateOptions extends WorkspaceSelectionOptions { tools?: string; + force?: boolean; } export interface WorkspaceOpenOptions extends WorkspaceSelectionOptions {