Compare commits

..
Author SHA1 Message Date
Tabish Bidiwale d9d3709fad docs(changes): plan interactive proposal qa flow 2025-09-30 11:47:37 +10:00
22 changed files with 229 additions and 263 deletions
+17 -6
View File
@@ -1,8 +1,19 @@
<!-- OPENSPEC:START -->
# OpenSpec Instructions
# Repository Guidelines
This project uses OpenSpec to manage AI assistant workflows.
## Project Structure & Module Organization
OpenSpec ships as a TypeScript-first CLI. Source code lives in `src`, with feature logic in `core`, interactive flows in `cli`, reusable helpers in `utils`, and command wiring in `commands`. After `pnpm run build`, deliverables land in `dist` and feed the published entry point `bin/openspec.js`. Specs and change proposals reside in `openspec/specs` and `openspec/changes`; update them whenever behavior shifts so automation stays aligned. Shared assets live in `assets`, and Vitest suites in `test` mirror the source layout for easy cross-reference.
- Full guidance lives in '@/openspec/AGENTS.md'.
- Keep this managed block so 'openspec update' can refresh the instructions.
<!-- OPENSPEC:END -->
## Build, Test, and Development Commands
Run `pnpm install` to sync dependencies. `pnpm run build` compiles TypeScript to `dist` and must stay green before release. Use `pnpm run dev` for a `tsc --watch` loop and `pnpm run dev:cli` to rebuild then execute the local CLI. `pnpm test` runs the Vitest suite once, `pnpm run test:watch` keeps it hot while iterating, and `pnpm run test:coverage` verifies instrumentation thresholds. Use `pnpm run changeset` when preparing a release entry.
## Coding Style & Naming Conventions
We follow idiomatic TypeScript with ES modules, 2-space indentation, and semicolons. Prefer named exports from index barrels and keep filenames kebab-cased (e.g., `list-command.ts`). Classes use `PascalCase`, functions and variables use `camelCase`, and constants representing flags may use `SCREAMING_SNAKE_CASE`. Keep modules small, colocate helpers under `src/utils`, and avoid new dependencies without spec-backed justification.
## Testing Guidelines
Every behavior change needs Vitest coverage under `test`, co-located by feature (e.g., `test/core/update.test.ts`). Name suites after the module under test and lean on `vitest.setup.ts` for shared configuration. Run `pnpm test` before pushing and add regression cases for each bug fix or spec requirement.
## Commit & Pull Request Guidelines
Commits follow Conventional Commits (`type(scope): subject`) and stay single-line. Reference the touched module in the scope when practical. Each PR should summarize the spec or issue it fulfills, list manual verification steps, and note updates to any `openspec/` assets. Include CLI output snippets or screenshots when the UX changes, and ensure CI and coverage checks pass before requesting review.
## OpenSpec Workflow Tips
Treat specs as the contract: update `openspec/project.md` or the relevant `openspec/specs/*.md` before coding, then run `pnpm run dev:cli` to validate the CLI against the revised artifacts. `openspec list --specs` confirms the catalog, and `openspec change` drafts proposals—commit these alongside code so reviewers can trace rationale to implementation.
-6
View File
@@ -1,11 +1,5 @@
# @fission-ai/openspec
## 0.6.0
### Minor Changes
- Slim the generated root agent instructions down to a managed hand-off stub and update the init/update flows to refresh it safely.
## 0.5.0
### Minor Changes
@@ -0,0 +1,13 @@
## Why
A recent feature request highlighted that users struggle to scope proposals when all clarifications have to be typed manually. They want a guided interview that surfaces missing assumptions, recommends best-practice defaults, and produces a sharper brief before handing off to `/openspec/proposal`. Delivering an interactive Q&A flow keeps OpenSpec competitive with other planning assistants and shortens the loop between idea and actionable change specification.
## What Changes
- Add a dedicated `/openspec/proposal-qa` slash command that runs a structured discovery interview before drafting a change proposal.
- Teach the agent to analyse the initial request, label what is already explicit versus ambiguous, and derive a small set of high-impact clarifying questions.
- Require every question to include rationale, 2–4 recommended options with a default, and clear guidance on when to pick each option.
- Capture the conversation outcome in a structured summary (problem, clarified decisions, open risks) that the user can accept or tweak before invoking `/openspec/proposal`.
- Update onboarding (`init`/`update`) and slash command templates so the new command ships everywhere OpenSpec currently provisions proposal/apply/archive helpers.
## Impact
- Affected specs: assistant-proposal-qa (new), cli-init, cli-update, slash-commands-template (if modelled separately)
- Affected code: `src/core/templates/slash-command-templates.ts`, `src/core/configurators/slash/*`, command scaffolding that writes `.claude/.cursor/.opencode` files, and associated tests.
@@ -0,0 +1,63 @@
# assistant-proposal-qa Specification
## ADDED Requirements
### Requirement: Interactive Proposal Q&A Command
The system SHALL provide a `/openspec/proposal-qa` slash command that prepares users for `/openspec/proposal` by running a structured discovery interview.
#### Scenario: Launching the proposal interview
- **WHEN** the user runs `/openspec/proposal-qa Build a notifications digest`
- **THEN** acknowledge the request and restate the draft problem statement
- **AND** highlight what parts of the request are already concrete versus ambiguous (e.g., "Strong signals" and "Needs clarity")
- **AND** outline the upcoming steps: targeted questions followed by a summary hand-off.
#### Scenario: Honour non-interactive environments
- **GIVEN** slash commands are invoked in a non-interactive environment (e.g., automation requesting defaults)
- **WHEN** `/openspec/proposal-qa` is triggered with `--no-interactive`
- **THEN** skip the question loop
- **AND** produce a summary that records the request, recommended defaults, and instructions for editing manually before running `/openspec/proposal`.
### Requirement: Targeted Clarifying Questions
The interview SHALL adaptively surface 3–6 high-leverage questions that eliminate ambiguity in the proposal brief.
#### Scenario: Ask one question at a time with rationale
- **WHEN** the interview begins gathering answers
- **THEN** select the next most risky/vague aspect of the feature
- **AND** present a single question that includes:
- A short rationale explaining why the question matters for the proposal
- 2–4 recommended options formatted as a bulleted list with `**Default**` clearly marked
- Guidance for when to choose each option (one sentence per option)
- **AND** wait for the user response (or `default`/`skip`) before showing another question.
#### Scenario: Provide fallbacks when users defer
- **WHEN** the user replies with `idk`, `default`, or leaves the answer empty
- **THEN** accept the default option for that question
- **AND** note in the transcript that the default was applied.
#### Scenario: Capture bespoke answers
- **WHEN** the user supplies an answer that does not match any recommended option
- **THEN** accept the custom answer
- **AND** record a short interpretation describing how it will shape the proposal.
### Requirement: Synthesis and Handoff
The interview SHALL produce an actionable summary that readies the agent to draft the formal change proposal.
#### Scenario: Summarise discoveries before exit
- **WHEN** the question loop completes (or is skipped)
- **THEN** output a structured summary containing:
- Problem statement and scope recap
- Table or bullet list of decisions (question → final answer → reasoning/default flag)
- Noted risks, open questions, and assumptions to confirm in the proposal
- **AND** recommend next actions: either ask for revisions, run `/openspec/proposal` with this summary, or request further research.
#### Scenario: Provide reusable prompt snippet
- **WHEN** the summary is generated
- **THEN** include a copyable prompt block that the user can paste into `/openspec/proposal`
- **AND** ensure the prompt references the summary decisions and flags any open items for follow-up.
#### Scenario: Allow re-entry for more questions
- **WHEN** the user indicates they want to refine further (e.g., "ask more" or "another pass")
- **THEN** identify remaining ambiguous areas not yet questioned
- **AND** continue with additional questions (up to the 6-question cap) before regenerating the summary.
@@ -0,0 +1,21 @@
## MODIFIED Requirements
### Requirement: Slash Command Configuration
The init command SHALL generate slash command files for supported editors using shared templates, including the interactive proposal interview instructions.
#### Scenario: Generating slash commands for Claude Code
- **WHEN** the user selects Claude Code during initialization
- **THEN** create `.claude/commands/openspec/proposal.md`, `.claude/commands/openspec/proposal-qa.md`, `.claude/commands/openspec/apply.md`, and `.claude/commands/openspec/archive.md`
- **AND** populate each file from shared templates so command text matches other tools
- **AND** include guidance that the `proposal-qa` command runs the interactive discovery interview before `/openspec/proposal`.
#### Scenario: Generating slash commands for Cursor
- **WHEN** the user selects Cursor during initialization
- **THEN** create `.cursor/commands/openspec-proposal.md`, `.cursor/commands/openspec-proposal-qa.md`, `.cursor/commands/openspec-apply.md`, and `.cursor/commands/openspec-archive.md`
- **AND** populate each file from shared templates so command text matches other tools
- **AND** include guidance that the `proposal-qa` command runs the interactive discovery interview before `/openspec/proposal`.
#### Scenario: Generating slash commands for OpenCode
- **WHEN** the user selects OpenCode during initialization
- **THEN** create `.opencode/commands/openspec-proposal.md`, `.opencode/commands/openspec-proposal-qa.md`, `.opencode/commands/openspec-apply.md`, and `.opencode/commands/openspec-archive.md`
- **AND** populate each file from shared templates so command text matches other tools
- **AND** include guidance that the `proposal-qa` command runs the interactive discovery interview before `/openspec/proposal`.
@@ -0,0 +1,18 @@
## MODIFIED Requirements
### Requirement: Slash Command Updates
The update command SHALL refresh existing slash command files for configured tools, including the interactive proposal interview template, without creating new ones.
#### Scenario: Updating slash commands for Claude Code
- **WHEN** `.claude/commands/openspec/` contains `proposal.md`, `proposal-qa.md`, `apply.md`, and `archive.md`
- **THEN** refresh each file using shared templates
- **AND** ensure the `proposal-qa` template contains the interactive discovery instructions aligned with the latest assistant guidance.
#### Scenario: Updating slash commands for Cursor
- **WHEN** `.cursor/commands/` contains `openspec-proposal.md`, `openspec-proposal-qa.md`, `openspec-apply.md`, and `openspec-archive.md`
- **THEN** refresh each file using shared templates
- **AND** ensure the `proposal-qa` template contains the interactive discovery instructions aligned with the latest assistant guidance.
#### Scenario: Updating slash commands for OpenCode
- **WHEN** `.opencode/commands/` contains `openspec-proposal.md`, `openspec-proposal-qa.md`, `openspec-apply.md`, and `openspec-archive.md`
- **THEN** refresh each file using shared templates
- **AND** ensure the `proposal-qa` template contains the interactive discovery instructions aligned with the latest assistant guidance.
@@ -0,0 +1,19 @@
## 1. Discovery & Design
- [ ] 1.1 Audit existing slash command files (`src/core/templates/slash-command-templates.ts`, configurators, tests) to mirror tone/format.
- [ ] 1.2 Draft conversational flow map covering request analysis, question generation heuristics, question formatting, and post-qa summary/next-steps.
- [ ] 1.3 Validate the flow against the example repo in issue #85 to ensure parity with proven UX patterns (e.g., defaults, recommended answers).
## 2. Specification Updates
- [ ] 2.1 Add `assistant-proposal-qa` capability spec capturing requirements for analysis, questioning, summary, and hand-off UX.
- [ ] 2.2 Update `cli-init` and `cli-update` specs so generated slash command files include the new `proposal-qa` command for all supported assistants.
## 3. Implementation
- [ ] 3.1 Extend `SlashCommandId` union, templates, and file writers to emit `/openspec/proposal-qa` with the new instructions body.
- [ ] 3.2 Implement helper(s) that build recommended answer options from agent analysis (list of option label, description, default marker).
- [ ] 3.3 Ensure question loop enforces 3–6 prompts, each with rationale and recommended default, and gracefully handles user-supplied alternatives.
- [ ] 3.4 Update onboarding/update flows to write `.claude/.cursor/.opencode` command markdown for `proposal-qa` alongside existing commands.
## 4. Validation & QA
- [ ] 4.1 Add unit tests covering slash template rendering for the new command and regression tests for init/update scaffolding.
- [ ] 4.2 Update documentation and examples (README, CHANGELOG as needed) showcasing how to use `/openspec/proposal-qa` and the resulting summary output.
- [ ] 4.3 Run `openspec validate add-interactive-proposal-qa --strict` and full test suite (`pnpm test`) to confirm specs and tooling stay green.
@@ -1,13 +0,0 @@
## Why
The project root currently receives a full copy of the OpenSpec agent instructions, duplicating the content that also lives in `openspec/AGENTS.md`. When teams edit one copy but not the other, the files drift and onboarding assistants see conflicting guidance.
## What Changes
- Keep generating the complete template in `openspec/AGENTS.md` during `openspec init` and follow-up updates.
- Replace the root-level file (`AGENTS.md` or `CLAUDE.md`, depending on tool selection) with a short hand-off that explains the project uses OpenSpec and points directly to `openspec/AGENTS.md`.
- Add a dedicated stub template so both the init and update flows reuse the same minimal copy instructions.
- Update CLI tests and documentation to reflect the new root-level messaging and ensure the OpenSpec marker block still protects future updates.
## Impact
- Affected specs: `cli-init`, `cli-update`
- Affected code: `src/core/init.ts`, `src/core/update.ts`, `src/core/templates/agents-template.ts`
- Update assets/readmes that mention the root `AGENTS.md` contents to reference the new stub message.
@@ -1,15 +0,0 @@
## 1. Templates
- [x] 1.1 Add a shared stub template that renders the root agent instructions hand-off message.
- [x] 1.2 Ensure the stub covers both `AGENTS.md` and `CLAUDE.md` variants.
## 2. Init Flow
- [x] 2.1 Update `createInitArtifacts` to write the stub to the project root instead of the full instructions.
- [x] 2.2 Preserve the managed block markers so future updates can overwrite the stub safely.
## 3. Update Flow
- [x] 3.1 Make the update command refresh the root stub rather than the full instructions.
- [x] 3.2 Confirm the update log output still reflects the files that changed.
## 4. Tests & Docs
- [x] 4.1 Adjust CLI/init tests to match the new root content.
- [x] 4.2 Document the stub message in `openspec/specs/cli-init` and `openspec/specs/cli-update` (and any relevant README snippets).
+5 -11
View File
@@ -38,7 +38,7 @@ The command SHALL generate required template files with appropriate content for
#### Scenario: Generating template files
- **WHEN** initializing OpenSpec
- **THEN** generate `openspec/AGENTS.md` containing complete OpenSpec instructions for AI assistants
- **THEN** generate `AGENTS.md` containing complete OpenSpec instructions for AI assistants
- **AND** generate `project.md` with project context template
### Requirement: AI Tool Configuration
@@ -52,7 +52,7 @@ The command SHALL configure AI coding assistants with OpenSpec instructions base
- **AND** list every available tool with a checkbox:
- Claude Code (creates or refreshes CLAUDE.md and slash commands)
- Cursor (creates or refreshes `.cursor/commands/*` slash commands)
- AGENTS.md standard (creates or refreshes AGENTS.md stub with OpenSpec markers)
- AGENTS.md standard (creates or refreshes AGENTS.md with OpenSpec markers)
- **AND** show "(already configured)" beside tools whose managed files exist so users understand selections will refresh content
- **AND** treat disabled tools as "coming soon" and keep them unselectable
- **AND** allow confirming with Enter after selecting one or more tools
@@ -65,22 +65,16 @@ The command SHALL properly configure selected AI tools with OpenSpec-specific in
- **WHEN** Claude Code is selected
- **THEN** create or update `CLAUDE.md` in the project root directory (not inside openspec/)
- **AND** populate the managed block with a short stub that points teammates to `@/openspec/AGENTS.md`
#### Scenario: Creating new CLAUDE.md
- **WHEN** CLAUDE.md does not exist
- **THEN** create new file with stub instructions wrapped in markers so the full workflow stays in `openspec/AGENTS.md`:
- **THEN** create new file with OpenSpec content wrapped in markers:
```markdown
<!-- OPENSPEC:START -->
# OpenSpec Instructions
This project uses OpenSpec to manage AI assistant workflows.
- Full guidance lives in '@/openspec/AGENTS.md'.
- Keep this managed block so 'openspec update' can refresh the instructions.
<!-- OPENSPEC:END -->
```
Instructions for AI coding assistants using OpenSpec for spec-driven development.
### Requirement: Interactive Mode
The command SHALL provide an interactive menu for AI tool selection with clear navigation instructions.
@@ -174,4 +168,4 @@ Manual creation of OpenSpec structure is error-prone and creates adoption fricti
- Consistent structure across all projects
- Proper AI instruction files are always included
- Quick onboarding for new projects
- Clear conventions from the start
- Clear conventions from the start
+3 -7
View File
@@ -10,7 +10,6 @@ The update command SHALL update OpenSpec instruction files to the latest templat
#### Scenario: Running update command
- **WHEN** a user runs `openspec update`
- **THEN** replace `openspec/AGENTS.md` with the latest template
- **AND** if a root-level stub (`AGENTS.md`/`CLAUDE.md`) exists, refresh it so it points to `@/openspec/AGENTS.md`
### Requirement: Prerequisites
@@ -29,7 +28,6 @@ The update command SHALL handle file updates in a predictable and safe manner.
#### Scenario: Updating files
- **WHEN** updating files
- **THEN** completely replace `openspec/AGENTS.md` with the latest template
- **AND** if a root-level stub exists, update the managed block content so it keeps directing teammates to `@/openspec/AGENTS.md`
### Requirement: Tool-Agnostic Updates
The update command SHALL handle file updates in a predictable and safe manner while respecting team tool choices.
@@ -38,12 +36,11 @@ The update command SHALL handle file updates in a predictable and safe manner wh
- **WHEN** updating files
- **THEN** completely replace `openspec/AGENTS.md` with the latest template
- **AND** update the root-level `AGENTS.md` using the OpenSpec markers only when that file already exists, keeping the stub content that links to `@/openspec/AGENTS.md`
- **AND** create or update the root-level `AGENTS.md` using the OpenSpec markers
- **AND** update only the OpenSpec-managed blocks in **existing** AI tool files using markers
- **AND** use the default directory name `openspec`
- **AND** be idempotent (repeated runs have no additional effect)
- **AND** respect team members' AI tool choices by not creating additional tool files beyond the root `AGENTS.md`
- **AND** do not create new root-level stub files when none are present
### Requirement: Core Files Always Updated
The update command SHALL always update the core OpenSpec files and display an ASCII-safe success message.
@@ -51,7 +48,6 @@ The update command SHALL always update the core OpenSpec files and display an AS
#### Scenario: Successful update
- **WHEN** the update completes successfully
- **THEN** replace `openspec/AGENTS.md` with the latest template
- **AND** if a root-level stub exists, refresh it so it still directs contributors to `@/openspec/AGENTS.md`
### Requirement: Slash Command Updates
The update command SHALL refresh existing slash command files for configured tools without creating new ones.
@@ -67,7 +63,7 @@ The update command SHALL refresh existing slash command files for configured too
- **AND** ensure templates include instructions for the relevant workflow stage
#### Scenario: Updating slash commands for OpenCode
- **WHEN** `.opencode/command/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **WHEN** `.opencode/commands/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **THEN** refresh each file using shared templates
- **AND** ensure templates include instructions for the relevant workflow stage
@@ -108,4 +104,4 @@ Users SHALL be able to:
The update process SHALL be:
- Simple and fast (no version checking)
- Predictable (same result every time)
- Self-contained (no network required)
- Self-contained (no network required)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@fission-ai/openspec",
"version": "0.6.0",
"version": "0.5.0",
"description": "AI-native system for spec-driven development",
"keywords": [
"openspec",
+7 -1
View File
@@ -22,13 +22,19 @@ import {
OPENSPEC_DIR_NAME,
AIToolOption,
} from './config.js';
import { PALETTE } from './styles/palette.js';
const PROGRESS_SPINNER = {
interval: 80,
frames: ['░░░', '▒░░', '▒▒░', '▒▒▒', '▓▒▒', '▓▓▒', '▓▓▓', '▒▓▓', '░▒▓'],
};
const PALETTE = {
white: chalk.hex('#f4f4f4'),
lightGray: chalk.hex('#c8c8c8'),
midGray: chalk.hex('#8a8a8a'),
darkGray: chalk.hex('#4a4a4a'),
};
const LETTER_MAP: Record<string, string[]> = {
O: [' ████ ', '██ ██', '██ ██', '██ ██', ' ████ '],
P: ['█████ ', '██ ██', '█████ ', '██ ', '██ '],
-8
View File
@@ -1,8 +0,0 @@
import chalk from 'chalk';
export const PALETTE = {
white: chalk.hex('#f4f4f4'),
lightGray: chalk.hex('#c8c8c8'),
midGray: chalk.hex('#8a8a8a'),
darkGray: chalk.hex('#4a4a4a')
};
-16
View File
@@ -1,16 +0,0 @@
export const agentsRootStubTemplate = `# OpenSpec Instructions
These instructions are for AI assistants working in this project.
Always open \`@/openspec/AGENTS.md\` when the request:
- Mentions planning or proposals (words like proposal, spec, change, plan)
- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
- Sounds ambiguous and you need the authoritative spec before coding
Use \`@/openspec/AGENTS.md\` to learn:
- How to create and apply change proposals
- Spec format and conventions
- Project structure and guidelines
Keep this managed block so 'openspec update' can refresh the instructions.
`;
+1 -1
View File
@@ -1 +1 @@
export { agentsRootStubTemplate as claudeTemplate } from './agents-root-stub.js';
export { agentsTemplate as claudeTemplate } from './agents-template.js';
+1 -2
View File
@@ -1,7 +1,6 @@
import { agentsTemplate } from './agents-template.js';
import { projectTemplate, ProjectContext } from './project-template.js';
import { claudeTemplate } from './claude-template.js';
import { agentsRootStubTemplate } from './agents-root-stub.js';
import { getSlashCommandBody, SlashCommandId } from './slash-command-templates.js';
export interface Template {
@@ -28,7 +27,7 @@ export class TemplateManager {
}
static getAgentsStandardTemplate(): string {
return agentsRootStubTemplate;
return agentsTemplate;
}
static getSlashCommandBody(id: SlashCommandId): string {
+49 -76
View File
@@ -1,9 +1,10 @@
import path from 'path';
import { FileSystemUtils } from '../utils/file-system.js';
import { OPENSPEC_DIR_NAME } from './config.js';
import { OPENSPEC_DIR_NAME, OPENSPEC_MARKERS } from './config.js';
import { agentsTemplate } from './templates/agents-template.js';
import { TemplateManager } from './templates/index.js';
import { ToolRegistry } from './configurators/registry.js';
import { SlashCommandRegistry } from './configurators/slash/registry.js';
import { agentsTemplate } from './templates/agents-template.js';
export class UpdateCommand {
async execute(projectPath: string): Promise<void> {
@@ -18,51 +19,41 @@ export class UpdateCommand {
// 2. Update AGENTS.md (full replacement)
const agentsPath = path.join(openspecPath, 'AGENTS.md');
const rootAgentsPath = path.join(resolvedProjectPath, 'AGENTS.md');
const rootAgentsExisted = await FileSystemUtils.fileExists(rootAgentsPath);
await FileSystemUtils.writeFile(agentsPath, agentsTemplate);
const agentsStandardContent = TemplateManager.getAgentsStandardTemplate();
await FileSystemUtils.updateFileWithMarkers(
rootAgentsPath,
agentsStandardContent,
OPENSPEC_MARKERS.start,
OPENSPEC_MARKERS.end
);
// 3. Update existing AI tool configuration files only
const configurators = ToolRegistry.getAll();
const slashConfigurators = SlashCommandRegistry.getAll();
const updatedFiles: string[] = [];
const createdFiles: string[] = [];
const failedFiles: string[] = [];
const updatedSlashFiles: string[] = [];
const failedSlashTools: string[] = [];
let updatedFiles: string[] = [];
let failedFiles: string[] = [];
let updatedSlashFiles: string[] = [];
let failedSlashTools: string[] = [];
for (const configurator of configurators) {
const configFilePath = path.join(
resolvedProjectPath,
configurator.configFileName
);
const fileExists = await FileSystemUtils.fileExists(configFilePath);
const shouldConfigure =
fileExists || configurator.configFileName === 'AGENTS.md';
if (!shouldConfigure) {
continue;
}
try {
if (fileExists && !await FileSystemUtils.canWriteFile(configFilePath)) {
throw new Error(
`Insufficient permissions to modify ${configurator.configFileName}`
);
const configFilePath = path.join(resolvedProjectPath, configurator.configFileName);
// Only update if the file already exists
if (await FileSystemUtils.fileExists(configFilePath)) {
try {
if (!await FileSystemUtils.canWriteFile(configFilePath)) {
throw new Error(`Insufficient permissions to modify ${configurator.configFileName}`);
}
await configurator.configure(resolvedProjectPath, openspecPath);
updatedFiles.push(configurator.configFileName);
} catch (error) {
failedFiles.push(configurator.configFileName);
console.error(`Failed to update ${configurator.configFileName}: ${error instanceof Error ? error.message : String(error)}`);
}
await configurator.configure(resolvedProjectPath, openspecPath);
updatedFiles.push(configurator.configFileName);
if (!fileExists) {
createdFiles.push(configurator.configFileName);
}
} catch (error) {
failedFiles.push(configurator.configFileName);
console.error(
`Failed to update ${configurator.configFileName}: ${
error instanceof Error ? error.message : String(error)
}`
);
}
}
@@ -72,56 +63,38 @@ export class UpdateCommand {
}
try {
const updated = await slashConfigurator.updateExisting(
resolvedProjectPath,
openspecPath
);
updatedSlashFiles.push(...updated);
const updated = await slashConfigurator.updateExisting(resolvedProjectPath, openspecPath);
updatedSlashFiles = updatedSlashFiles.concat(updated);
} catch (error) {
failedSlashTools.push(slashConfigurator.toolId);
console.error(
`Failed to update slash commands for ${slashConfigurator.toolId}: ${
error instanceof Error ? error.message : String(error)
}`
`Failed to update slash commands for ${slashConfigurator.toolId}: ${error instanceof Error ? error.message : String(error)}`
);
}
}
const summaryParts: string[] = [];
const instructionFiles: string[] = ['openspec/AGENTS.md'];
// 4. Success message (ASCII-safe)
const instructionUpdates = ['openspec/AGENTS.md'];
instructionUpdates.push(`AGENTS.md${rootAgentsExisted ? '' : ' (created)'}`);
if (updatedFiles.includes('AGENTS.md')) {
instructionFiles.push(
createdFiles.includes('AGENTS.md') ? 'AGENTS.md (created)' : 'AGENTS.md'
);
}
summaryParts.push(
`Updated OpenSpec instructions (${instructionFiles.join(', ')})`
);
const aiToolFiles = updatedFiles.filter((file) => file !== 'AGENTS.md');
if (aiToolFiles.length > 0) {
summaryParts.push(`Updated AI tool files: ${aiToolFiles.join(', ')}`);
const messages: string[] = [`Updated OpenSpec instructions (${instructionUpdates.join(', ')})`];
if (updatedFiles.length > 0) {
messages.push(`Updated AI tool files: ${updatedFiles.join(', ')}`);
}
if (updatedSlashFiles.length > 0) {
summaryParts.push(
`Updated slash commands: ${updatedSlashFiles.join(', ')}`
);
messages.push(`Updated slash commands: ${updatedSlashFiles.join(', ')}`);
}
if (failedFiles.length > 0) {
messages.push(`Failed to update: ${failedFiles.join(', ')}`);
}
const failedItems = [
...failedFiles,
...failedSlashTools.map(
(toolId) => `slash command refresh (${toolId})`
),
];
if (failedItems.length > 0) {
summaryParts.push(`Failed to update: ${failedItems.join(', ')}`);
if (failedSlashTools.length > 0) {
messages.push(`Failed slash command updates: ${failedSlashTools.join(', ')}`);
}
console.log(summaryParts.join(' | '));
console.log(messages.join('\n'));
}
}
+4 -52
View File
@@ -1,46 +1,6 @@
import { promises as fs } from 'fs';
import path from 'path';
function isMarkerOnOwnLine(content: string, markerIndex: number, markerLength: number): boolean {
let leftIndex = markerIndex - 1;
while (leftIndex >= 0 && content[leftIndex] !== '\n') {
const char = content[leftIndex];
if (char !== ' ' && char !== '\t' && char !== '\r') {
return false;
}
leftIndex--;
}
let rightIndex = markerIndex + markerLength;
while (rightIndex < content.length && content[rightIndex] !== '\n') {
const char = content[rightIndex];
if (char !== ' ' && char !== '\t' && char !== '\r') {
return false;
}
rightIndex++;
}
return true;
}
function findMarkerIndex(
content: string,
marker: string,
fromIndex = 0
): number {
let currentIndex = content.indexOf(marker, fromIndex);
while (currentIndex !== -1) {
if (isMarkerOnOwnLine(content, currentIndex, marker.length)) {
return currentIndex;
}
currentIndex = content.indexOf(marker, currentIndex + marker.length);
}
return -1;
}
export class FileSystemUtils {
static async createDirectory(dirPath: string): Promise<void> {
await fs.mkdir(dirPath, { recursive: true });
@@ -110,18 +70,10 @@ export class FileSystemUtils {
if (await this.fileExists(filePath)) {
existingContent = await this.readFile(filePath);
const startIndex = findMarkerIndex(existingContent, startMarker);
const endIndex = startIndex !== -1
? findMarkerIndex(existingContent, endMarker, startIndex + startMarker.length)
: findMarkerIndex(existingContent, endMarker);
const startIndex = existingContent.indexOf(startMarker);
const endIndex = existingContent.indexOf(endMarker);
if (startIndex !== -1 && endIndex !== -1) {
if (endIndex < startIndex) {
throw new Error(
`Invalid marker state in ${filePath}. End marker appears before start marker.`
);
}
const before = existingContent.substring(0, startIndex);
const after = existingContent.substring(endIndex + endMarker.length);
existingContent = before + startMarker + '\n' + content + '\n' + endMarker + after;
@@ -157,4 +109,4 @@ export class FileSystemUtils {
return false;
}
}
}
}
+3 -6
View File
@@ -106,8 +106,7 @@ describe('InitCommand', () => {
const content = await fs.readFile(claudePath, 'utf-8');
expect(content).toContain('<!-- OPENSPEC:START -->');
expect(content).toContain("@/openspec/AGENTS.md");
expect(content).toContain('openspec update');
expect(content).toContain('OpenSpec Instructions');
expect(content).toContain('<!-- OPENSPEC:END -->');
});
@@ -123,8 +122,7 @@ describe('InitCommand', () => {
const updatedContent = await fs.readFile(claudePath, 'utf-8');
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
expect(updatedContent).toContain("@/openspec/AGENTS.md");
expect(updatedContent).toContain('openspec update');
expect(updatedContent).toContain('OpenSpec Instructions');
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
expect(updatedContent).toContain('Custom instructions here');
});
@@ -139,8 +137,7 @@ describe('InitCommand', () => {
const content = await fs.readFile(rootAgentsPath, 'utf-8');
expect(content).toContain('<!-- OPENSPEC:START -->');
expect(content).toContain("@/openspec/AGENTS.md");
expect(content).toContain('openspec update');
expect(content).toContain('OpenSpec Instructions');
expect(content).toContain('<!-- OPENSPEC:END -->');
const claudeExists = await fileExists(path.join(testDir, 'CLAUDE.md'));
+3 -6
View File
@@ -51,8 +51,7 @@ More content after.`;
const updatedContent = await fs.readFile(claudePath, 'utf-8');
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
expect(updatedContent).toContain("@/openspec/AGENTS.md");
expect(updatedContent).toContain('openspec update');
expect(updatedContent).toContain('OpenSpec Instructions');
expect(updatedContent).toContain('Some existing content here');
expect(updatedContent).toContain('More content after');
@@ -304,8 +303,7 @@ Old content
const content = await fs.readFile(rootAgentsPath, 'utf-8');
expect(content).toContain('<!-- OPENSPEC:START -->');
expect(content).toContain("@/openspec/AGENTS.md");
expect(content).toContain('openspec update');
expect(content).toContain('OpenSpec Instructions');
expect(content).toContain('<!-- OPENSPEC:END -->');
});
@@ -321,8 +319,7 @@ Old content
const updated = await fs.readFile(rootAgentsPath, 'utf-8');
expect(updated).toContain('# Custom intro');
expect(updated).toContain('# Footnotes');
expect(updated).toContain("@/openspec/AGENTS.md");
expect(updated).toContain('openspec update');
expect(updated).toContain('OpenSpec Instructions');
expect(updated).not.toContain('Old content');
const [logMessage] = consoleSpy.mock.calls[0];
+1 -36
View File
@@ -248,40 +248,5 @@ Line 5 with gap`;
const result = await fs.readFile(filePath, 'utf-8');
expect(result).toContain(content);
});
it('should ignore inline mentions of markers when updating content', async () => {
const filePath = path.join(testDir, 'inline-mentions.md');
const existingFile = `Intro referencing markers like ${START_MARKER} and ${END_MARKER} inside text.
${START_MARKER}
Original content
${END_MARKER}
`;
await fs.writeFile(filePath, existingFile);
await FileSystemUtils.updateFileWithMarkers(
filePath,
'Updated content',
START_MARKER,
END_MARKER
);
const firstResult = await fs.readFile(filePath, 'utf-8');
expect(firstResult).toContain('Intro referencing markers like');
expect(firstResult).toContain('Updated content');
expect(firstResult.match(new RegExp(START_MARKER, 'g'))?.length).toBe(2);
expect(firstResult.match(new RegExp(END_MARKER, 'g'))?.length).toBe(2);
await FileSystemUtils.updateFileWithMarkers(
filePath,
'Updated content',
START_MARKER,
END_MARKER
);
const secondResult = await fs.readFile(filePath, 'utf-8');
expect(secondResult).toBe(firstResult);
});
});
});
});