clearup
This commit is contained in:
parent
b1b38b485e
commit
aebc4011c7
|
|
@ -0,0 +1,152 @@
|
|||
---
|
||||
name: "OPSX: Apply"
|
||||
description: Implement tasks from an OpenSpec change (Experimental)
|
||||
category: Workflow
|
||||
tags: [workflow, artifacts, experimental]
|
||||
---
|
||||
|
||||
Implement tasks from an OpenSpec change.
|
||||
|
||||
**Input**: Optionally specify a change name (e.g., `/opsx:apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Select the change**
|
||||
|
||||
If a name is provided, use it. Otherwise:
|
||||
- Infer from conversation context if the user mentioned a change
|
||||
- Auto-select if only one active change exists
|
||||
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
|
||||
|
||||
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
|
||||
|
||||
2. **Check status to understand the schema**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
||||
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
||||
|
||||
3. **Get apply instructions**
|
||||
|
||||
```bash
|
||||
openspec instructions apply --change "<name>" --json
|
||||
```
|
||||
|
||||
This returns:
|
||||
- Context file paths (varies by schema)
|
||||
- Progress (total, complete, remaining)
|
||||
- Task list with status
|
||||
- Dynamic instruction based on current state
|
||||
|
||||
**Handle states:**
|
||||
- If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx:continue`
|
||||
- If `state: "all_done"`: congratulate, suggest archive
|
||||
- Otherwise: proceed to implementation
|
||||
|
||||
4. **Read context files**
|
||||
|
||||
Read the files listed in `contextFiles` from the apply instructions output.
|
||||
The files depend on the schema being used:
|
||||
- **spec-driven**: proposal, specs, design, tasks
|
||||
- Other schemas: follow the contextFiles from CLI output
|
||||
|
||||
5. **Show current progress**
|
||||
|
||||
Display:
|
||||
- Schema being used
|
||||
- Progress: "N/M tasks complete"
|
||||
- Remaining tasks overview
|
||||
- Dynamic instruction from CLI
|
||||
|
||||
6. **Implement tasks (loop until done or blocked)**
|
||||
|
||||
For each pending task:
|
||||
- Show which task is being worked on
|
||||
- Make the code changes required
|
||||
- Keep changes minimal and focused
|
||||
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
|
||||
- Continue to next task
|
||||
|
||||
**Pause if:**
|
||||
- Task is unclear → ask for clarification
|
||||
- Implementation reveals a design issue → suggest updating artifacts
|
||||
- Error or blocker encountered → report and wait for guidance
|
||||
- User interrupts
|
||||
|
||||
7. **On completion or pause, show status**
|
||||
|
||||
Display:
|
||||
- Tasks completed this session
|
||||
- Overall progress: "N/M tasks complete"
|
||||
- If all done: suggest archive
|
||||
- If paused: explain why and wait for guidance
|
||||
|
||||
**Output During Implementation**
|
||||
|
||||
```
|
||||
## Implementing: <change-name> (schema: <schema-name>)
|
||||
|
||||
Working on task 3/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
|
||||
Working on task 4/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
```
|
||||
|
||||
**Output On Completion**
|
||||
|
||||
```
|
||||
## Implementation Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 7/7 tasks complete ✓
|
||||
|
||||
### Completed This Session
|
||||
- [x] Task 1
|
||||
- [x] Task 2
|
||||
...
|
||||
|
||||
All tasks complete! You can archive this change with `/opsx:archive`.
|
||||
```
|
||||
|
||||
**Output On Pause (Issue Encountered)**
|
||||
|
||||
```
|
||||
## Implementation Paused
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 4/7 tasks complete
|
||||
|
||||
### Issue Encountered
|
||||
<description of the issue>
|
||||
|
||||
**Options:**
|
||||
1. <option 1>
|
||||
2. <option 2>
|
||||
3. Other approach
|
||||
|
||||
What would you like to do?
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Keep going through tasks until done or blocked
|
||||
- Always read context files before starting (from the apply instructions output)
|
||||
- If task is ambiguous, pause and ask before implementing
|
||||
- If implementation reveals issues, pause and suggest artifact updates
|
||||
- Keep code changes minimal and scoped to each task
|
||||
- Update task checkbox immediately after completing each task
|
||||
- Pause on errors, blockers, or unclear requirements - don't guess
|
||||
- Use contextFiles from CLI output, don't assume specific file names
|
||||
|
||||
**Fluid Workflow Integration**
|
||||
|
||||
This skill supports the "actions on a change" model:
|
||||
|
||||
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
|
||||
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
---
|
||||
name: "OPSX: Archive"
|
||||
description: Archive a completed change in the experimental workflow
|
||||
category: Workflow
|
||||
tags: [workflow, archive, experimental]
|
||||
---
|
||||
|
||||
Archive a completed change in the experimental workflow.
|
||||
|
||||
**Input**: Optionally specify a change name after `/opsx:archive` (e.g., `/opsx:archive add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||
|
||||
Show only active changes (not already archived).
|
||||
Include the schema used for each change if available.
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check artifact completion status**
|
||||
|
||||
Run `openspec status --change "<name>" --json` to check artifact completion.
|
||||
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used
|
||||
- `artifacts`: List of artifacts with their status (`done` or other)
|
||||
|
||||
**If any artifacts are not `done`:**
|
||||
- Display warning listing incomplete artifacts
|
||||
- Prompt user for confirmation to continue
|
||||
- Proceed if user confirms
|
||||
|
||||
3. **Check task completion status**
|
||||
|
||||
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
|
||||
|
||||
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
|
||||
|
||||
**If incomplete tasks found:**
|
||||
- Display warning showing count of incomplete tasks
|
||||
- Prompt user for confirmation to continue
|
||||
- Proceed if user confirms
|
||||
|
||||
**If no tasks file exists:** Proceed without task-related warning.
|
||||
|
||||
4. **Assess delta spec sync state**
|
||||
|
||||
Check for delta specs at `openspec/changes/<name>/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`
|
||||
- Determine what changes would be applied (adds, modifications, removals, renames)
|
||||
- Show a combined summary before prompting
|
||||
|
||||
**Prompt options:**
|
||||
- If changes needed: "Sync now (recommended)", "Archive without syncing"
|
||||
- If already synced: "Archive now", "Sync anyway", "Cancel"
|
||||
|
||||
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
|
||||
|
||||
5. **Perform the archive**
|
||||
|
||||
Create the archive directory if it doesn't exist:
|
||||
```bash
|
||||
mkdir -p openspec/changes/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
|
||||
|
||||
```bash
|
||||
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
||||
```
|
||||
|
||||
6. **Display summary**
|
||||
|
||||
Show archive completion summary including:
|
||||
- Change name
|
||||
- Schema that was used
|
||||
- Archive location
|
||||
- Spec sync status (synced / sync skipped / no delta specs)
|
||||
- Note about any warnings (incomplete artifacts/tasks)
|
||||
|
||||
**Output On Success**
|
||||
|
||||
```
|
||||
## Archive Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** ✓ Synced to main specs
|
||||
|
||||
All artifacts complete. All tasks complete.
|
||||
```
|
||||
|
||||
**Output On Success (No Delta Specs)**
|
||||
|
||||
```
|
||||
## Archive Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** No delta specs
|
||||
|
||||
All artifacts complete. All tasks complete.
|
||||
```
|
||||
|
||||
**Output On Success With Warnings**
|
||||
|
||||
```
|
||||
## Archive Complete (with warnings)
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** Sync skipped (user chose to skip)
|
||||
|
||||
**Warnings:**
|
||||
- Archived with 2 incomplete artifacts
|
||||
- Archived with 3 incomplete tasks
|
||||
- Delta spec sync was skipped (user chose to skip)
|
||||
|
||||
Review the archive if this was not intentional.
|
||||
```
|
||||
|
||||
**Output On Error (Archive Exists)**
|
||||
|
||||
```
|
||||
## Archive Failed
|
||||
|
||||
**Change:** <change-name>
|
||||
**Target:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
|
||||
Target archive directory already exists.
|
||||
|
||||
**Options:**
|
||||
1. Rename the existing archive
|
||||
2. Delete the existing archive if it's a duplicate
|
||||
3. Wait until a different date to archive
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Always prompt for change selection if not provided
|
||||
- Use artifact graph (openspec status --json) for completion checking
|
||||
- Don't block archive on warnings - just inform and confirm
|
||||
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
||||
- Show clear summary of what happened
|
||||
- If sync is requested, use the Skill tool to invoke `openspec-sync-specs` (agent-driven)
|
||||
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
---
|
||||
name: "OPSX: Bulk Archive"
|
||||
description: Archive multiple completed changes at once
|
||||
category: Workflow
|
||||
tags: [workflow, archive, experimental, bulk]
|
||||
---
|
||||
|
||||
Archive multiple completed changes in a single operation.
|
||||
|
||||
This skill allows you to batch-archive changes, handling spec conflicts intelligently by checking the codebase to determine what's actually implemented.
|
||||
|
||||
**Input**: None required (prompts for selection)
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Get active changes**
|
||||
|
||||
Run `openspec list --json` to get all active changes.
|
||||
|
||||
If no active changes exist, inform user and stop.
|
||||
|
||||
2. **Prompt for change selection**
|
||||
|
||||
Use **AskUserQuestion tool** with multi-select to let user choose changes:
|
||||
- Show each change with its schema
|
||||
- Include an option for "All changes"
|
||||
- Allow any number of selections (1+ works, 2+ is the typical use case)
|
||||
|
||||
**IMPORTANT**: Do NOT auto-select. Always let the user choose.
|
||||
|
||||
3. **Batch validation - gather status for all selected changes**
|
||||
|
||||
For each selected change, collect:
|
||||
|
||||
a. **Artifact status** - Run `openspec status --change "<name>" --json`
|
||||
- Parse `schemaName` and `artifacts` list
|
||||
- Note which artifacts are `done` vs other states
|
||||
|
||||
b. **Task completion** - Read `openspec/changes/<name>/tasks.md`
|
||||
- Count `- [ ]` (incomplete) vs `- [x]` (complete)
|
||||
- If no tasks file exists, note as "No tasks"
|
||||
|
||||
c. **Delta specs** - Check `openspec/changes/<name>/specs/` directory
|
||||
- List which capability specs exist
|
||||
- For each, extract requirement names (lines matching `### Requirement: <name>`)
|
||||
|
||||
4. **Detect spec conflicts**
|
||||
|
||||
Build a map of `capability -> [changes that touch it]`:
|
||||
|
||||
```
|
||||
auth -> [change-a, change-b] <- CONFLICT (2+ changes)
|
||||
api -> [change-c] <- OK (only 1 change)
|
||||
```
|
||||
|
||||
A conflict exists when 2+ selected changes have delta specs for the same capability.
|
||||
|
||||
5. **Resolve conflicts agentically**
|
||||
|
||||
**For each conflict**, investigate the codebase:
|
||||
|
||||
a. **Read the delta specs** from each conflicting change to understand what each claims to add/modify
|
||||
|
||||
b. **Search the codebase** for implementation evidence:
|
||||
- Look for code implementing requirements from each delta spec
|
||||
- Check for related files, functions, or tests
|
||||
|
||||
c. **Determine resolution**:
|
||||
- If only one change is actually implemented -> sync that one's specs
|
||||
- If both implemented -> apply in chronological order (older first, newer overwrites)
|
||||
- If neither implemented -> skip spec sync, warn user
|
||||
|
||||
d. **Record resolution** for each conflict:
|
||||
- Which change's specs to apply
|
||||
- In what order (if both)
|
||||
- Rationale (what was found in codebase)
|
||||
|
||||
6. **Show consolidated status table**
|
||||
|
||||
Display a table summarizing all changes:
|
||||
|
||||
```
|
||||
| Change | Artifacts | Tasks | Specs | Conflicts | Status |
|
||||
|---------------------|-----------|-------|---------|-----------|--------|
|
||||
| schema-management | Done | 5/5 | 2 delta | None | Ready |
|
||||
| project-config | Done | 3/3 | 1 delta | None | Ready |
|
||||
| add-oauth | Done | 4/4 | 1 delta | auth (!) | Ready* |
|
||||
| add-verify-skill | 1 left | 2/5 | None | None | Warn |
|
||||
```
|
||||
|
||||
For conflicts, show the resolution:
|
||||
```
|
||||
* Conflict resolution:
|
||||
- auth spec: Will apply add-oauth then add-jwt (both implemented, chronological order)
|
||||
```
|
||||
|
||||
For incomplete changes, show warnings:
|
||||
```
|
||||
Warnings:
|
||||
- add-verify-skill: 1 incomplete artifact, 3 incomplete tasks
|
||||
```
|
||||
|
||||
7. **Confirm batch operation**
|
||||
|
||||
Use **AskUserQuestion tool** with a single confirmation:
|
||||
|
||||
- "Archive N changes?" with options based on status
|
||||
- Options might include:
|
||||
- "Archive all N changes"
|
||||
- "Archive only N ready changes (skip incomplete)"
|
||||
- "Cancel"
|
||||
|
||||
If there are incomplete changes, make clear they'll be archived with warnings.
|
||||
|
||||
8. **Execute archive for each confirmed change**
|
||||
|
||||
Process changes in the determined order (respecting conflict resolution):
|
||||
|
||||
a. **Sync specs** if delta specs exist:
|
||||
- Use the openspec-sync-specs approach (agent-driven intelligent merge)
|
||||
- For conflicts, apply in resolved order
|
||||
- Track if sync was done
|
||||
|
||||
b. **Perform the archive**:
|
||||
```bash
|
||||
mkdir -p openspec/changes/archive
|
||||
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
||||
```
|
||||
|
||||
c. **Track outcome** for each change:
|
||||
- Success: archived successfully
|
||||
- Failed: error during archive (record error)
|
||||
- Skipped: user chose not to archive (if applicable)
|
||||
|
||||
9. **Display summary**
|
||||
|
||||
Show final results:
|
||||
|
||||
```
|
||||
## Bulk Archive Complete
|
||||
|
||||
Archived 3 changes:
|
||||
- schema-management-cli -> archive/2026-01-19-schema-management-cli/
|
||||
- project-config -> archive/2026-01-19-project-config/
|
||||
- add-oauth -> archive/2026-01-19-add-oauth/
|
||||
|
||||
Skipped 1 change:
|
||||
- add-verify-skill (user chose not to archive incomplete)
|
||||
|
||||
Spec sync summary:
|
||||
- 4 delta specs synced to main specs
|
||||
- 1 conflict resolved (auth: applied both in chronological order)
|
||||
```
|
||||
|
||||
If any failures:
|
||||
```
|
||||
Failed 1 change:
|
||||
- some-change: Archive directory already exists
|
||||
```
|
||||
|
||||
**Conflict Resolution Examples**
|
||||
|
||||
Example 1: Only one implemented
|
||||
```
|
||||
Conflict: specs/auth/spec.md touched by [add-oauth, add-jwt]
|
||||
|
||||
Checking add-oauth:
|
||||
- Delta adds "OAuth Provider Integration" requirement
|
||||
- Searching codebase... found src/auth/oauth.ts implementing OAuth flow
|
||||
|
||||
Checking add-jwt:
|
||||
- Delta adds "JWT Token Handling" requirement
|
||||
- Searching codebase... no JWT implementation found
|
||||
|
||||
Resolution: Only add-oauth is implemented. Will sync add-oauth specs only.
|
||||
```
|
||||
|
||||
Example 2: Both implemented
|
||||
```
|
||||
Conflict: specs/api/spec.md touched by [add-rest-api, add-graphql]
|
||||
|
||||
Checking add-rest-api (created 2026-01-10):
|
||||
- Delta adds "REST Endpoints" requirement
|
||||
- Searching codebase... found src/api/rest.ts
|
||||
|
||||
Checking add-graphql (created 2026-01-15):
|
||||
- Delta adds "GraphQL Schema" requirement
|
||||
- Searching codebase... found src/api/graphql.ts
|
||||
|
||||
Resolution: Both implemented. Will apply add-rest-api specs first,
|
||||
then add-graphql specs (chronological order, newer takes precedence).
|
||||
```
|
||||
|
||||
**Output On Success**
|
||||
|
||||
```
|
||||
## Bulk Archive Complete
|
||||
|
||||
Archived N changes:
|
||||
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
|
||||
- <change-2> -> archive/YYYY-MM-DD-<change-2>/
|
||||
|
||||
Spec sync summary:
|
||||
- N delta specs synced to main specs
|
||||
- No conflicts (or: M conflicts resolved)
|
||||
```
|
||||
|
||||
**Output On Partial Success**
|
||||
|
||||
```
|
||||
## Bulk Archive Complete (partial)
|
||||
|
||||
Archived N changes:
|
||||
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
|
||||
|
||||
Skipped M changes:
|
||||
- <change-2> (user chose not to archive incomplete)
|
||||
|
||||
Failed K changes:
|
||||
- <change-3>: Archive directory already exists
|
||||
```
|
||||
|
||||
**Output When No Changes**
|
||||
|
||||
```
|
||||
## No Changes to Archive
|
||||
|
||||
No active changes found. Create a new change to get started.
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Allow any number of changes (1+ is fine, 2+ is the typical use case)
|
||||
- Always prompt for selection, never auto-select
|
||||
- Detect spec conflicts early and resolve by checking codebase
|
||||
- When both changes are implemented, apply specs in chronological order
|
||||
- Skip spec sync only when implementation is missing (warn user)
|
||||
- Show clear per-change status before confirming
|
||||
- Use single confirmation for entire batch
|
||||
- Track and report all outcomes (success/skip/fail)
|
||||
- Preserve .openspec.yaml when moving to archive
|
||||
- Archive directory target uses current date: YYYY-MM-DD-<name>
|
||||
- If archive target exists, fail that change but continue with others
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
---
|
||||
name: "OPSX: Continue"
|
||||
description: Continue working on a change - create the next artifact (Experimental)
|
||||
category: Workflow
|
||||
tags: [workflow, artifacts, experimental]
|
||||
---
|
||||
|
||||
Continue working on a change by creating the next artifact.
|
||||
|
||||
**Input**: Optionally specify a change name after `/opsx:continue` (e.g., `/opsx:continue add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes sorted by most recently modified. Then use the **AskUserQuestion tool** to let the user select which change to work on.
|
||||
|
||||
Present the top 3-4 most recently modified changes as options, showing:
|
||||
- Change name
|
||||
- Schema (from `schema` field if present, otherwise "spec-driven")
|
||||
- Status (e.g., "0/5 tasks", "complete", "no tasks")
|
||||
- How recently it was modified (from `lastModified` field)
|
||||
|
||||
Mark the most recently modified change as "(Recommended)" since it's likely what the user wants to continue.
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check current status**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to understand current state. The response includes:
|
||||
- `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
|
||||
|
||||
3. **Act based on status**:
|
||||
|
||||
---
|
||||
|
||||
**If all artifacts are complete (`isComplete: true`)**:
|
||||
- Congratulate the user
|
||||
- Show final status including the schema used
|
||||
- Suggest: "All artifacts created! You can now implement this change with `/opsx:apply` or archive it with `/opsx:archive`."
|
||||
- STOP
|
||||
|
||||
---
|
||||
|
||||
**If artifacts are ready to create** (status shows artifacts with `status: "ready"`):
|
||||
- Pick the FIRST artifact with `status: "ready"` from the status output
|
||||
- Get its instructions:
|
||||
```bash
|
||||
openspec instructions <artifact-id> --change "<name>" --json
|
||||
```
|
||||
- Parse the JSON. The key fields are:
|
||||
- `context`: Project background (constraints for you - do NOT include in output)
|
||||
- `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
|
||||
- `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
|
||||
- Show what was created and what's now unlocked
|
||||
- STOP after creating ONE artifact
|
||||
|
||||
---
|
||||
|
||||
**If no artifacts are ready (all blocked)**:
|
||||
- This shouldn't happen with a valid schema
|
||||
- Show status and suggest checking for issues
|
||||
|
||||
4. **After creating an artifact, show progress**
|
||||
```bash
|
||||
openspec status --change "<name>"
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
After each invocation, show:
|
||||
- Which artifact was created
|
||||
- Schema workflow being used
|
||||
- Current progress (N/M complete)
|
||||
- What artifacts are now unlocked
|
||||
- Prompt: "Run `/opsx:continue` to create the next artifact"
|
||||
|
||||
**Artifact Creation Guidelines**
|
||||
|
||||
The artifact types and their purpose depend on the schema. Use the `instruction` field from the instructions output to understand what to create.
|
||||
|
||||
Common artifact patterns:
|
||||
|
||||
**spec-driven schema** (proposal → specs → design → tasks):
|
||||
- **proposal.md**: Ask user about the change if not clear. Fill in Why, What Changes, Capabilities, Impact.
|
||||
- The Capabilities section is critical - each capability listed will need a spec file.
|
||||
- **specs/<capability>/spec.md**: Create one spec per capability listed in the proposal's Capabilities section (use the capability name, not the change name).
|
||||
- **design.md**: Document technical decisions, architecture, and implementation approach.
|
||||
- **tasks.md**: Break down implementation into checkboxed tasks.
|
||||
|
||||
For other schemas, follow the `instruction` field from the CLI output.
|
||||
|
||||
**Guardrails**
|
||||
- Create ONE artifact per invocation
|
||||
- Always read dependency artifacts before creating a new one
|
||||
- Never skip artifacts or create out of order
|
||||
- If context is unclear, ask the user before creating
|
||||
- Verify the artifact file exists after writing before marking progress
|
||||
- Use the schema's artifact sequence, don't assume specific artifact names
|
||||
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
||||
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
||||
- These guide what you write, but should never appear in the output
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
---
|
||||
name: "OPSX: Explore"
|
||||
description: "Enter explore mode - think through ideas, investigate problems, clarify requirements"
|
||||
category: Workflow
|
||||
tags: [workflow, explore, experimental, thinking]
|
||||
---
|
||||
|
||||
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
|
||||
|
||||
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
|
||||
|
||||
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
|
||||
|
||||
**Input**: The argument after `/opsx:explore` is whatever the user wants to think about. Could be:
|
||||
- A vague idea: "real-time collaboration"
|
||||
- A specific problem: "the auth system is getting unwieldy"
|
||||
- A change name: "add-dark-mode" (to explore in context of that change)
|
||||
- A comparison: "postgres vs sqlite for this"
|
||||
- Nothing (just enter explore mode)
|
||||
|
||||
---
|
||||
|
||||
## The Stance
|
||||
|
||||
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
|
||||
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
|
||||
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
|
||||
- **Adaptive** - Follow interesting threads, pivot when new information emerges
|
||||
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
|
||||
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
|
||||
|
||||
---
|
||||
|
||||
## What You Might Do
|
||||
|
||||
Depending on what the user brings, you might:
|
||||
|
||||
**Explore the problem space**
|
||||
- Ask clarifying questions that emerge from what they said
|
||||
- Challenge assumptions
|
||||
- Reframe the problem
|
||||
- Find analogies
|
||||
|
||||
**Investigate the codebase**
|
||||
- Map existing architecture relevant to the discussion
|
||||
- Find integration points
|
||||
- Identify patterns already in use
|
||||
- Surface hidden complexity
|
||||
|
||||
**Compare options**
|
||||
- Brainstorm multiple approaches
|
||||
- Build comparison tables
|
||||
- Sketch tradeoffs
|
||||
- Recommend a path (if asked)
|
||||
|
||||
**Visualize**
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Use ASCII diagrams liberally │
|
||||
├─────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌────────┐ ┌────────┐ │
|
||||
│ │ State │────────▶│ State │ │
|
||||
│ │ A │ │ B │ │
|
||||
│ └────────┘ └────────┘ │
|
||||
│ │
|
||||
│ System diagrams, state machines, │
|
||||
│ data flows, architecture sketches, │
|
||||
│ dependency graphs, comparison tables │
|
||||
│ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Surface risks and unknowns**
|
||||
- Identify what could go wrong
|
||||
- Find gaps in understanding
|
||||
- Suggest spikes or investigations
|
||||
|
||||
---
|
||||
|
||||
## OpenSpec Awareness
|
||||
|
||||
You have full context of the OpenSpec system. Use it naturally, don't force it.
|
||||
|
||||
### Check for context
|
||||
|
||||
At the start, quickly check what exists:
|
||||
```bash
|
||||
openspec list --json
|
||||
```
|
||||
|
||||
This tells you:
|
||||
- If there are active changes
|
||||
- Their names, schemas, and status
|
||||
- What the user might be working on
|
||||
|
||||
If the user mentioned a specific change name, read its artifacts for context.
|
||||
|
||||
### When no change exists
|
||||
|
||||
Think freely. When insights crystallize, you might offer:
|
||||
|
||||
- "This feels solid enough to start a change. Want me to create a proposal?"
|
||||
- Or keep exploring - no pressure to formalize
|
||||
|
||||
### When a change exists
|
||||
|
||||
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.
|
||||
|
||||
2. **Reference them naturally in conversation**
|
||||
- "Your design mentions using Redis, but we just realized SQLite fits better..."
|
||||
- "The proposal scopes this to premium users, but we're now thinking everyone..."
|
||||
|
||||
3. **Offer to capture when decisions are made**
|
||||
|
||||
| Insight Type | Where to Capture |
|
||||
|--------------|------------------|
|
||||
| New requirement discovered | `specs/<capability>/spec.md` |
|
||||
| Requirement changed | `specs/<capability>/spec.md` |
|
||||
| Design decision made | `design.md` |
|
||||
| Scope changed | `proposal.md` |
|
||||
| New work identified | `tasks.md` |
|
||||
| Assumption invalidated | Relevant artifact |
|
||||
|
||||
Example offers:
|
||||
- "That's a design decision. Capture it in design.md?"
|
||||
- "This is a new requirement. Add it to specs?"
|
||||
- "This changes scope. Update the proposal?"
|
||||
|
||||
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
|
||||
|
||||
---
|
||||
|
||||
## What You Don't Have To Do
|
||||
|
||||
- Follow a script
|
||||
- Ask the same questions every time
|
||||
- Produce a specific artifact
|
||||
- Reach a conclusion
|
||||
- Stay on topic if a tangent is valuable
|
||||
- Be brief (this is thinking time)
|
||||
|
||||
---
|
||||
|
||||
## Ending Discovery
|
||||
|
||||
There's no required ending. Discovery might:
|
||||
|
||||
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
|
||||
- **Result in artifact updates**: "Updated design.md with these decisions"
|
||||
- **Just provide clarity**: User has what they need, moves on
|
||||
- **Continue later**: "We can pick this up anytime"
|
||||
|
||||
When things crystallize, you might offer a summary - but it's optional. Sometimes the thinking IS the value.
|
||||
|
||||
---
|
||||
|
||||
## Guardrails
|
||||
|
||||
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
|
||||
- **Don't fake understanding** - If something is unclear, dig deeper
|
||||
- **Don't rush** - Discovery is thinking time, not task time
|
||||
- **Don't force structure** - Let patterns emerge naturally
|
||||
- **Don't auto-capture** - Offer to save insights, don't just do it
|
||||
- **Do visualize** - A good diagram is worth many paragraphs
|
||||
- **Do explore the codebase** - Ground discussions in reality
|
||||
- **Do question assumptions** - Including the user's and your own
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
---
|
||||
name: "OPSX: Fast Forward"
|
||||
description: Create a change and generate all artifacts needed for implementation in one go
|
||||
category: Workflow
|
||||
tags: [workflow, artifacts, experimental]
|
||||
---
|
||||
|
||||
Fast-forward through artifact creation - generate everything needed to start implementation.
|
||||
|
||||
**Input**: The argument after `/opsx:ff` is the change name (kebab-case), OR a description of what the user wants to build.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no input provided, ask what they want to build**
|
||||
|
||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||
> "What change do you want to work on? Describe what you want to build or fix."
|
||||
|
||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||
|
||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||
|
||||
2. **Create the change directory**
|
||||
```bash
|
||||
openspec new change "<name>"
|
||||
```
|
||||
This creates a scaffolded change at `openspec/changes/<name>/`.
|
||||
|
||||
3. **Get the artifact build order**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
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
|
||||
|
||||
4. **Create artifacts in sequence until apply-ready**
|
||||
|
||||
Use the **TodoWrite tool** to track progress through the artifacts.
|
||||
|
||||
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
||||
|
||||
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
||||
- Get instructions:
|
||||
```bash
|
||||
openspec instructions <artifact-id> --change "<name>" --json
|
||||
```
|
||||
- The instructions JSON includes:
|
||||
- `context`: Project background (constraints for you - do NOT include in output)
|
||||
- `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
|
||||
- `dependencies`: Completed artifacts to read for context
|
||||
- Read any completed dependency files for context
|
||||
- Create the artifact file using `template` as the structure
|
||||
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
||||
- Show brief progress: "✓ Created <artifact-id>"
|
||||
|
||||
b. **Continue until all `applyRequires` artifacts are complete**
|
||||
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
||||
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
||||
- Stop when all `applyRequires` artifacts are done
|
||||
|
||||
c. **If an artifact requires user input** (unclear context):
|
||||
- Use **AskUserQuestion tool** to clarify
|
||||
- Then continue with creation
|
||||
|
||||
5. **Show final status**
|
||||
```bash
|
||||
openspec status --change "<name>"
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
After completing all artifacts, summarize:
|
||||
- Change name and location
|
||||
- List of artifacts created with brief descriptions
|
||||
- What's ready: "All artifacts created! Ready for implementation."
|
||||
- Prompt: "Run `/opsx:apply` to start implementing."
|
||||
|
||||
**Artifact Creation Guidelines**
|
||||
|
||||
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
||||
- The schema defines what each artifact should contain - follow it
|
||||
- Read dependency artifacts for context before creating new ones
|
||||
- Use `template` as the structure for your output file - fill in its sections
|
||||
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
||||
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
||||
- These guide what you write, but should never appear in the output
|
||||
|
||||
**Guardrails**
|
||||
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
||||
- Always read dependency artifacts before creating a new one
|
||||
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
||||
- If a change with that name already exists, ask if user wants to continue it or create a new one
|
||||
- Verify each artifact file exists after writing before proceeding to next
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
---
|
||||
name: "OPSX: New"
|
||||
description: Start a new change using the experimental artifact workflow (OPSX)
|
||||
category: Workflow
|
||||
tags: [workflow, artifacts, experimental]
|
||||
---
|
||||
|
||||
Start a new change using the experimental artifact-driven approach.
|
||||
|
||||
**Input**: The argument after `/opsx:new` is the change name (kebab-case), OR a description of what the user wants to build.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no input provided, ask what they want to build**
|
||||
|
||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||
> "What change do you want to work on? Describe what you want to build or fix."
|
||||
|
||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||
|
||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||
|
||||
2. **Determine the workflow schema**
|
||||
|
||||
Use the default schema (omit `--schema`) unless the user explicitly requests a different workflow.
|
||||
|
||||
**Use a different schema only if the user mentions:**
|
||||
- A specific schema name → use `--schema <name>`
|
||||
- "show workflows" or "what workflows" → run `openspec schemas --json` and let them choose
|
||||
|
||||
**Otherwise**: Omit `--schema` to use the default.
|
||||
|
||||
3. **Create the change directory**
|
||||
```bash
|
||||
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.
|
||||
|
||||
4. **Show the artifact status**
|
||||
```bash
|
||||
openspec status --change "<name>"
|
||||
```
|
||||
This shows which artifacts need to be created and which are ready (dependencies satisfied).
|
||||
|
||||
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".
|
||||
```bash
|
||||
openspec instructions <first-artifact-id> --change "<name>"
|
||||
```
|
||||
This outputs the template and context for creating the first artifact.
|
||||
|
||||
6. **STOP and wait for user direction**
|
||||
|
||||
**Output**
|
||||
|
||||
After completing the steps, summarize:
|
||||
- Change name and location
|
||||
- Schema/workflow being used and its artifact sequence
|
||||
- Current status (0/N artifacts complete)
|
||||
- The template for the first artifact
|
||||
- Prompt: "Ready to create the first artifact? Run `/opsx:continue` or just describe what this change is about and I'll draft it."
|
||||
|
||||
**Guardrails**
|
||||
- Do NOT create any artifacts yet - just show the instructions
|
||||
- Do NOT advance beyond showing the first artifact template
|
||||
- If the name is invalid (not kebab-case), ask for a valid name
|
||||
- If a change with that name already exists, suggest using `/opsx:continue` instead
|
||||
- Pass --schema if using a non-default workflow
|
||||
|
|
@ -0,0 +1,550 @@
|
|||
---
|
||||
name: "OPSX: Onboard"
|
||||
description: Guided onboarding - walk through a complete OpenSpec workflow cycle with narration
|
||||
category: Workflow
|
||||
tags: [workflow, onboarding, tutorial, learning]
|
||||
---
|
||||
|
||||
Guide the user through their first complete OpenSpec workflow cycle. This is a teaching experience—you'll do real work in their codebase while explaining each step.
|
||||
|
||||
---
|
||||
|
||||
## Preflight
|
||||
|
||||
Before starting, check if the OpenSpec CLI is installed:
|
||||
|
||||
```bash
|
||||
# Unix/macOS
|
||||
openspec --version 2>&1 || echo "CLI_NOT_INSTALLED"
|
||||
# Windows (PowerShell)
|
||||
# if (Get-Command openspec -ErrorAction SilentlyContinue) { openspec --version } else { echo "CLI_NOT_INSTALLED" }
|
||||
```
|
||||
|
||||
**If CLI not installed:**
|
||||
> OpenSpec CLI is not installed. Install it first, then come back to `/opsx:onboard`.
|
||||
|
||||
Stop here if not installed.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Welcome
|
||||
|
||||
Display:
|
||||
|
||||
```
|
||||
## Welcome to OpenSpec!
|
||||
|
||||
I'll walk you through a complete change cycle—from idea to implementation—using a real task in your codebase. Along the way, you'll learn the workflow by doing it.
|
||||
|
||||
**What we'll do:**
|
||||
1. Pick a small, real task in your codebase
|
||||
2. Explore the problem briefly
|
||||
3. Create a change (the container for our work)
|
||||
4. Build the artifacts: proposal → specs → design → tasks
|
||||
5. Implement the tasks
|
||||
6. Archive the completed change
|
||||
|
||||
**Time:** ~15-20 minutes
|
||||
|
||||
Let's start by finding something to work on.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Task Selection
|
||||
|
||||
### Codebase Analysis
|
||||
|
||||
Scan the codebase for small improvement opportunities. Look for:
|
||||
|
||||
1. **TODO/FIXME comments** - Search for `TODO`, `FIXME`, `HACK`, `XXX` in code files
|
||||
2. **Missing error handling** - `catch` blocks that swallow errors, risky operations without try-catch
|
||||
3. **Functions without tests** - Cross-reference `src/` with test directories
|
||||
4. **Type issues** - `any` types in TypeScript files (`: any`, `as any`)
|
||||
5. **Debug artifacts** - `console.log`, `console.debug`, `debugger` statements in non-debug code
|
||||
6. **Missing validation** - User input handlers without validation
|
||||
|
||||
Also check recent git activity:
|
||||
```bash
|
||||
# Unix/macOS
|
||||
git log --oneline -10 2>/dev/null || echo "No git history"
|
||||
# Windows (PowerShell)
|
||||
# git log --oneline -10 2>$null; if ($LASTEXITCODE -ne 0) { echo "No git history" }
|
||||
```
|
||||
|
||||
### Present Suggestions
|
||||
|
||||
From your analysis, present 3-4 specific suggestions:
|
||||
|
||||
```
|
||||
## Task Suggestions
|
||||
|
||||
Based on scanning your codebase, here are some good starter tasks:
|
||||
|
||||
**1. [Most promising task]**
|
||||
Location: `src/path/to/file.ts:42`
|
||||
Scope: ~1-2 files, ~20-30 lines
|
||||
Why it's good: [brief reason]
|
||||
|
||||
**2. [Second task]**
|
||||
Location: `src/another/file.ts`
|
||||
Scope: ~1 file, ~15 lines
|
||||
Why it's good: [brief reason]
|
||||
|
||||
**3. [Third task]**
|
||||
Location: [location]
|
||||
Scope: [estimate]
|
||||
Why it's good: [brief reason]
|
||||
|
||||
**4. Something else?**
|
||||
Tell me what you'd like to work on.
|
||||
|
||||
Which task interests you? (Pick a number or describe your own)
|
||||
```
|
||||
|
||||
**If nothing found:** Fall back to asking what the user wants to build:
|
||||
> I didn't find obvious quick wins in your codebase. What's something small you've been meaning to add or fix?
|
||||
|
||||
### Scope Guardrail
|
||||
|
||||
If the user picks or describes something too large (major feature, multi-day work):
|
||||
|
||||
```
|
||||
That's a valuable task, but it's probably larger than ideal for your first OpenSpec run-through.
|
||||
|
||||
For learning the workflow, smaller is better—it lets you see the full cycle without getting stuck in implementation details.
|
||||
|
||||
**Options:**
|
||||
1. **Slice it smaller** - What's the smallest useful piece of [their task]? Maybe just [specific slice]?
|
||||
2. **Pick something else** - One of the other suggestions, or a different small task?
|
||||
3. **Do it anyway** - If you really want to tackle this, we can. Just know it'll take longer.
|
||||
|
||||
What would you prefer?
|
||||
```
|
||||
|
||||
Let the user override if they insist—this is a soft guardrail.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Explore Demo
|
||||
|
||||
Once a task is selected, briefly demonstrate explore mode:
|
||||
|
||||
```
|
||||
Before we create a change, let me quickly show you **explore mode**—it's how you think through problems before committing to a direction.
|
||||
```
|
||||
|
||||
Spend 1-2 minutes investigating the relevant code:
|
||||
- Read the file(s) involved
|
||||
- Draw a quick ASCII diagram if it helps
|
||||
- Note any considerations
|
||||
|
||||
```
|
||||
## Quick Exploration
|
||||
|
||||
[Your brief analysis—what you found, any considerations]
|
||||
|
||||
┌─────────────────────────────────────────┐
|
||||
│ [Optional: ASCII diagram if helpful] │
|
||||
└─────────────────────────────────────────┘
|
||||
|
||||
Explore mode (`/opsx:explore`) is for this kind of thinking—investigating before implementing. You can use it anytime you need to think through a problem.
|
||||
|
||||
Now let's create a change to hold our work.
|
||||
```
|
||||
|
||||
**PAUSE** - Wait for user acknowledgment before proceeding.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Create the Change
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## 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.
|
||||
|
||||
Let me create one for our task.
|
||||
```
|
||||
|
||||
**DO:** Create the change with a derived kebab-case name:
|
||||
```bash
|
||||
openspec new change "<derived-name>"
|
||||
```
|
||||
|
||||
**SHOW:**
|
||||
```
|
||||
Created: `openspec/changes/<name>/`
|
||||
|
||||
The folder structure:
|
||||
```
|
||||
openspec/changes/<name>/
|
||||
├── proposal.md ← Why we're doing this (empty, we'll fill it)
|
||||
├── design.md ← How we'll build it (empty)
|
||||
├── specs/ ← Detailed requirements (empty)
|
||||
└── tasks.md ← Implementation checklist (empty)
|
||||
```
|
||||
|
||||
Now let's fill in the first artifact—the proposal.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Proposal
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## The Proposal
|
||||
|
||||
The proposal captures **why** we're making this change and **what** it involves at a high level. It's the "elevator pitch" for the work.
|
||||
|
||||
I'll draft one based on our task.
|
||||
```
|
||||
|
||||
**DO:** Draft the proposal content (don't save yet):
|
||||
|
||||
```
|
||||
Here's a draft proposal:
|
||||
|
||||
---
|
||||
|
||||
## Why
|
||||
|
||||
[1-2 sentences explaining the problem/opportunity]
|
||||
|
||||
## What Changes
|
||||
|
||||
[Bullet points of what will be different]
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `<capability-name>`: [brief description]
|
||||
|
||||
### Modified Capabilities
|
||||
<!-- If modifying existing behavior -->
|
||||
|
||||
## Impact
|
||||
|
||||
- `src/path/to/file.ts`: [what changes]
|
||||
- [other files if applicable]
|
||||
|
||||
---
|
||||
|
||||
Does this capture the intent? I can adjust before we save it.
|
||||
```
|
||||
|
||||
**PAUSE** - Wait for user approval/feedback.
|
||||
|
||||
After approval, save the proposal:
|
||||
```bash
|
||||
openspec instructions proposal --change "<name>" --json
|
||||
```
|
||||
Then write the content to `openspec/changes/<name>/proposal.md`.
|
||||
|
||||
```
|
||||
Proposal saved. This is your "why" document—you can always come back and refine it as understanding evolves.
|
||||
|
||||
Next up: specs.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Specs
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## Specs
|
||||
|
||||
Specs define **what** we're building in precise, testable terms. They use a requirement/scenario format that makes expected behavior crystal clear.
|
||||
|
||||
For a small task like this, we might only need one spec file.
|
||||
```
|
||||
|
||||
**DO:** Create the spec file:
|
||||
```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>"
|
||||
```
|
||||
|
||||
Draft the spec content:
|
||||
|
||||
```
|
||||
Here's the spec:
|
||||
|
||||
---
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: <Name>
|
||||
|
||||
<Description of what the system should do>
|
||||
|
||||
#### Scenario: <Scenario name>
|
||||
|
||||
- **WHEN** <trigger condition>
|
||||
- **THEN** <expected outcome>
|
||||
- **AND** <additional outcome if needed>
|
||||
|
||||
---
|
||||
|
||||
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`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Design
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## Design
|
||||
|
||||
The design captures **how** we'll build it—technical decisions, tradeoffs, approach.
|
||||
|
||||
For small changes, this might be brief. That's fine—not every change needs deep design discussion.
|
||||
```
|
||||
|
||||
**DO:** Draft design.md:
|
||||
|
||||
```
|
||||
Here's the design:
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
[Brief context about the current state]
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- [What we're trying to achieve]
|
||||
|
||||
**Non-Goals:**
|
||||
- [What's explicitly out of scope]
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: [Key decision]
|
||||
|
||||
[Explanation of approach and rationale]
|
||||
|
||||
---
|
||||
|
||||
For a small task, this captures the key decisions without over-engineering.
|
||||
```
|
||||
|
||||
Save to `openspec/changes/<name>/design.md`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Tasks
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## Tasks
|
||||
|
||||
Finally, we break the work into implementation tasks—checkboxes that drive the apply phase.
|
||||
|
||||
These should be small, clear, and in logical order.
|
||||
```
|
||||
|
||||
**DO:** Generate tasks based on specs and design:
|
||||
|
||||
```
|
||||
Here are the implementation tasks:
|
||||
|
||||
---
|
||||
|
||||
## 1. [Category or file]
|
||||
|
||||
- [ ] 1.1 [Specific task]
|
||||
- [ ] 1.2 [Specific task]
|
||||
|
||||
## 2. Verify
|
||||
|
||||
- [ ] 2.1 [Verification step]
|
||||
|
||||
---
|
||||
|
||||
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`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 9: Apply (Implementation)
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## Implementation
|
||||
|
||||
Now we implement each task, checking them off as we go. I'll announce each one and occasionally note how the specs/design informed the approach.
|
||||
```
|
||||
|
||||
**DO:** For each task:
|
||||
|
||||
1. Announce: "Working on task N: [description]"
|
||||
2. Implement the change in the codebase
|
||||
3. Reference specs/design naturally: "The spec says X, so I'm doing Y"
|
||||
4. Mark complete in tasks.md: `- [ ]` → `- [x]`
|
||||
5. Brief status: "✓ Task N complete"
|
||||
|
||||
Keep narration light—don't over-explain every line of code.
|
||||
|
||||
After all tasks:
|
||||
|
||||
```
|
||||
## Implementation Complete
|
||||
|
||||
All tasks done:
|
||||
- [x] Task 1
|
||||
- [x] Task 2
|
||||
- [x] ...
|
||||
|
||||
The change is implemented! One more step—let's archive it.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 10: Archive
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## Archiving
|
||||
|
||||
When a change is complete, we archive it. This moves it from `openspec/changes/` to `openspec/changes/archive/YYYY-MM-DD-<name>/`.
|
||||
|
||||
Archived changes become your project's decision history—you can always find them later to understand why something was built a certain way.
|
||||
```
|
||||
|
||||
**DO:**
|
||||
```bash
|
||||
openspec archive "<name>"
|
||||
```
|
||||
|
||||
**SHOW:**
|
||||
```
|
||||
Archived to: `openspec/changes/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.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 11: Recap & Next Steps
|
||||
|
||||
```
|
||||
## Congratulations!
|
||||
|
||||
You just completed a full OpenSpec cycle:
|
||||
|
||||
1. **Explore** - Thought through the problem
|
||||
2. **New** - Created a change container
|
||||
3. **Proposal** - Captured WHY
|
||||
4. **Specs** - Defined WHAT in detail
|
||||
5. **Design** - Decided HOW
|
||||
6. **Tasks** - Broke it into steps
|
||||
7. **Apply** - Implemented the work
|
||||
8. **Archive** - Preserved the record
|
||||
|
||||
This same rhythm works for any size change—a small fix or a major feature.
|
||||
|
||||
---
|
||||
|
||||
## Command Reference
|
||||
|
||||
**Core workflow:**
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/opsx:propose` | Create a change and generate all artifacts |
|
||||
| `/opsx:explore` | Think through problems before/during work |
|
||||
| `/opsx:apply` | Implement tasks from a change |
|
||||
| `/opsx:archive` | Archive a completed change |
|
||||
|
||||
**Additional commands:**
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/opsx:new` | Start a new change, step through artifacts one at a time |
|
||||
| `/opsx:continue` | Continue working on an existing change |
|
||||
| `/opsx:ff` | Fast-forward: create all artifacts at once |
|
||||
| `/opsx:verify` | Verify implementation matches artifacts |
|
||||
|
||||
---
|
||||
|
||||
## What's Next?
|
||||
|
||||
Try `/opsx:propose` on something you actually want to build. You've got the rhythm now!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Graceful Exit Handling
|
||||
|
||||
### User wants to stop mid-way
|
||||
|
||||
If the user says they need to stop, want to pause, or seem disengaged:
|
||||
|
||||
```
|
||||
No problem! Your change is saved at `openspec/changes/<name>/`.
|
||||
|
||||
To pick up where we left off later:
|
||||
- `/opsx:continue <name>` - Resume artifact creation
|
||||
- `/opsx:apply <name>` - Jump to implementation (if tasks exist)
|
||||
|
||||
The work won't be lost. Come back whenever you're ready.
|
||||
```
|
||||
|
||||
Exit gracefully without pressure.
|
||||
|
||||
### User just wants command reference
|
||||
|
||||
If the user says they just want to see the commands or skip the tutorial:
|
||||
|
||||
```
|
||||
## OpenSpec Quick Reference
|
||||
|
||||
**Core workflow:**
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/opsx:propose <name>` | Create a change and generate all artifacts |
|
||||
| `/opsx:explore` | Think through problems (no code changes) |
|
||||
| `/opsx:apply <name>` | Implement tasks |
|
||||
| `/opsx:archive <name>` | Archive when done |
|
||||
|
||||
**Additional commands:**
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/opsx:new <name>` | Start a new change, step by step |
|
||||
| `/opsx:continue <name>` | Continue an existing change |
|
||||
| `/opsx:ff <name>` | Fast-forward: all artifacts at once |
|
||||
| `/opsx:verify <name>` | Verify implementation |
|
||||
|
||||
Try `/opsx:propose` to start your first change.
|
||||
```
|
||||
|
||||
Exit gracefully.
|
||||
|
||||
---
|
||||
|
||||
## Guardrails
|
||||
|
||||
- **Follow the EXPLAIN → DO → SHOW → PAUSE pattern** at key transitions (after explore, after proposal draft, after tasks, after archive)
|
||||
- **Keep narration light** during implementation—teach without lecturing
|
||||
- **Don't skip phases** even if the change is small—the goal is teaching the workflow
|
||||
- **Pause for acknowledgment** at marked points, but don't over-pause
|
||||
- **Handle exits gracefully**—never pressure the user to continue
|
||||
- **Use real codebase tasks**—don't simulate or use fake examples
|
||||
- **Adjust scope gently**—guide toward smaller tasks but respect user choice
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
---
|
||||
name: "OPSX: Propose"
|
||||
description: Propose a new change - create it and generate all artifacts in one step
|
||||
category: Workflow
|
||||
tags: [workflow, artifacts, experimental]
|
||||
---
|
||||
|
||||
Propose a new change - create the change and generate all artifacts in one step.
|
||||
|
||||
I'll create a change with artifacts:
|
||||
- proposal.md (what & why)
|
||||
- design.md (how)
|
||||
- tasks.md (implementation steps)
|
||||
|
||||
When ready to implement, run /opsx:apply
|
||||
|
||||
---
|
||||
|
||||
**Input**: The argument after `/opsx:propose` is the change name (kebab-case), OR a description of what the user wants to build.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no input provided, ask what they want to build**
|
||||
|
||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||
> "What change do you want to work on? Describe what you want to build or fix."
|
||||
|
||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||
|
||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||
|
||||
2. **Create the change directory**
|
||||
```bash
|
||||
openspec new change "<name>"
|
||||
```
|
||||
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
|
||||
|
||||
3. **Get the artifact build order**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
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
|
||||
|
||||
4. **Create artifacts in sequence until apply-ready**
|
||||
|
||||
Use the **TodoWrite tool** to track progress through the artifacts.
|
||||
|
||||
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
||||
|
||||
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
||||
- Get instructions:
|
||||
```bash
|
||||
openspec instructions <artifact-id> --change "<name>" --json
|
||||
```
|
||||
- The instructions JSON includes:
|
||||
- `context`: Project background (constraints for you - do NOT include in output)
|
||||
- `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
|
||||
- `dependencies`: Completed artifacts to read for context
|
||||
- Read any completed dependency files for context
|
||||
- Create the artifact file using `template` as the structure
|
||||
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
||||
- Show brief progress: "Created <artifact-id>"
|
||||
|
||||
b. **Continue until all `applyRequires` artifacts are complete**
|
||||
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
||||
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
||||
- Stop when all `applyRequires` artifacts are done
|
||||
|
||||
c. **If an artifact requires user input** (unclear context):
|
||||
- Use **AskUserQuestion tool** to clarify
|
||||
- Then continue with creation
|
||||
|
||||
5. **Show final status**
|
||||
```bash
|
||||
openspec status --change "<name>"
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
After completing all artifacts, summarize:
|
||||
- Change name and location
|
||||
- List of artifacts created with brief descriptions
|
||||
- What's ready: "All artifacts created! Ready for implementation."
|
||||
- Prompt: "Run `/opsx:apply` to start implementing."
|
||||
|
||||
**Artifact Creation Guidelines**
|
||||
|
||||
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
||||
- The schema defines what each artifact should contain - follow it
|
||||
- Read dependency artifacts for context before creating new ones
|
||||
- Use `template` as the structure for your output file - fill in its sections
|
||||
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
||||
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
||||
- These guide what you write, but should never appear in the output
|
||||
|
||||
**Guardrails**
|
||||
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
||||
- Always read dependency artifacts before creating a new one
|
||||
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
||||
- If a change with that name already exists, ask if user wants to continue it or create a new one
|
||||
- Verify each artifact file exists after writing before proceeding to next
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
---
|
||||
name: "OPSX: Sync"
|
||||
description: Sync delta specs from a change to main specs
|
||||
category: Workflow
|
||||
tags: [workflow, specs, experimental]
|
||||
---
|
||||
|
||||
Sync delta specs from a change to main specs.
|
||||
|
||||
This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement).
|
||||
|
||||
**Input**: Optionally specify a change name after `/opsx:sync` (e.g., `/opsx:sync add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||
|
||||
Show changes that have delta specs (under `specs/` directory).
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Find delta specs**
|
||||
|
||||
Look for delta spec files in `openspec/changes/<name>/specs/*/spec.md`.
|
||||
|
||||
Each delta spec file contains sections like:
|
||||
- `## ADDED Requirements` - New requirements to add
|
||||
- `## MODIFIED Requirements` - Changes to existing requirements
|
||||
- `## REMOVED Requirements` - Requirements to remove
|
||||
- `## RENAMED Requirements` - Requirements to rename (FROM:/TO: format)
|
||||
|
||||
If no delta specs found, inform user and stop.
|
||||
|
||||
3. **For each delta spec, apply changes to main specs**
|
||||
|
||||
For each capability with a delta spec at `openspec/changes/<name>/specs/<capability>/spec.md`:
|
||||
|
||||
a. **Read the delta spec** to understand the intended changes
|
||||
|
||||
b. **Read the main spec** at `openspec/specs/<capability>/spec.md` (may not exist yet)
|
||||
|
||||
c. **Apply changes intelligently**:
|
||||
|
||||
**ADDED Requirements:**
|
||||
- If requirement doesn't exist in main spec → add it
|
||||
- If requirement already exists → update it to match (treat as implicit MODIFIED)
|
||||
|
||||
**MODIFIED Requirements:**
|
||||
- Find the requirement in main spec
|
||||
- Apply the changes - this can be:
|
||||
- Adding new scenarios (don't need to copy existing ones)
|
||||
- Modifying existing scenarios
|
||||
- Changing the requirement description
|
||||
- Preserve scenarios/content not mentioned in the delta
|
||||
|
||||
**REMOVED Requirements:**
|
||||
- Remove the entire requirement block from main spec
|
||||
|
||||
**RENAMED Requirements:**
|
||||
- Find the FROM requirement, rename to TO
|
||||
|
||||
d. **Create new main spec** if capability doesn't exist yet:
|
||||
- Create `openspec/specs/<capability>/spec.md`
|
||||
- Add Purpose section (can be brief, mark as TBD)
|
||||
- Add Requirements section with the ADDED requirements
|
||||
|
||||
4. **Show summary**
|
||||
|
||||
After applying all changes, summarize:
|
||||
- Which capabilities were updated
|
||||
- What changes were made (requirements added/modified/removed/renamed)
|
||||
|
||||
**Delta Spec Format Reference**
|
||||
|
||||
```markdown
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: New Feature
|
||||
The system SHALL do something new.
|
||||
|
||||
#### Scenario: Basic case
|
||||
- **WHEN** user does X
|
||||
- **THEN** system does Y
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Existing Feature
|
||||
#### Scenario: New scenario to add
|
||||
- **WHEN** user does A
|
||||
- **THEN** system does B
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Deprecated Feature
|
||||
|
||||
## RENAMED Requirements
|
||||
|
||||
- FROM: `### Requirement: Old Name`
|
||||
- TO: `### Requirement: New Name`
|
||||
```
|
||||
|
||||
**Key Principle: Intelligent Merging**
|
||||
|
||||
Unlike programmatic merging, you can apply **partial updates**:
|
||||
- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
|
||||
- The delta represents *intent*, not a wholesale replacement
|
||||
- Use your judgment to merge changes sensibly
|
||||
|
||||
**Output On Success**
|
||||
|
||||
```
|
||||
## Specs Synced: <change-name>
|
||||
|
||||
Updated main specs:
|
||||
|
||||
**<capability-1>**:
|
||||
- Added requirement: "New Feature"
|
||||
- Modified requirement: "Existing Feature" (added 1 scenario)
|
||||
|
||||
**<capability-2>**:
|
||||
- Created new spec file
|
||||
- Added requirement: "Another Feature"
|
||||
|
||||
Main specs are now updated. The change remains active - archive when implementation is complete.
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Read both delta and main specs before making changes
|
||||
- Preserve existing content not mentioned in delta
|
||||
- If something is unclear, ask for clarification
|
||||
- Show what you're changing as you go
|
||||
- The operation should be idempotent - running twice should give same result
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
---
|
||||
name: "OPSX: Verify"
|
||||
description: Verify implementation matches change artifacts before archiving
|
||||
category: Workflow
|
||||
tags: [workflow, verify, experimental]
|
||||
---
|
||||
|
||||
Verify that an implementation matches the change artifacts (specs, tasks, design).
|
||||
|
||||
**Input**: Optionally specify a change name after `/opsx:verify` (e.g., `/opsx:verify add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||
|
||||
Show changes that have implementation tasks (tasks artifact exists).
|
||||
Include the schema used for each change if available.
|
||||
Mark changes with incomplete tasks as "(In Progress)".
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check status to understand the schema**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
||||
- Which artifacts exist for this change
|
||||
|
||||
3. **Get the change directory and load artifacts**
|
||||
|
||||
```bash
|
||||
openspec instructions apply --change "<name>" --json
|
||||
```
|
||||
|
||||
This returns the change directory and context files. Read all available artifacts from `contextFiles`.
|
||||
|
||||
4. **Initialize verification report structure**
|
||||
|
||||
Create a report structure with three dimensions:
|
||||
- **Completeness**: Track tasks and spec coverage
|
||||
- **Correctness**: Track requirement implementation and scenario coverage
|
||||
- **Coherence**: Track design adherence and pattern consistency
|
||||
|
||||
Each dimension can have CRITICAL, WARNING, or SUGGESTION issues.
|
||||
|
||||
5. **Verify Completeness**
|
||||
|
||||
**Task Completion**:
|
||||
- If tasks.md exists in contextFiles, read it
|
||||
- Parse checkboxes: `- [ ]` (incomplete) vs `- [x]` (complete)
|
||||
- Count complete vs total tasks
|
||||
- If incomplete tasks exist:
|
||||
- Add CRITICAL issue for each incomplete task
|
||||
- Recommendation: "Complete task: <description>" or "Mark as done if already implemented"
|
||||
|
||||
**Spec Coverage**:
|
||||
- If delta specs exist in `openspec/changes/<name>/specs/`:
|
||||
- Extract all requirements (marked with "### Requirement:")
|
||||
- For each requirement:
|
||||
- Search codebase for keywords related to the requirement
|
||||
- Assess if implementation likely exists
|
||||
- If requirements appear unimplemented:
|
||||
- Add CRITICAL issue: "Requirement not found: <requirement name>"
|
||||
- Recommendation: "Implement requirement X: <description>"
|
||||
|
||||
6. **Verify Correctness**
|
||||
|
||||
**Requirement Implementation Mapping**:
|
||||
- For each requirement from delta specs:
|
||||
- Search codebase for implementation evidence
|
||||
- If found, note file paths and line ranges
|
||||
- Assess if implementation matches requirement intent
|
||||
- If divergence detected:
|
||||
- Add WARNING: "Implementation may diverge from spec: <details>"
|
||||
- Recommendation: "Review <file>:<lines> against requirement X"
|
||||
|
||||
**Scenario Coverage**:
|
||||
- For each scenario in delta specs (marked with "#### Scenario:"):
|
||||
- Check if conditions are handled in code
|
||||
- Check if tests exist covering the scenario
|
||||
- If scenario appears uncovered:
|
||||
- Add WARNING: "Scenario not covered: <scenario name>"
|
||||
- Recommendation: "Add test or implementation for scenario: <description>"
|
||||
|
||||
7. **Verify Coherence**
|
||||
|
||||
**Design Adherence**:
|
||||
- If design.md exists in contextFiles:
|
||||
- Extract key decisions (look for sections like "Decision:", "Approach:", "Architecture:")
|
||||
- Verify implementation follows those decisions
|
||||
- If contradiction detected:
|
||||
- Add WARNING: "Design decision not followed: <decision>"
|
||||
- Recommendation: "Update implementation or revise design.md to match reality"
|
||||
- If no design.md: Skip design adherence check, note "No design.md to verify against"
|
||||
|
||||
**Code Pattern Consistency**:
|
||||
- Review new code for consistency with project patterns
|
||||
- Check file naming, directory structure, coding style
|
||||
- If significant deviations found:
|
||||
- Add SUGGESTION: "Code pattern deviation: <details>"
|
||||
- Recommendation: "Consider following project pattern: <example>"
|
||||
|
||||
8. **Generate Verification Report**
|
||||
|
||||
**Summary Scorecard**:
|
||||
```
|
||||
## Verification Report: <change-name>
|
||||
|
||||
### Summary
|
||||
| Dimension | Status |
|
||||
|--------------|------------------|
|
||||
| Completeness | X/Y tasks, N reqs|
|
||||
| Correctness | M/N reqs covered |
|
||||
| Coherence | Followed/Issues |
|
||||
```
|
||||
|
||||
**Issues by Priority**:
|
||||
|
||||
1. **CRITICAL** (Must fix before archive):
|
||||
- Incomplete tasks
|
||||
- Missing requirement implementations
|
||||
- Each with specific, actionable recommendation
|
||||
|
||||
2. **WARNING** (Should fix):
|
||||
- Spec/design divergences
|
||||
- Missing scenario coverage
|
||||
- Each with specific recommendation
|
||||
|
||||
3. **SUGGESTION** (Nice to fix):
|
||||
- Pattern inconsistencies
|
||||
- Minor improvements
|
||||
- Each with specific recommendation
|
||||
|
||||
**Final Assessment**:
|
||||
- If CRITICAL issues: "X critical issue(s) found. Fix before archiving."
|
||||
- If only warnings: "No critical issues. Y warning(s) to consider. Ready for archive (with noted improvements)."
|
||||
- If all clear: "All checks passed. Ready for archive."
|
||||
|
||||
**Verification Heuristics**
|
||||
|
||||
- **Completeness**: Focus on objective checklist items (checkboxes, requirements list)
|
||||
- **Correctness**: Use keyword search, file path analysis, reasonable inference - don't require perfect certainty
|
||||
- **Coherence**: Look for glaring inconsistencies, don't nitpick style
|
||||
- **False Positives**: When uncertain, prefer SUGGESTION over WARNING, WARNING over CRITICAL
|
||||
- **Actionability**: Every issue must have a specific recommendation with file/line references where applicable
|
||||
|
||||
**Graceful Degradation**
|
||||
|
||||
- If only tasks.md exists: verify task completion only, skip spec/design checks
|
||||
- If tasks + specs exist: verify completeness and correctness, skip design
|
||||
- If full artifacts: verify all three dimensions
|
||||
- Always note which checks were skipped and why
|
||||
|
||||
**Output Format**
|
||||
|
||||
Use clear markdown with:
|
||||
- Table for summary scorecard
|
||||
- Grouped lists for issues (CRITICAL/WARNING/SUGGESTION)
|
||||
- Code references in format: `file.ts:123`
|
||||
- Specific, actionable recommendations
|
||||
- No vague suggestions like "consider reviewing"
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(openspec new:*)",
|
||||
"Bash(openspec status:*)",
|
||||
"Bash(openspec instructions:*)",
|
||||
"Bash(dotnet build:*)",
|
||||
"Bash(dotnet test:*)"
|
||||
]
|
||||
},
|
||||
"outputStyle": "default"
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
---
|
||||
name: openspec-apply-change
|
||||
description: Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Implement tasks from an OpenSpec change.
|
||||
|
||||
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Select the change**
|
||||
|
||||
If a name is provided, use it. Otherwise:
|
||||
- Infer from conversation context if the user mentioned a change
|
||||
- Auto-select if only one active change exists
|
||||
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
|
||||
|
||||
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
|
||||
|
||||
2. **Check status to understand the schema**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
||||
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
||||
|
||||
3. **Get apply instructions**
|
||||
|
||||
```bash
|
||||
openspec instructions apply --change "<name>" --json
|
||||
```
|
||||
|
||||
This returns:
|
||||
- Context file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
|
||||
- Progress (total, complete, remaining)
|
||||
- Task list with status
|
||||
- Dynamic instruction based on current state
|
||||
|
||||
**Handle states:**
|
||||
- If `state: "blocked"` (missing artifacts): show message, suggest using openspec-continue-change
|
||||
- If `state: "all_done"`: congratulate, suggest archive
|
||||
- Otherwise: proceed to implementation
|
||||
|
||||
4. **Read context files**
|
||||
|
||||
Read the files listed in `contextFiles` from the apply instructions output.
|
||||
The files depend on the schema being used:
|
||||
- **spec-driven**: proposal, specs, design, tasks
|
||||
- Other schemas: follow the contextFiles from CLI output
|
||||
|
||||
5. **Show current progress**
|
||||
|
||||
Display:
|
||||
- Schema being used
|
||||
- Progress: "N/M tasks complete"
|
||||
- Remaining tasks overview
|
||||
- Dynamic instruction from CLI
|
||||
|
||||
6. **Implement tasks (loop until done or blocked)**
|
||||
|
||||
For each pending task:
|
||||
- Show which task is being worked on
|
||||
- Make the code changes required
|
||||
- Keep changes minimal and focused
|
||||
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
|
||||
- Continue to next task
|
||||
|
||||
**Pause if:**
|
||||
- Task is unclear → ask for clarification
|
||||
- Implementation reveals a design issue → suggest updating artifacts
|
||||
- Error or blocker encountered → report and wait for guidance
|
||||
- User interrupts
|
||||
|
||||
7. **On completion or pause, show status**
|
||||
|
||||
Display:
|
||||
- Tasks completed this session
|
||||
- Overall progress: "N/M tasks complete"
|
||||
- If all done: suggest archive
|
||||
- If paused: explain why and wait for guidance
|
||||
|
||||
**Output During Implementation**
|
||||
|
||||
```
|
||||
## Implementing: <change-name> (schema: <schema-name>)
|
||||
|
||||
Working on task 3/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
|
||||
Working on task 4/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
```
|
||||
|
||||
**Output On Completion**
|
||||
|
||||
```
|
||||
## Implementation Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 7/7 tasks complete ✓
|
||||
|
||||
### Completed This Session
|
||||
- [x] Task 1
|
||||
- [x] Task 2
|
||||
...
|
||||
|
||||
All tasks complete! Ready to archive this change.
|
||||
```
|
||||
|
||||
**Output On Pause (Issue Encountered)**
|
||||
|
||||
```
|
||||
## Implementation Paused
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 4/7 tasks complete
|
||||
|
||||
### Issue Encountered
|
||||
<description of the issue>
|
||||
|
||||
**Options:**
|
||||
1. <option 1>
|
||||
2. <option 2>
|
||||
3. Other approach
|
||||
|
||||
What would you like to do?
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Keep going through tasks until done or blocked
|
||||
- Always read context files before starting (from the apply instructions output)
|
||||
- If task is ambiguous, pause and ask before implementing
|
||||
- If implementation reveals issues, pause and suggest artifact updates
|
||||
- Keep code changes minimal and scoped to each task
|
||||
- Update task checkbox immediately after completing each task
|
||||
- Pause on errors, blockers, or unclear requirements - don't guess
|
||||
- Use contextFiles from CLI output, don't assume specific file names
|
||||
|
||||
**Fluid Workflow Integration**
|
||||
|
||||
This skill supports the "actions on a change" model:
|
||||
|
||||
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
|
||||
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
---
|
||||
name: openspec-archive-change
|
||||
description: Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Archive a completed change in the experimental workflow.
|
||||
|
||||
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||
|
||||
Show only active changes (not already archived).
|
||||
Include the schema used for each change if available.
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check artifact completion status**
|
||||
|
||||
Run `openspec status --change "<name>" --json` to check artifact completion.
|
||||
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used
|
||||
- `artifacts`: List of artifacts with their status (`done` or other)
|
||||
|
||||
**If any artifacts are not `done`:**
|
||||
- Display warning listing incomplete artifacts
|
||||
- Use **AskUserQuestion tool** to confirm user wants to proceed
|
||||
- Proceed if user confirms
|
||||
|
||||
3. **Check task completion status**
|
||||
|
||||
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
|
||||
|
||||
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
|
||||
|
||||
**If incomplete tasks found:**
|
||||
- Display warning showing count of incomplete tasks
|
||||
- Use **AskUserQuestion tool** to confirm user wants to proceed
|
||||
- Proceed if user confirms
|
||||
|
||||
**If no tasks file exists:** Proceed without task-related warning.
|
||||
|
||||
4. **Assess delta spec sync state**
|
||||
|
||||
Check for delta specs at `openspec/changes/<name>/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`
|
||||
- Determine what changes would be applied (adds, modifications, removals, renames)
|
||||
- Show a combined summary before prompting
|
||||
|
||||
**Prompt options:**
|
||||
- If changes needed: "Sync now (recommended)", "Archive without syncing"
|
||||
- If already synced: "Archive now", "Sync anyway", "Cancel"
|
||||
|
||||
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
|
||||
|
||||
5. **Perform the archive**
|
||||
|
||||
Create the archive directory if it doesn't exist:
|
||||
```bash
|
||||
mkdir -p openspec/changes/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
|
||||
|
||||
```bash
|
||||
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
||||
```
|
||||
|
||||
6. **Display summary**
|
||||
|
||||
Show archive completion summary including:
|
||||
- Change name
|
||||
- Schema that was used
|
||||
- Archive location
|
||||
- Whether specs were synced (if applicable)
|
||||
- Note about any warnings (incomplete artifacts/tasks)
|
||||
|
||||
**Output On Success**
|
||||
|
||||
```
|
||||
## Archive Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** ✓ Synced to main specs (or "No delta specs" or "Sync skipped")
|
||||
|
||||
All artifacts complete. All tasks complete.
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Always prompt for change selection if not provided
|
||||
- Use artifact graph (openspec status --json) for completion checking
|
||||
- Don't block archive on warnings - just inform and confirm
|
||||
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
||||
- Show clear summary of what happened
|
||||
- If sync is requested, use openspec-sync-specs approach (agent-driven)
|
||||
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
---
|
||||
name: openspec-bulk-archive-change
|
||||
description: Archive multiple completed changes at once. Use when archiving several parallel changes.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Archive multiple completed changes in a single operation.
|
||||
|
||||
This skill allows you to batch-archive changes, handling spec conflicts intelligently by checking the codebase to determine what's actually implemented.
|
||||
|
||||
**Input**: None required (prompts for selection)
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Get active changes**
|
||||
|
||||
Run `openspec list --json` to get all active changes.
|
||||
|
||||
If no active changes exist, inform user and stop.
|
||||
|
||||
2. **Prompt for change selection**
|
||||
|
||||
Use **AskUserQuestion tool** with multi-select to let user choose changes:
|
||||
- Show each change with its schema
|
||||
- Include an option for "All changes"
|
||||
- Allow any number of selections (1+ works, 2+ is the typical use case)
|
||||
|
||||
**IMPORTANT**: Do NOT auto-select. Always let the user choose.
|
||||
|
||||
3. **Batch validation - gather status for all selected changes**
|
||||
|
||||
For each selected change, collect:
|
||||
|
||||
a. **Artifact status** - Run `openspec status --change "<name>" --json`
|
||||
- Parse `schemaName` and `artifacts` list
|
||||
- Note which artifacts are `done` vs other states
|
||||
|
||||
b. **Task completion** - Read `openspec/changes/<name>/tasks.md`
|
||||
- Count `- [ ]` (incomplete) vs `- [x]` (complete)
|
||||
- If no tasks file exists, note as "No tasks"
|
||||
|
||||
c. **Delta specs** - Check `openspec/changes/<name>/specs/` directory
|
||||
- List which capability specs exist
|
||||
- For each, extract requirement names (lines matching `### Requirement: <name>`)
|
||||
|
||||
4. **Detect spec conflicts**
|
||||
|
||||
Build a map of `capability -> [changes that touch it]`:
|
||||
|
||||
```
|
||||
auth -> [change-a, change-b] <- CONFLICT (2+ changes)
|
||||
api -> [change-c] <- OK (only 1 change)
|
||||
```
|
||||
|
||||
A conflict exists when 2+ selected changes have delta specs for the same capability.
|
||||
|
||||
5. **Resolve conflicts agentically**
|
||||
|
||||
**For each conflict**, investigate the codebase:
|
||||
|
||||
a. **Read the delta specs** from each conflicting change to understand what each claims to add/modify
|
||||
|
||||
b. **Search the codebase** for implementation evidence:
|
||||
- Look for code implementing requirements from each delta spec
|
||||
- Check for related files, functions, or tests
|
||||
|
||||
c. **Determine resolution**:
|
||||
- If only one change is actually implemented -> sync that one's specs
|
||||
- If both implemented -> apply in chronological order (older first, newer overwrites)
|
||||
- If neither implemented -> skip spec sync, warn user
|
||||
|
||||
d. **Record resolution** for each conflict:
|
||||
- Which change's specs to apply
|
||||
- In what order (if both)
|
||||
- Rationale (what was found in codebase)
|
||||
|
||||
6. **Show consolidated status table**
|
||||
|
||||
Display a table summarizing all changes:
|
||||
|
||||
```
|
||||
| Change | Artifacts | Tasks | Specs | Conflicts | Status |
|
||||
|---------------------|-----------|-------|---------|-----------|--------|
|
||||
| schema-management | Done | 5/5 | 2 delta | None | Ready |
|
||||
| project-config | Done | 3/3 | 1 delta | None | Ready |
|
||||
| add-oauth | Done | 4/4 | 1 delta | auth (!) | Ready* |
|
||||
| add-verify-skill | 1 left | 2/5 | None | None | Warn |
|
||||
```
|
||||
|
||||
For conflicts, show the resolution:
|
||||
```
|
||||
* Conflict resolution:
|
||||
- auth spec: Will apply add-oauth then add-jwt (both implemented, chronological order)
|
||||
```
|
||||
|
||||
For incomplete changes, show warnings:
|
||||
```
|
||||
Warnings:
|
||||
- add-verify-skill: 1 incomplete artifact, 3 incomplete tasks
|
||||
```
|
||||
|
||||
7. **Confirm batch operation**
|
||||
|
||||
Use **AskUserQuestion tool** with a single confirmation:
|
||||
|
||||
- "Archive N changes?" with options based on status
|
||||
- Options might include:
|
||||
- "Archive all N changes"
|
||||
- "Archive only N ready changes (skip incomplete)"
|
||||
- "Cancel"
|
||||
|
||||
If there are incomplete changes, make clear they'll be archived with warnings.
|
||||
|
||||
8. **Execute archive for each confirmed change**
|
||||
|
||||
Process changes in the determined order (respecting conflict resolution):
|
||||
|
||||
a. **Sync specs** if delta specs exist:
|
||||
- Use the openspec-sync-specs approach (agent-driven intelligent merge)
|
||||
- For conflicts, apply in resolved order
|
||||
- Track if sync was done
|
||||
|
||||
b. **Perform the archive**:
|
||||
```bash
|
||||
mkdir -p openspec/changes/archive
|
||||
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
||||
```
|
||||
|
||||
c. **Track outcome** for each change:
|
||||
- Success: archived successfully
|
||||
- Failed: error during archive (record error)
|
||||
- Skipped: user chose not to archive (if applicable)
|
||||
|
||||
9. **Display summary**
|
||||
|
||||
Show final results:
|
||||
|
||||
```
|
||||
## Bulk Archive Complete
|
||||
|
||||
Archived 3 changes:
|
||||
- schema-management-cli -> archive/2026-01-19-schema-management-cli/
|
||||
- project-config -> archive/2026-01-19-project-config/
|
||||
- add-oauth -> archive/2026-01-19-add-oauth/
|
||||
|
||||
Skipped 1 change:
|
||||
- add-verify-skill (user chose not to archive incomplete)
|
||||
|
||||
Spec sync summary:
|
||||
- 4 delta specs synced to main specs
|
||||
- 1 conflict resolved (auth: applied both in chronological order)
|
||||
```
|
||||
|
||||
If any failures:
|
||||
```
|
||||
Failed 1 change:
|
||||
- some-change: Archive directory already exists
|
||||
```
|
||||
|
||||
**Conflict Resolution Examples**
|
||||
|
||||
Example 1: Only one implemented
|
||||
```
|
||||
Conflict: specs/auth/spec.md touched by [add-oauth, add-jwt]
|
||||
|
||||
Checking add-oauth:
|
||||
- Delta adds "OAuth Provider Integration" requirement
|
||||
- Searching codebase... found src/auth/oauth.ts implementing OAuth flow
|
||||
|
||||
Checking add-jwt:
|
||||
- Delta adds "JWT Token Handling" requirement
|
||||
- Searching codebase... no JWT implementation found
|
||||
|
||||
Resolution: Only add-oauth is implemented. Will sync add-oauth specs only.
|
||||
```
|
||||
|
||||
Example 2: Both implemented
|
||||
```
|
||||
Conflict: specs/api/spec.md touched by [add-rest-api, add-graphql]
|
||||
|
||||
Checking add-rest-api (created 2026-01-10):
|
||||
- Delta adds "REST Endpoints" requirement
|
||||
- Searching codebase... found src/api/rest.ts
|
||||
|
||||
Checking add-graphql (created 2026-01-15):
|
||||
- Delta adds "GraphQL Schema" requirement
|
||||
- Searching codebase... found src/api/graphql.ts
|
||||
|
||||
Resolution: Both implemented. Will apply add-rest-api specs first,
|
||||
then add-graphql specs (chronological order, newer takes precedence).
|
||||
```
|
||||
|
||||
**Output On Success**
|
||||
|
||||
```
|
||||
## Bulk Archive Complete
|
||||
|
||||
Archived N changes:
|
||||
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
|
||||
- <change-2> -> archive/YYYY-MM-DD-<change-2>/
|
||||
|
||||
Spec sync summary:
|
||||
- N delta specs synced to main specs
|
||||
- No conflicts (or: M conflicts resolved)
|
||||
```
|
||||
|
||||
**Output On Partial Success**
|
||||
|
||||
```
|
||||
## Bulk Archive Complete (partial)
|
||||
|
||||
Archived N changes:
|
||||
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
|
||||
|
||||
Skipped M changes:
|
||||
- <change-2> (user chose not to archive incomplete)
|
||||
|
||||
Failed K changes:
|
||||
- <change-3>: Archive directory already exists
|
||||
```
|
||||
|
||||
**Output When No Changes**
|
||||
|
||||
```
|
||||
## No Changes to Archive
|
||||
|
||||
No active changes found. Create a new change to get started.
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Allow any number of changes (1+ is fine, 2+ is the typical use case)
|
||||
- Always prompt for selection, never auto-select
|
||||
- Detect spec conflicts early and resolve by checking codebase
|
||||
- When both changes are implemented, apply specs in chronological order
|
||||
- Skip spec sync only when implementation is missing (warn user)
|
||||
- Show clear per-change status before confirming
|
||||
- Use single confirmation for entire batch
|
||||
- Track and report all outcomes (success/skip/fail)
|
||||
- Preserve .openspec.yaml when moving to archive
|
||||
- Archive directory target uses current date: YYYY-MM-DD-<name>
|
||||
- If archive target exists, fail that change but continue with others
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
---
|
||||
name: openspec-continue-change
|
||||
description: Continue working on an OpenSpec change by creating the next artifact. Use when the user wants to progress their change, create the next artifact, or continue their workflow.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Continue working on a change by creating the next artifact.
|
||||
|
||||
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes sorted by most recently modified. Then use the **AskUserQuestion tool** to let the user select which change to work on.
|
||||
|
||||
Present the top 3-4 most recently modified changes as options, showing:
|
||||
- Change name
|
||||
- Schema (from `schema` field if present, otherwise "spec-driven")
|
||||
- Status (e.g., "0/5 tasks", "complete", "no tasks")
|
||||
- How recently it was modified (from `lastModified` field)
|
||||
|
||||
Mark the most recently modified change as "(Recommended)" since it's likely what the user wants to continue.
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check current status**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to understand current state. The response includes:
|
||||
- `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
|
||||
|
||||
3. **Act based on status**:
|
||||
|
||||
---
|
||||
|
||||
**If all artifacts are complete (`isComplete: true`)**:
|
||||
- Congratulate the user
|
||||
- Show final status including the schema used
|
||||
- Suggest: "All artifacts created! You can now implement this change or archive it."
|
||||
- STOP
|
||||
|
||||
---
|
||||
|
||||
**If artifacts are ready to create** (status shows artifacts with `status: "ready"`):
|
||||
- Pick the FIRST artifact with `status: "ready"` from the status output
|
||||
- Get its instructions:
|
||||
```bash
|
||||
openspec instructions <artifact-id> --change "<name>" --json
|
||||
```
|
||||
- Parse the JSON. The key fields are:
|
||||
- `context`: Project background (constraints for you - do NOT include in output)
|
||||
- `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
|
||||
- `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
|
||||
- Show what was created and what's now unlocked
|
||||
- STOP after creating ONE artifact
|
||||
|
||||
---
|
||||
|
||||
**If no artifacts are ready (all blocked)**:
|
||||
- This shouldn't happen with a valid schema
|
||||
- Show status and suggest checking for issues
|
||||
|
||||
4. **After creating an artifact, show progress**
|
||||
```bash
|
||||
openspec status --change "<name>"
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
After each invocation, show:
|
||||
- Which artifact was created
|
||||
- Schema workflow being used
|
||||
- Current progress (N/M complete)
|
||||
- What artifacts are now unlocked
|
||||
- Prompt: "Want to continue? Just ask me to continue or tell me what to do next."
|
||||
|
||||
**Artifact Creation Guidelines**
|
||||
|
||||
The artifact types and their purpose depend on the schema. Use the `instruction` field from the instructions output to understand what to create.
|
||||
|
||||
Common artifact patterns:
|
||||
|
||||
**spec-driven schema** (proposal → specs → design → tasks):
|
||||
- **proposal.md**: Ask user about the change if not clear. Fill in Why, What Changes, Capabilities, Impact.
|
||||
- The Capabilities section is critical - each capability listed will need a spec file.
|
||||
- **specs/<capability>/spec.md**: Create one spec per capability listed in the proposal's Capabilities section (use the capability name, not the change name).
|
||||
- **design.md**: Document technical decisions, architecture, and implementation approach.
|
||||
- **tasks.md**: Break down implementation into checkboxed tasks.
|
||||
|
||||
For other schemas, follow the `instruction` field from the CLI output.
|
||||
|
||||
**Guardrails**
|
||||
- Create ONE artifact per invocation
|
||||
- Always read dependency artifacts before creating a new one
|
||||
- Never skip artifacts or create out of order
|
||||
- If context is unclear, ask the user before creating
|
||||
- Verify the artifact file exists after writing before marking progress
|
||||
- Use the schema's artifact sequence, don't assume specific artifact names
|
||||
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
||||
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
||||
- These guide what you write, but should never appear in the output
|
||||
|
|
@ -0,0 +1,288 @@
|
|||
---
|
||||
name: openspec-explore
|
||||
description: Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
|
||||
|
||||
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
|
||||
|
||||
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
|
||||
|
||||
---
|
||||
|
||||
## The Stance
|
||||
|
||||
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
|
||||
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
|
||||
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
|
||||
- **Adaptive** - Follow interesting threads, pivot when new information emerges
|
||||
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
|
||||
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
|
||||
|
||||
---
|
||||
|
||||
## What You Might Do
|
||||
|
||||
Depending on what the user brings, you might:
|
||||
|
||||
**Explore the problem space**
|
||||
- Ask clarifying questions that emerge from what they said
|
||||
- Challenge assumptions
|
||||
- Reframe the problem
|
||||
- Find analogies
|
||||
|
||||
**Investigate the codebase**
|
||||
- Map existing architecture relevant to the discussion
|
||||
- Find integration points
|
||||
- Identify patterns already in use
|
||||
- Surface hidden complexity
|
||||
|
||||
**Compare options**
|
||||
- Brainstorm multiple approaches
|
||||
- Build comparison tables
|
||||
- Sketch tradeoffs
|
||||
- Recommend a path (if asked)
|
||||
|
||||
**Visualize**
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Use ASCII diagrams liberally │
|
||||
├─────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌────────┐ ┌────────┐ │
|
||||
│ │ State │────────▶│ State │ │
|
||||
│ │ A │ │ B │ │
|
||||
│ └────────┘ └────────┘ │
|
||||
│ │
|
||||
│ System diagrams, state machines, │
|
||||
│ data flows, architecture sketches, │
|
||||
│ dependency graphs, comparison tables │
|
||||
│ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Surface risks and unknowns**
|
||||
- Identify what could go wrong
|
||||
- Find gaps in understanding
|
||||
- Suggest spikes or investigations
|
||||
|
||||
---
|
||||
|
||||
## OpenSpec Awareness
|
||||
|
||||
You have full context of the OpenSpec system. Use it naturally, don't force it.
|
||||
|
||||
### Check for context
|
||||
|
||||
At the start, quickly check what exists:
|
||||
```bash
|
||||
openspec list --json
|
||||
```
|
||||
|
||||
This tells you:
|
||||
- If there are active changes
|
||||
- Their names, schemas, and status
|
||||
- What the user might be working on
|
||||
|
||||
### When no change exists
|
||||
|
||||
Think freely. When insights crystallize, you might offer:
|
||||
|
||||
- "This feels solid enough to start a change. Want me to create a proposal?"
|
||||
- Or keep exploring - no pressure to formalize
|
||||
|
||||
### When a change exists
|
||||
|
||||
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.
|
||||
|
||||
2. **Reference them naturally in conversation**
|
||||
- "Your design mentions using Redis, but we just realized SQLite fits better..."
|
||||
- "The proposal scopes this to premium users, but we're now thinking everyone..."
|
||||
|
||||
3. **Offer to capture when decisions are made**
|
||||
|
||||
| Insight Type | Where to Capture |
|
||||
|--------------|------------------|
|
||||
| New requirement discovered | `specs/<capability>/spec.md` |
|
||||
| Requirement changed | `specs/<capability>/spec.md` |
|
||||
| Design decision made | `design.md` |
|
||||
| Scope changed | `proposal.md` |
|
||||
| New work identified | `tasks.md` |
|
||||
| Assumption invalidated | Relevant artifact |
|
||||
|
||||
Example offers:
|
||||
- "That's a design decision. Capture it in design.md?"
|
||||
- "This is a new requirement. Add it to specs?"
|
||||
- "This changes scope. Update the proposal?"
|
||||
|
||||
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
|
||||
|
||||
---
|
||||
|
||||
## What You Don't Have To Do
|
||||
|
||||
- Follow a script
|
||||
- Ask the same questions every time
|
||||
- Produce a specific artifact
|
||||
- Reach a conclusion
|
||||
- Stay on topic if a tangent is valuable
|
||||
- Be brief (this is thinking time)
|
||||
|
||||
---
|
||||
|
||||
## Handling Different Entry Points
|
||||
|
||||
**User brings a vague idea:**
|
||||
```
|
||||
User: I'm thinking about adding real-time collaboration
|
||||
|
||||
You: Real-time collab is a big space. Let me think about this...
|
||||
|
||||
COLLABORATION SPECTRUM
|
||||
════════════════════════════════════════════
|
||||
|
||||
Awareness Coordination Sync
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌────────┐ ┌────────┐ ┌────────┐
|
||||
│Presence│ │Cursors │ │ CRDT │
|
||||
│ "3 │ │ Multi │ │Conflict│
|
||||
│online" │ │ select │ │ free │
|
||||
└────────┘ └────────┘ └────────┘
|
||||
│ │ │
|
||||
trivial moderate complex
|
||||
|
||||
Where's your head at?
|
||||
```
|
||||
|
||||
**User brings a specific problem:**
|
||||
```
|
||||
User: The auth system is a mess
|
||||
|
||||
You: [reads codebase]
|
||||
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ CURRENT AUTH FLOW │
|
||||
└─────────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────┼─────────────┐
|
||||
▼ ▼ ▼
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ Google │ │ GitHub │ │ Email │
|
||||
│ OAuth │ │ OAuth │ │ Magic │
|
||||
└────┬────┘ └────┬────┘ └────┬────┘
|
||||
│ │ │
|
||||
└─────────────┼─────────────┘
|
||||
▼
|
||||
┌───────────┐
|
||||
│ Session │
|
||||
└─────┬─────┘
|
||||
│
|
||||
▼
|
||||
┌───────────┐
|
||||
│ Perms │
|
||||
└───────────┘
|
||||
|
||||
I see three tangles. Which one's burning?
|
||||
```
|
||||
|
||||
**User is stuck mid-implementation:**
|
||||
```
|
||||
User: /opsx:explore add-auth-system
|
||||
The OAuth integration is more complex than expected
|
||||
|
||||
You: [reads change artifacts]
|
||||
|
||||
You're on task 4: "Implement OAuth flow"
|
||||
|
||||
Let me trace what's involved...
|
||||
|
||||
[draws diagram, explores options, suggests paths]
|
||||
|
||||
Want to update the design to reflect this?
|
||||
Or add a spike task to investigate?
|
||||
```
|
||||
|
||||
**User wants to compare options:**
|
||||
```
|
||||
User: Should we use Postgres or SQLite?
|
||||
|
||||
You: Generic answer is boring. What's the context?
|
||||
|
||||
User: A CLI tool that tracks local dev environments
|
||||
|
||||
You: That changes everything.
|
||||
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ CLI TOOL DATA STORAGE │
|
||||
└─────────────────────────────────────────────────┘
|
||||
|
||||
Key constraints:
|
||||
• No daemon running
|
||||
• Must work offline
|
||||
• Single user
|
||||
|
||||
SQLite Postgres
|
||||
Deployment embedded ✓ needs server ✗
|
||||
Offline yes ✓ no ✗
|
||||
Single file yes ✓ no ✗
|
||||
|
||||
SQLite. Not even close.
|
||||
|
||||
Unless... is there a sync component?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ending Discovery
|
||||
|
||||
There's no required ending. Discovery might:
|
||||
|
||||
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
|
||||
- **Result in artifact updates**: "Updated design.md with these decisions"
|
||||
- **Just provide clarity**: User has what they need, moves on
|
||||
- **Continue later**: "We can pick this up anytime"
|
||||
|
||||
When it feels like things are crystallizing, you might summarize:
|
||||
|
||||
```
|
||||
## What We Figured Out
|
||||
|
||||
**The problem**: [crystallized understanding]
|
||||
|
||||
**The approach**: [if one emerged]
|
||||
|
||||
**Open questions**: [if any remain]
|
||||
|
||||
**Next steps** (if ready):
|
||||
- Create a change proposal
|
||||
- Keep exploring: just keep talking
|
||||
```
|
||||
|
||||
But this summary is optional. Sometimes the thinking IS the value.
|
||||
|
||||
---
|
||||
|
||||
## Guardrails
|
||||
|
||||
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
|
||||
- **Don't fake understanding** - If something is unclear, dig deeper
|
||||
- **Don't rush** - Discovery is thinking time, not task time
|
||||
- **Don't force structure** - Let patterns emerge naturally
|
||||
- **Don't auto-capture** - Offer to save insights, don't just do it
|
||||
- **Do visualize** - A good diagram is worth many paragraphs
|
||||
- **Do explore the codebase** - Ground discussions in reality
|
||||
- **Do question assumptions** - Including the user's and your own
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
---
|
||||
name: openspec-ff-change
|
||||
description: Fast-forward through OpenSpec artifact creation. Use when the user wants to quickly create all artifacts needed for implementation without stepping through each one individually.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Fast-forward through artifact creation - generate everything needed to start implementation in one go.
|
||||
|
||||
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no clear input provided, ask what they want to build**
|
||||
|
||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||
> "What change do you want to work on? Describe what you want to build or fix."
|
||||
|
||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||
|
||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||
|
||||
2. **Create the change directory**
|
||||
```bash
|
||||
openspec new change "<name>"
|
||||
```
|
||||
This creates a scaffolded change at `openspec/changes/<name>/`.
|
||||
|
||||
3. **Get the artifact build order**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
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
|
||||
|
||||
4. **Create artifacts in sequence until apply-ready**
|
||||
|
||||
Use the **TodoWrite tool** to track progress through the artifacts.
|
||||
|
||||
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
||||
|
||||
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
||||
- Get instructions:
|
||||
```bash
|
||||
openspec instructions <artifact-id> --change "<name>" --json
|
||||
```
|
||||
- The instructions JSON includes:
|
||||
- `context`: Project background (constraints for you - do NOT include in output)
|
||||
- `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
|
||||
- `dependencies`: Completed artifacts to read for context
|
||||
- Read any completed dependency files for context
|
||||
- Create the artifact file using `template` as the structure
|
||||
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
||||
- Show brief progress: "✓ Created <artifact-id>"
|
||||
|
||||
b. **Continue until all `applyRequires` artifacts are complete**
|
||||
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
||||
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
||||
- Stop when all `applyRequires` artifacts are done
|
||||
|
||||
c. **If an artifact requires user input** (unclear context):
|
||||
- Use **AskUserQuestion tool** to clarify
|
||||
- Then continue with creation
|
||||
|
||||
5. **Show final status**
|
||||
```bash
|
||||
openspec status --change "<name>"
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
After completing all artifacts, summarize:
|
||||
- Change name and location
|
||||
- List of artifacts created with brief descriptions
|
||||
- What's ready: "All artifacts created! Ready for implementation."
|
||||
- Prompt: "Run `/opsx:apply` or ask me to implement to start working on the tasks."
|
||||
|
||||
**Artifact Creation Guidelines**
|
||||
|
||||
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
||||
- The schema defines what each artifact should contain - follow it
|
||||
- Read dependency artifacts for context before creating new ones
|
||||
- Use `template` as the structure for your output file - fill in its sections
|
||||
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
||||
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
||||
- These guide what you write, but should never appear in the output
|
||||
|
||||
**Guardrails**
|
||||
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
||||
- Always read dependency artifacts before creating a new one
|
||||
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
||||
- If a change with that name already exists, suggest continuing that change instead
|
||||
- Verify each artifact file exists after writing before proceeding to next
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
---
|
||||
name: openspec-new-change
|
||||
description: Start a new OpenSpec change using the experimental artifact workflow. Use when the user wants to create a new feature, fix, or modification with a structured step-by-step approach.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Start a new change using the experimental artifact-driven approach.
|
||||
|
||||
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no clear input provided, ask what they want to build**
|
||||
|
||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||
> "What change do you want to work on? Describe what you want to build or fix."
|
||||
|
||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||
|
||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||
|
||||
2. **Determine the workflow schema**
|
||||
|
||||
Use the default schema (omit `--schema`) unless the user explicitly requests a different workflow.
|
||||
|
||||
**Use a different schema only if the user mentions:**
|
||||
- A specific schema name → use `--schema <name>`
|
||||
- "show workflows" or "what workflows" → run `openspec schemas --json` and let them choose
|
||||
|
||||
**Otherwise**: Omit `--schema` to use the default.
|
||||
|
||||
3. **Create the change directory**
|
||||
```bash
|
||||
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.
|
||||
|
||||
4. **Show the artifact status**
|
||||
```bash
|
||||
openspec status --change "<name>"
|
||||
```
|
||||
This shows which artifacts need to be created and which are ready (dependencies satisfied).
|
||||
|
||||
5. **Get instructions for the first artifact**
|
||||
The first artifact depends on the schema (e.g., `proposal` for spec-driven).
|
||||
Check the status output to find the first artifact with status "ready".
|
||||
```bash
|
||||
openspec instructions <first-artifact-id> --change "<name>"
|
||||
```
|
||||
This outputs the template and context for creating the first artifact.
|
||||
|
||||
6. **STOP and wait for user direction**
|
||||
|
||||
**Output**
|
||||
|
||||
After completing the steps, summarize:
|
||||
- Change name and location
|
||||
- Schema/workflow being used and its artifact sequence
|
||||
- Current status (0/N artifacts complete)
|
||||
- The template for the first artifact
|
||||
- Prompt: "Ready to create the first artifact? Just describe what this change is about and I'll draft it, or ask me to continue."
|
||||
|
||||
**Guardrails**
|
||||
- Do NOT create any artifacts yet - just show the instructions
|
||||
- Do NOT advance beyond showing the first artifact template
|
||||
- If the name is invalid (not kebab-case), ask for a valid name
|
||||
- If a change with that name already exists, suggest continuing that change instead
|
||||
- Pass --schema if using a non-default workflow
|
||||
|
|
@ -0,0 +1,554 @@
|
|||
---
|
||||
name: openspec-onboard
|
||||
description: Guided onboarding for OpenSpec - walk through a complete workflow cycle with narration and real codebase work.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Guide the user through their first complete OpenSpec workflow cycle. This is a teaching experience—you'll do real work in their codebase while explaining each step.
|
||||
|
||||
---
|
||||
|
||||
## Preflight
|
||||
|
||||
Before starting, check if the OpenSpec CLI is installed:
|
||||
|
||||
```bash
|
||||
# Unix/macOS
|
||||
openspec --version 2>&1 || echo "CLI_NOT_INSTALLED"
|
||||
# Windows (PowerShell)
|
||||
# if (Get-Command openspec -ErrorAction SilentlyContinue) { openspec --version } else { echo "CLI_NOT_INSTALLED" }
|
||||
```
|
||||
|
||||
**If CLI not installed:**
|
||||
> OpenSpec CLI is not installed. Install it first, then come back to `/opsx:onboard`.
|
||||
|
||||
Stop here if not installed.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Welcome
|
||||
|
||||
Display:
|
||||
|
||||
```
|
||||
## Welcome to OpenSpec!
|
||||
|
||||
I'll walk you through a complete change cycle—from idea to implementation—using a real task in your codebase. Along the way, you'll learn the workflow by doing it.
|
||||
|
||||
**What we'll do:**
|
||||
1. Pick a small, real task in your codebase
|
||||
2. Explore the problem briefly
|
||||
3. Create a change (the container for our work)
|
||||
4. Build the artifacts: proposal → specs → design → tasks
|
||||
5. Implement the tasks
|
||||
6. Archive the completed change
|
||||
|
||||
**Time:** ~15-20 minutes
|
||||
|
||||
Let's start by finding something to work on.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Task Selection
|
||||
|
||||
### Codebase Analysis
|
||||
|
||||
Scan the codebase for small improvement opportunities. Look for:
|
||||
|
||||
1. **TODO/FIXME comments** - Search for `TODO`, `FIXME`, `HACK`, `XXX` in code files
|
||||
2. **Missing error handling** - `catch` blocks that swallow errors, risky operations without try-catch
|
||||
3. **Functions without tests** - Cross-reference `src/` with test directories
|
||||
4. **Type issues** - `any` types in TypeScript files (`: any`, `as any`)
|
||||
5. **Debug artifacts** - `console.log`, `console.debug`, `debugger` statements in non-debug code
|
||||
6. **Missing validation** - User input handlers without validation
|
||||
|
||||
Also check recent git activity:
|
||||
```bash
|
||||
# Unix/macOS
|
||||
git log --oneline -10 2>/dev/null || echo "No git history"
|
||||
# Windows (PowerShell)
|
||||
# git log --oneline -10 2>$null; if ($LASTEXITCODE -ne 0) { echo "No git history" }
|
||||
```
|
||||
|
||||
### Present Suggestions
|
||||
|
||||
From your analysis, present 3-4 specific suggestions:
|
||||
|
||||
```
|
||||
## Task Suggestions
|
||||
|
||||
Based on scanning your codebase, here are some good starter tasks:
|
||||
|
||||
**1. [Most promising task]**
|
||||
Location: `src/path/to/file.ts:42`
|
||||
Scope: ~1-2 files, ~20-30 lines
|
||||
Why it's good: [brief reason]
|
||||
|
||||
**2. [Second task]**
|
||||
Location: `src/another/file.ts`
|
||||
Scope: ~1 file, ~15 lines
|
||||
Why it's good: [brief reason]
|
||||
|
||||
**3. [Third task]**
|
||||
Location: [location]
|
||||
Scope: [estimate]
|
||||
Why it's good: [brief reason]
|
||||
|
||||
**4. Something else?**
|
||||
Tell me what you'd like to work on.
|
||||
|
||||
Which task interests you? (Pick a number or describe your own)
|
||||
```
|
||||
|
||||
**If nothing found:** Fall back to asking what the user wants to build:
|
||||
> I didn't find obvious quick wins in your codebase. What's something small you've been meaning to add or fix?
|
||||
|
||||
### Scope Guardrail
|
||||
|
||||
If the user picks or describes something too large (major feature, multi-day work):
|
||||
|
||||
```
|
||||
That's a valuable task, but it's probably larger than ideal for your first OpenSpec run-through.
|
||||
|
||||
For learning the workflow, smaller is better—it lets you see the full cycle without getting stuck in implementation details.
|
||||
|
||||
**Options:**
|
||||
1. **Slice it smaller** - What's the smallest useful piece of [their task]? Maybe just [specific slice]?
|
||||
2. **Pick something else** - One of the other suggestions, or a different small task?
|
||||
3. **Do it anyway** - If you really want to tackle this, we can. Just know it'll take longer.
|
||||
|
||||
What would you prefer?
|
||||
```
|
||||
|
||||
Let the user override if they insist—this is a soft guardrail.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Explore Demo
|
||||
|
||||
Once a task is selected, briefly demonstrate explore mode:
|
||||
|
||||
```
|
||||
Before we create a change, let me quickly show you **explore mode**—it's how you think through problems before committing to a direction.
|
||||
```
|
||||
|
||||
Spend 1-2 minutes investigating the relevant code:
|
||||
- Read the file(s) involved
|
||||
- Draw a quick ASCII diagram if it helps
|
||||
- Note any considerations
|
||||
|
||||
```
|
||||
## Quick Exploration
|
||||
|
||||
[Your brief analysis—what you found, any considerations]
|
||||
|
||||
┌─────────────────────────────────────────┐
|
||||
│ [Optional: ASCII diagram if helpful] │
|
||||
└─────────────────────────────────────────┘
|
||||
|
||||
Explore mode (`/opsx:explore`) is for this kind of thinking—investigating before implementing. You can use it anytime you need to think through a problem.
|
||||
|
||||
Now let's create a change to hold our work.
|
||||
```
|
||||
|
||||
**PAUSE** - Wait for user acknowledgment before proceeding.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Create the Change
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## 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.
|
||||
|
||||
Let me create one for our task.
|
||||
```
|
||||
|
||||
**DO:** Create the change with a derived kebab-case name:
|
||||
```bash
|
||||
openspec new change "<derived-name>"
|
||||
```
|
||||
|
||||
**SHOW:**
|
||||
```
|
||||
Created: `openspec/changes/<name>/`
|
||||
|
||||
The folder structure:
|
||||
```
|
||||
openspec/changes/<name>/
|
||||
├── proposal.md ← Why we're doing this (empty, we'll fill it)
|
||||
├── design.md ← How we'll build it (empty)
|
||||
├── specs/ ← Detailed requirements (empty)
|
||||
└── tasks.md ← Implementation checklist (empty)
|
||||
```
|
||||
|
||||
Now let's fill in the first artifact—the proposal.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Proposal
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## The Proposal
|
||||
|
||||
The proposal captures **why** we're making this change and **what** it involves at a high level. It's the "elevator pitch" for the work.
|
||||
|
||||
I'll draft one based on our task.
|
||||
```
|
||||
|
||||
**DO:** Draft the proposal content (don't save yet):
|
||||
|
||||
```
|
||||
Here's a draft proposal:
|
||||
|
||||
---
|
||||
|
||||
## Why
|
||||
|
||||
[1-2 sentences explaining the problem/opportunity]
|
||||
|
||||
## What Changes
|
||||
|
||||
[Bullet points of what will be different]
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `<capability-name>`: [brief description]
|
||||
|
||||
### Modified Capabilities
|
||||
<!-- If modifying existing behavior -->
|
||||
|
||||
## Impact
|
||||
|
||||
- `src/path/to/file.ts`: [what changes]
|
||||
- [other files if applicable]
|
||||
|
||||
---
|
||||
|
||||
Does this capture the intent? I can adjust before we save it.
|
||||
```
|
||||
|
||||
**PAUSE** - Wait for user approval/feedback.
|
||||
|
||||
After approval, save the proposal:
|
||||
```bash
|
||||
openspec instructions proposal --change "<name>" --json
|
||||
```
|
||||
Then write the content to `openspec/changes/<name>/proposal.md`.
|
||||
|
||||
```
|
||||
Proposal saved. This is your "why" document—you can always come back and refine it as understanding evolves.
|
||||
|
||||
Next up: specs.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Specs
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## Specs
|
||||
|
||||
Specs define **what** we're building in precise, testable terms. They use a requirement/scenario format that makes expected behavior crystal clear.
|
||||
|
||||
For a small task like this, we might only need one spec file.
|
||||
```
|
||||
|
||||
**DO:** Create the spec file:
|
||||
```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>"
|
||||
```
|
||||
|
||||
Draft the spec content:
|
||||
|
||||
```
|
||||
Here's the spec:
|
||||
|
||||
---
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: <Name>
|
||||
|
||||
<Description of what the system should do>
|
||||
|
||||
#### Scenario: <Scenario name>
|
||||
|
||||
- **WHEN** <trigger condition>
|
||||
- **THEN** <expected outcome>
|
||||
- **AND** <additional outcome if needed>
|
||||
|
||||
---
|
||||
|
||||
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`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Design
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## Design
|
||||
|
||||
The design captures **how** we'll build it—technical decisions, tradeoffs, approach.
|
||||
|
||||
For small changes, this might be brief. That's fine—not every change needs deep design discussion.
|
||||
```
|
||||
|
||||
**DO:** Draft design.md:
|
||||
|
||||
```
|
||||
Here's the design:
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
[Brief context about the current state]
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- [What we're trying to achieve]
|
||||
|
||||
**Non-Goals:**
|
||||
- [What's explicitly out of scope]
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: [Key decision]
|
||||
|
||||
[Explanation of approach and rationale]
|
||||
|
||||
---
|
||||
|
||||
For a small task, this captures the key decisions without over-engineering.
|
||||
```
|
||||
|
||||
Save to `openspec/changes/<name>/design.md`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Tasks
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## Tasks
|
||||
|
||||
Finally, we break the work into implementation tasks—checkboxes that drive the apply phase.
|
||||
|
||||
These should be small, clear, and in logical order.
|
||||
```
|
||||
|
||||
**DO:** Generate tasks based on specs and design:
|
||||
|
||||
```
|
||||
Here are the implementation tasks:
|
||||
|
||||
---
|
||||
|
||||
## 1. [Category or file]
|
||||
|
||||
- [ ] 1.1 [Specific task]
|
||||
- [ ] 1.2 [Specific task]
|
||||
|
||||
## 2. Verify
|
||||
|
||||
- [ ] 2.1 [Verification step]
|
||||
|
||||
---
|
||||
|
||||
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`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 9: Apply (Implementation)
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## Implementation
|
||||
|
||||
Now we implement each task, checking them off as we go. I'll announce each one and occasionally note how the specs/design informed the approach.
|
||||
```
|
||||
|
||||
**DO:** For each task:
|
||||
|
||||
1. Announce: "Working on task N: [description]"
|
||||
2. Implement the change in the codebase
|
||||
3. Reference specs/design naturally: "The spec says X, so I'm doing Y"
|
||||
4. Mark complete in tasks.md: `- [ ]` → `- [x]`
|
||||
5. Brief status: "✓ Task N complete"
|
||||
|
||||
Keep narration light—don't over-explain every line of code.
|
||||
|
||||
After all tasks:
|
||||
|
||||
```
|
||||
## Implementation Complete
|
||||
|
||||
All tasks done:
|
||||
- [x] Task 1
|
||||
- [x] Task 2
|
||||
- [x] ...
|
||||
|
||||
The change is implemented! One more step—let's archive it.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 10: Archive
|
||||
|
||||
**EXPLAIN:**
|
||||
```
|
||||
## Archiving
|
||||
|
||||
When a change is complete, we archive it. This moves it from `openspec/changes/` to `openspec/changes/archive/YYYY-MM-DD-<name>/`.
|
||||
|
||||
Archived changes become your project's decision history—you can always find them later to understand why something was built a certain way.
|
||||
```
|
||||
|
||||
**DO:**
|
||||
```bash
|
||||
openspec archive "<name>"
|
||||
```
|
||||
|
||||
**SHOW:**
|
||||
```
|
||||
Archived to: `openspec/changes/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.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 11: Recap & Next Steps
|
||||
|
||||
```
|
||||
## Congratulations!
|
||||
|
||||
You just completed a full OpenSpec cycle:
|
||||
|
||||
1. **Explore** - Thought through the problem
|
||||
2. **New** - Created a change container
|
||||
3. **Proposal** - Captured WHY
|
||||
4. **Specs** - Defined WHAT in detail
|
||||
5. **Design** - Decided HOW
|
||||
6. **Tasks** - Broke it into steps
|
||||
7. **Apply** - Implemented the work
|
||||
8. **Archive** - Preserved the record
|
||||
|
||||
This same rhythm works for any size change—a small fix or a major feature.
|
||||
|
||||
---
|
||||
|
||||
## Command Reference
|
||||
|
||||
**Core workflow:**
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/opsx:propose` | Create a change and generate all artifacts |
|
||||
| `/opsx:explore` | Think through problems before/during work |
|
||||
| `/opsx:apply` | Implement tasks from a change |
|
||||
| `/opsx:archive` | Archive a completed change |
|
||||
|
||||
**Additional commands:**
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/opsx:new` | Start a new change, step through artifacts one at a time |
|
||||
| `/opsx:continue` | Continue working on an existing change |
|
||||
| `/opsx:ff` | Fast-forward: create all artifacts at once |
|
||||
| `/opsx:verify` | Verify implementation matches artifacts |
|
||||
|
||||
---
|
||||
|
||||
## What's Next?
|
||||
|
||||
Try `/opsx:propose` on something you actually want to build. You've got the rhythm now!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Graceful Exit Handling
|
||||
|
||||
### User wants to stop mid-way
|
||||
|
||||
If the user says they need to stop, want to pause, or seem disengaged:
|
||||
|
||||
```
|
||||
No problem! Your change is saved at `openspec/changes/<name>/`.
|
||||
|
||||
To pick up where we left off later:
|
||||
- `/opsx:continue <name>` - Resume artifact creation
|
||||
- `/opsx:apply <name>` - Jump to implementation (if tasks exist)
|
||||
|
||||
The work won't be lost. Come back whenever you're ready.
|
||||
```
|
||||
|
||||
Exit gracefully without pressure.
|
||||
|
||||
### User just wants command reference
|
||||
|
||||
If the user says they just want to see the commands or skip the tutorial:
|
||||
|
||||
```
|
||||
## OpenSpec Quick Reference
|
||||
|
||||
**Core workflow:**
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/opsx:propose <name>` | Create a change and generate all artifacts |
|
||||
| `/opsx:explore` | Think through problems (no code changes) |
|
||||
| `/opsx:apply <name>` | Implement tasks |
|
||||
| `/opsx:archive <name>` | Archive when done |
|
||||
|
||||
**Additional commands:**
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/opsx:new <name>` | Start a new change, step by step |
|
||||
| `/opsx:continue <name>` | Continue an existing change |
|
||||
| `/opsx:ff <name>` | Fast-forward: all artifacts at once |
|
||||
| `/opsx:verify <name>` | Verify implementation |
|
||||
|
||||
Try `/opsx:propose` to start your first change.
|
||||
```
|
||||
|
||||
Exit gracefully.
|
||||
|
||||
---
|
||||
|
||||
## Guardrails
|
||||
|
||||
- **Follow the EXPLAIN → DO → SHOW → PAUSE pattern** at key transitions (after explore, after proposal draft, after tasks, after archive)
|
||||
- **Keep narration light** during implementation—teach without lecturing
|
||||
- **Don't skip phases** even if the change is small—the goal is teaching the workflow
|
||||
- **Pause for acknowledgment** at marked points, but don't over-pause
|
||||
- **Handle exits gracefully**—never pressure the user to continue
|
||||
- **Use real codebase tasks**—don't simulate or use fake examples
|
||||
- **Adjust scope gently**—guide toward smaller tasks but respect user choice
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
---
|
||||
name: openspec-propose
|
||||
description: Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Propose a new change - create the change and generate all artifacts in one step.
|
||||
|
||||
I'll create a change with artifacts:
|
||||
- proposal.md (what & why)
|
||||
- design.md (how)
|
||||
- tasks.md (implementation steps)
|
||||
|
||||
When ready to implement, run /opsx:apply
|
||||
|
||||
---
|
||||
|
||||
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no clear input provided, ask what they want to build**
|
||||
|
||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||
> "What change do you want to work on? Describe what you want to build or fix."
|
||||
|
||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||
|
||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||
|
||||
2. **Create the change directory**
|
||||
```bash
|
||||
openspec new change "<name>"
|
||||
```
|
||||
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
|
||||
|
||||
3. **Get the artifact build order**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
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
|
||||
|
||||
4. **Create artifacts in sequence until apply-ready**
|
||||
|
||||
Use the **TodoWrite tool** to track progress through the artifacts.
|
||||
|
||||
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
||||
|
||||
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
||||
- Get instructions:
|
||||
```bash
|
||||
openspec instructions <artifact-id> --change "<name>" --json
|
||||
```
|
||||
- The instructions JSON includes:
|
||||
- `context`: Project background (constraints for you - do NOT include in output)
|
||||
- `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
|
||||
- `dependencies`: Completed artifacts to read for context
|
||||
- Read any completed dependency files for context
|
||||
- Create the artifact file using `template` as the structure
|
||||
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
||||
- Show brief progress: "Created <artifact-id>"
|
||||
|
||||
b. **Continue until all `applyRequires` artifacts are complete**
|
||||
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
||||
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
||||
- Stop when all `applyRequires` artifacts are done
|
||||
|
||||
c. **If an artifact requires user input** (unclear context):
|
||||
- Use **AskUserQuestion tool** to clarify
|
||||
- Then continue with creation
|
||||
|
||||
5. **Show final status**
|
||||
```bash
|
||||
openspec status --change "<name>"
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
After completing all artifacts, summarize:
|
||||
- Change name and location
|
||||
- List of artifacts created with brief descriptions
|
||||
- What's ready: "All artifacts created! Ready for implementation."
|
||||
- Prompt: "Run `/opsx:apply` or ask me to implement to start working on the tasks."
|
||||
|
||||
**Artifact Creation Guidelines**
|
||||
|
||||
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
||||
- The schema defines what each artifact should contain - follow it
|
||||
- Read dependency artifacts for context before creating new ones
|
||||
- Use `template` as the structure for your output file - fill in its sections
|
||||
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
||||
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
||||
- These guide what you write, but should never appear in the output
|
||||
|
||||
**Guardrails**
|
||||
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
||||
- Always read dependency artifacts before creating a new one
|
||||
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
||||
- If a change with that name already exists, ask if user wants to continue it or create a new one
|
||||
- Verify each artifact file exists after writing before proceeding to next
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
---
|
||||
name: openspec-sync-specs
|
||||
description: Sync delta specs from a change to main specs. Use when the user wants to update main specs with changes from a delta spec, without archiving the change.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Sync delta specs from a change to main specs.
|
||||
|
||||
This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement).
|
||||
|
||||
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||
|
||||
Show changes that have delta specs (under `specs/` directory).
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Find delta specs**
|
||||
|
||||
Look for delta spec files in `openspec/changes/<name>/specs/*/spec.md`.
|
||||
|
||||
Each delta spec file contains sections like:
|
||||
- `## ADDED Requirements` - New requirements to add
|
||||
- `## MODIFIED Requirements` - Changes to existing requirements
|
||||
- `## REMOVED Requirements` - Requirements to remove
|
||||
- `## RENAMED Requirements` - Requirements to rename (FROM:/TO: format)
|
||||
|
||||
If no delta specs found, inform user and stop.
|
||||
|
||||
3. **For each delta spec, apply changes to main specs**
|
||||
|
||||
For each capability with a delta spec at `openspec/changes/<name>/specs/<capability>/spec.md`:
|
||||
|
||||
a. **Read the delta spec** to understand the intended changes
|
||||
|
||||
b. **Read the main spec** at `openspec/specs/<capability>/spec.md` (may not exist yet)
|
||||
|
||||
c. **Apply changes intelligently**:
|
||||
|
||||
**ADDED Requirements:**
|
||||
- If requirement doesn't exist in main spec → add it
|
||||
- If requirement already exists → update it to match (treat as implicit MODIFIED)
|
||||
|
||||
**MODIFIED Requirements:**
|
||||
- Find the requirement in main spec
|
||||
- Apply the changes - this can be:
|
||||
- Adding new scenarios (don't need to copy existing ones)
|
||||
- Modifying existing scenarios
|
||||
- Changing the requirement description
|
||||
- Preserve scenarios/content not mentioned in the delta
|
||||
|
||||
**REMOVED Requirements:**
|
||||
- Remove the entire requirement block from main spec
|
||||
|
||||
**RENAMED Requirements:**
|
||||
- Find the FROM requirement, rename to TO
|
||||
|
||||
d. **Create new main spec** if capability doesn't exist yet:
|
||||
- Create `openspec/specs/<capability>/spec.md`
|
||||
- Add Purpose section (can be brief, mark as TBD)
|
||||
- Add Requirements section with the ADDED requirements
|
||||
|
||||
4. **Show summary**
|
||||
|
||||
After applying all changes, summarize:
|
||||
- Which capabilities were updated
|
||||
- What changes were made (requirements added/modified/removed/renamed)
|
||||
|
||||
**Delta Spec Format Reference**
|
||||
|
||||
```markdown
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: New Feature
|
||||
The system SHALL do something new.
|
||||
|
||||
#### Scenario: Basic case
|
||||
- **WHEN** user does X
|
||||
- **THEN** system does Y
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Existing Feature
|
||||
#### Scenario: New scenario to add
|
||||
- **WHEN** user does A
|
||||
- **THEN** system does B
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Deprecated Feature
|
||||
|
||||
## RENAMED Requirements
|
||||
|
||||
- FROM: `### Requirement: Old Name`
|
||||
- TO: `### Requirement: New Name`
|
||||
```
|
||||
|
||||
**Key Principle: Intelligent Merging**
|
||||
|
||||
Unlike programmatic merging, you can apply **partial updates**:
|
||||
- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
|
||||
- The delta represents *intent*, not a wholesale replacement
|
||||
- Use your judgment to merge changes sensibly
|
||||
|
||||
**Output On Success**
|
||||
|
||||
```
|
||||
## Specs Synced: <change-name>
|
||||
|
||||
Updated main specs:
|
||||
|
||||
**<capability-1>**:
|
||||
- Added requirement: "New Feature"
|
||||
- Modified requirement: "Existing Feature" (added 1 scenario)
|
||||
|
||||
**<capability-2>**:
|
||||
- Created new spec file
|
||||
- Added requirement: "Another Feature"
|
||||
|
||||
Main specs are now updated. The change remains active - archive when implementation is complete.
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Read both delta and main specs before making changes
|
||||
- Preserve existing content not mentioned in delta
|
||||
- If something is unclear, ask for clarification
|
||||
- Show what you're changing as you go
|
||||
- The operation should be idempotent - running twice should give same result
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
---
|
||||
name: openspec-verify-change
|
||||
description: Verify implementation matches change artifacts. Use when the user wants to validate that implementation is complete, correct, and coherent before archiving.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Verify that an implementation matches the change artifacts (specs, tasks, design).
|
||||
|
||||
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||
|
||||
Show changes that have implementation tasks (tasks artifact exists).
|
||||
Include the schema used for each change if available.
|
||||
Mark changes with incomplete tasks as "(In Progress)".
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check status to understand the schema**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
||||
- Which artifacts exist for this change
|
||||
|
||||
3. **Get the change directory and load artifacts**
|
||||
|
||||
```bash
|
||||
openspec instructions apply --change "<name>" --json
|
||||
```
|
||||
|
||||
This returns the change directory and context files. Read all available artifacts from `contextFiles`.
|
||||
|
||||
4. **Initialize verification report structure**
|
||||
|
||||
Create a report structure with three dimensions:
|
||||
- **Completeness**: Track tasks and spec coverage
|
||||
- **Correctness**: Track requirement implementation and scenario coverage
|
||||
- **Coherence**: Track design adherence and pattern consistency
|
||||
|
||||
Each dimension can have CRITICAL, WARNING, or SUGGESTION issues.
|
||||
|
||||
5. **Verify Completeness**
|
||||
|
||||
**Task Completion**:
|
||||
- If tasks.md exists in contextFiles, read it
|
||||
- Parse checkboxes: `- [ ]` (incomplete) vs `- [x]` (complete)
|
||||
- Count complete vs total tasks
|
||||
- If incomplete tasks exist:
|
||||
- Add CRITICAL issue for each incomplete task
|
||||
- Recommendation: "Complete task: <description>" or "Mark as done if already implemented"
|
||||
|
||||
**Spec Coverage**:
|
||||
- If delta specs exist in `openspec/changes/<name>/specs/`:
|
||||
- Extract all requirements (marked with "### Requirement:")
|
||||
- For each requirement:
|
||||
- Search codebase for keywords related to the requirement
|
||||
- Assess if implementation likely exists
|
||||
- If requirements appear unimplemented:
|
||||
- Add CRITICAL issue: "Requirement not found: <requirement name>"
|
||||
- Recommendation: "Implement requirement X: <description>"
|
||||
|
||||
6. **Verify Correctness**
|
||||
|
||||
**Requirement Implementation Mapping**:
|
||||
- For each requirement from delta specs:
|
||||
- Search codebase for implementation evidence
|
||||
- If found, note file paths and line ranges
|
||||
- Assess if implementation matches requirement intent
|
||||
- If divergence detected:
|
||||
- Add WARNING: "Implementation may diverge from spec: <details>"
|
||||
- Recommendation: "Review <file>:<lines> against requirement X"
|
||||
|
||||
**Scenario Coverage**:
|
||||
- For each scenario in delta specs (marked with "#### Scenario:"):
|
||||
- Check if conditions are handled in code
|
||||
- Check if tests exist covering the scenario
|
||||
- If scenario appears uncovered:
|
||||
- Add WARNING: "Scenario not covered: <scenario name>"
|
||||
- Recommendation: "Add test or implementation for scenario: <description>"
|
||||
|
||||
7. **Verify Coherence**
|
||||
|
||||
**Design Adherence**:
|
||||
- If design.md exists in contextFiles:
|
||||
- Extract key decisions (look for sections like "Decision:", "Approach:", "Architecture:")
|
||||
- Verify implementation follows those decisions
|
||||
- If contradiction detected:
|
||||
- Add WARNING: "Design decision not followed: <decision>"
|
||||
- Recommendation: "Update implementation or revise design.md to match reality"
|
||||
- If no design.md: Skip design adherence check, note "No design.md to verify against"
|
||||
|
||||
**Code Pattern Consistency**:
|
||||
- Review new code for consistency with project patterns
|
||||
- Check file naming, directory structure, coding style
|
||||
- If significant deviations found:
|
||||
- Add SUGGESTION: "Code pattern deviation: <details>"
|
||||
- Recommendation: "Consider following project pattern: <example>"
|
||||
|
||||
8. **Generate Verification Report**
|
||||
|
||||
**Summary Scorecard**:
|
||||
```
|
||||
## Verification Report: <change-name>
|
||||
|
||||
### Summary
|
||||
| Dimension | Status |
|
||||
|--------------|------------------|
|
||||
| Completeness | X/Y tasks, N reqs|
|
||||
| Correctness | M/N reqs covered |
|
||||
| Coherence | Followed/Issues |
|
||||
```
|
||||
|
||||
**Issues by Priority**:
|
||||
|
||||
1. **CRITICAL** (Must fix before archive):
|
||||
- Incomplete tasks
|
||||
- Missing requirement implementations
|
||||
- Each with specific, actionable recommendation
|
||||
|
||||
2. **WARNING** (Should fix):
|
||||
- Spec/design divergences
|
||||
- Missing scenario coverage
|
||||
- Each with specific recommendation
|
||||
|
||||
3. **SUGGESTION** (Nice to fix):
|
||||
- Pattern inconsistencies
|
||||
- Minor improvements
|
||||
- Each with specific recommendation
|
||||
|
||||
**Final Assessment**:
|
||||
- If CRITICAL issues: "X critical issue(s) found. Fix before archiving."
|
||||
- If only warnings: "No critical issues. Y warning(s) to consider. Ready for archive (with noted improvements)."
|
||||
- If all clear: "All checks passed. Ready for archive."
|
||||
|
||||
**Verification Heuristics**
|
||||
|
||||
- **Completeness**: Focus on objective checklist items (checkboxes, requirements list)
|
||||
- **Correctness**: Use keyword search, file path analysis, reasonable inference - don't require perfect certainty
|
||||
- **Coherence**: Look for glaring inconsistencies, don't nitpick style
|
||||
- **False Positives**: When uncertain, prefer SUGGESTION over WARNING, WARNING over CRITICAL
|
||||
- **Actionability**: Every issue must have a specific recommendation with file/line references where applicable
|
||||
|
||||
**Graceful Degradation**
|
||||
|
||||
- If only tasks.md exists: verify task completion only, skip spec/design checks
|
||||
- If tasks + specs exist: verify completeness and correctness, skip design
|
||||
- If full artifacts: verify all three dimensions
|
||||
- Always note which checks were skipped and why
|
||||
|
||||
**Output Format**
|
||||
|
||||
Use clear markdown with:
|
||||
- Table for summary scorecard
|
||||
- Grouped lists for issues (CRITICAL/WARNING/SUGGESTION)
|
||||
- Code references in format: `file.ts:123`
|
||||
- Specific, actionable recommendations
|
||||
- No vague suggestions like "consider reviewing"
|
||||
|
|
@ -98,6 +98,7 @@ public sealed class ClientAuthoritativePlayerStateSnapshot
|
|||
SourceState = state.Clone();
|
||||
PlayerId = SourceState.PlayerId ?? string.Empty;
|
||||
Tick = SourceState.Tick;
|
||||
AcknowledgedMoveTick = SourceState.AcknowledgedMoveTick;
|
||||
Position = SourceState.Position != null ? SourceState.Position.ToVector3() : Vector3.zero;
|
||||
Velocity = SourceState.Velocity != null ? SourceState.Velocity.ToVector3() : Vector3.zero;
|
||||
Rotation = SourceState.Rotation;
|
||||
|
|
@ -110,6 +111,8 @@ public sealed class ClientAuthoritativePlayerStateSnapshot
|
|||
|
||||
public long Tick { get; }
|
||||
|
||||
public long AcknowledgedMoveTick { get; }
|
||||
|
||||
public Vector3 Position { get; }
|
||||
|
||||
public Vector3 Velocity { get; }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,159 @@
|
|||
using UnityEngine;
|
||||
using Vector3 = UnityEngine.Vector3;
|
||||
|
||||
public readonly struct ControlledPlayerCorrectionSettings
|
||||
{
|
||||
public ControlledPlayerCorrectionSettings(
|
||||
float authoritativeCadenceSeconds,
|
||||
float moveSpeed,
|
||||
float turnSpeedDegreesPerSecond,
|
||||
float snapDistanceMultiplier = 3f,
|
||||
float snapAngleMultiplier = 15f)
|
||||
{
|
||||
AuthoritativeCadenceSeconds = Mathf.Max(0f, authoritativeCadenceSeconds);
|
||||
MoveSpeed = Mathf.Max(0f, moveSpeed);
|
||||
TurnSpeedDegreesPerSecond = Mathf.Max(0f, turnSpeedDegreesPerSecond);
|
||||
SnapDistanceMultiplier = Mathf.Max(1f, snapDistanceMultiplier);
|
||||
SnapAngleMultiplier = Mathf.Max(1f, snapAngleMultiplier);
|
||||
}
|
||||
|
||||
public float AuthoritativeCadenceSeconds { get; }
|
||||
|
||||
public float MoveSpeed { get; }
|
||||
|
||||
public float TurnSpeedDegreesPerSecond { get; }
|
||||
|
||||
public float SnapDistanceMultiplier { get; }
|
||||
|
||||
public float SnapAngleMultiplier { get; }
|
||||
|
||||
public float MaxBoundedPositionCorrection => MoveSpeed * AuthoritativeCadenceSeconds;
|
||||
|
||||
public float MaxBoundedRotationCorrectionDegrees => TurnSpeedDegreesPerSecond * AuthoritativeCadenceSeconds;
|
||||
|
||||
public float SnapPositionThreshold => MaxBoundedPositionCorrection * SnapDistanceMultiplier;
|
||||
|
||||
public float SnapRotationThresholdDegrees => MaxBoundedRotationCorrectionDegrees * SnapAngleMultiplier;
|
||||
|
||||
public int MaxCorrectionSteps => Mathf.Max(1, Mathf.CeilToInt(SnapDistanceMultiplier));
|
||||
}
|
||||
|
||||
public readonly struct ControlledPlayerVisualCorrectionState
|
||||
{
|
||||
public static ControlledPlayerVisualCorrectionState None => default;
|
||||
|
||||
public ControlledPlayerVisualCorrectionState(Vector3 targetPosition, Quaternion targetRotation, int remainingStepBudget)
|
||||
{
|
||||
TargetPosition = targetPosition;
|
||||
TargetRotation = targetRotation;
|
||||
RemainingStepBudget = Mathf.Max(0, remainingStepBudget);
|
||||
}
|
||||
|
||||
public Vector3 TargetPosition { get; }
|
||||
|
||||
public Quaternion TargetRotation { get; }
|
||||
|
||||
public int RemainingStepBudget { get; }
|
||||
|
||||
public bool IsActive => RemainingStepBudget > 0;
|
||||
}
|
||||
|
||||
public readonly struct ControlledPlayerCorrectionResult
|
||||
{
|
||||
public ControlledPlayerCorrectionResult(
|
||||
Vector3 position,
|
||||
Quaternion rotation,
|
||||
bool usedHardSnap,
|
||||
ControlledPlayerVisualCorrectionState nextState)
|
||||
{
|
||||
Position = position;
|
||||
Rotation = rotation;
|
||||
UsedHardSnap = usedHardSnap;
|
||||
NextState = nextState;
|
||||
}
|
||||
|
||||
public Vector3 Position { get; }
|
||||
|
||||
public Quaternion Rotation { get; }
|
||||
|
||||
public bool UsedHardSnap { get; }
|
||||
|
||||
public ControlledPlayerVisualCorrectionState NextState { get; }
|
||||
}
|
||||
|
||||
public static class ControlledPlayerCorrection
|
||||
{
|
||||
public static ControlledPlayerCorrectionResult Resolve(
|
||||
Vector3 currentPosition,
|
||||
Quaternion currentRotation,
|
||||
Vector3 targetPosition,
|
||||
Quaternion targetRotation,
|
||||
ControlledPlayerCorrectionSettings settings)
|
||||
{
|
||||
return Resolve(
|
||||
currentPosition,
|
||||
currentRotation,
|
||||
targetPosition,
|
||||
targetRotation,
|
||||
settings,
|
||||
ControlledPlayerVisualCorrectionState.None);
|
||||
}
|
||||
|
||||
public static ControlledPlayerCorrectionResult Resolve(
|
||||
Vector3 currentPosition,
|
||||
Quaternion currentRotation,
|
||||
Vector3 targetPosition,
|
||||
Quaternion targetRotation,
|
||||
ControlledPlayerCorrectionSettings settings,
|
||||
ControlledPlayerVisualCorrectionState activeCorrection)
|
||||
{
|
||||
var positionError = Vector3.Distance(currentPosition, targetPosition);
|
||||
var rotationError = Quaternion.Angle(currentRotation, targetRotation);
|
||||
var boundedPositionCorrection = settings.MaxBoundedPositionCorrection;
|
||||
var boundedRotationCorrection = settings.MaxBoundedRotationCorrectionDegrees;
|
||||
|
||||
if (positionError <= Mathf.Epsilon && rotationError <= Mathf.Epsilon)
|
||||
{
|
||||
return new ControlledPlayerCorrectionResult(targetPosition, targetRotation, false, ControlledPlayerVisualCorrectionState.None);
|
||||
}
|
||||
|
||||
if (boundedPositionCorrection <= Mathf.Epsilon && boundedRotationCorrection <= Mathf.Epsilon)
|
||||
{
|
||||
return new ControlledPlayerCorrectionResult(targetPosition, targetRotation, true, ControlledPlayerVisualCorrectionState.None);
|
||||
}
|
||||
|
||||
if (positionError > settings.SnapPositionThreshold || rotationError > settings.SnapRotationThresholdDegrees)
|
||||
{
|
||||
return new ControlledPlayerCorrectionResult(targetPosition, targetRotation, true, ControlledPlayerVisualCorrectionState.None);
|
||||
}
|
||||
|
||||
var remainingStepBudget = activeCorrection.IsActive
|
||||
? activeCorrection.RemainingStepBudget
|
||||
: settings.MaxCorrectionSteps;
|
||||
if (remainingStepBudget <= 0)
|
||||
{
|
||||
return new ControlledPlayerCorrectionResult(targetPosition, targetRotation, true, ControlledPlayerVisualCorrectionState.None);
|
||||
}
|
||||
|
||||
var correctedPosition = Vector3.MoveTowards(currentPosition, targetPosition, boundedPositionCorrection);
|
||||
var correctedRotation = Quaternion.RotateTowards(currentRotation, targetRotation, boundedRotationCorrection);
|
||||
var nextPositionError = Vector3.Distance(correctedPosition, targetPosition);
|
||||
var nextRotationError = Quaternion.Angle(correctedRotation, targetRotation);
|
||||
if (nextPositionError <= Mathf.Epsilon && nextRotationError <= Mathf.Epsilon)
|
||||
{
|
||||
return new ControlledPlayerCorrectionResult(targetPosition, targetRotation, false, ControlledPlayerVisualCorrectionState.None);
|
||||
}
|
||||
|
||||
remainingStepBudget--;
|
||||
if (remainingStepBudget <= 0)
|
||||
{
|
||||
return new ControlledPlayerCorrectionResult(targetPosition, targetRotation, true, ControlledPlayerVisualCorrectionState.None);
|
||||
}
|
||||
|
||||
return new ControlledPlayerCorrectionResult(
|
||||
correctedPosition,
|
||||
correctedRotation,
|
||||
false,
|
||||
new ControlledPlayerVisualCorrectionState(targetPosition, targetRotation, remainingStepBudget));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9f70a048da9736c498daa681ba153fb0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using Network.Defines;
|
||||
using Network.NetworkApplication;
|
||||
using UnityEngine;
|
||||
using Vector3 = UnityEngine.Vector3;
|
||||
|
||||
|
|
@ -20,6 +21,7 @@ public class MasterManager : MonoBehaviour
|
|||
|
||||
public void InitPlayersState(LoginResponse response)
|
||||
{
|
||||
var localBootstrap = ClientMovementBootstrap.FromLoginResponse(response);
|
||||
for (int i = 0; i < response.Positions.Count; i++)
|
||||
{
|
||||
string id = response.PlayerId[i];
|
||||
|
|
@ -31,17 +33,17 @@ public class MasterManager : MonoBehaviour
|
|||
}
|
||||
else
|
||||
{
|
||||
RegisterLocalPlayer(response.Speed, response.ServerTick);
|
||||
RegisterLocalPlayer(localBootstrap);
|
||||
var ui = GameObject.Find("RegisterCanvas");
|
||||
ui.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterLocalPlayer(int speed, long serverTick)
|
||||
private void RegisterLocalPlayer(ClientMovementBootstrap bootstrap)
|
||||
{
|
||||
Player player = GameObject.Instantiate(_playerPrefab, _playerParent).GetComponent<Player>();
|
||||
player.LocalInit(LocalPlayerId, speed, serverTick);
|
||||
player.LocalInit(LocalPlayerId, bootstrap);
|
||||
_players.Add(LocalPlayerId, player);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -106,6 +106,11 @@ public class MovementComponent : MonoBehaviour
|
|||
private Player _master;
|
||||
private const float TurnSpeedDegreesPerSecond = 180f;
|
||||
private const float UnityYawOffsetDegrees = 90f;
|
||||
|
||||
// Server authoritative movement cadence used for replay substepping.
|
||||
// This matches ServerAuthoritativeMovementConfiguration.SimulationInterval (50ms).
|
||||
private const float kServerSimulationStepSeconds = 0.05f;
|
||||
|
||||
private int _speed = 2;
|
||||
[SerializeField] private Rigidbody _rigid;
|
||||
private float _lastSendTime = 0;
|
||||
|
|
@ -114,6 +119,7 @@ public class MovementComponent : MonoBehaviour
|
|||
private Vector3 _serverPosition;
|
||||
private bool _hasServerState = false;
|
||||
private ClientAuthoritativePlayerStateSnapshot _lastAuthoritativeState;
|
||||
private ControlledPlayerVisualCorrectionState _activeVisualCorrection;
|
||||
|
||||
public long Tick { get; private set; } = 0;
|
||||
private long _startTickOffset = 0;
|
||||
|
|
@ -127,6 +133,11 @@ public class MovementComponent : MonoBehaviour
|
|||
private bool _wasMovingLastFrame;
|
||||
private bool _stopMessagePending;
|
||||
|
||||
public void Init(bool isControlled, Player master, ClientMovementBootstrap bootstrap)
|
||||
{
|
||||
Init(isControlled, master, bootstrap.AuthoritativeMoveSpeed, bootstrap.ServerTick);
|
||||
}
|
||||
|
||||
public void Init(bool isControlled, Player master, int speed = 0, long serverTick = 0)
|
||||
{
|
||||
_master = master;
|
||||
|
|
@ -212,15 +223,24 @@ public class MovementComponent : MonoBehaviour
|
|||
|
||||
private void Reconcile(ClientAuthoritativePlayerStateSnapshot snapshot)
|
||||
{
|
||||
_serverPosition = snapshot.Position;
|
||||
if (!_predictionBuffer.TryApplyAuthoritativeState(snapshot.SourceState, out var replayInputs))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_serverPosition = snapshot.Position;
|
||||
_rigid.position = _serverPosition;
|
||||
_rigid.rotation = snapshot.RotationQuaternion;
|
||||
_rigid.velocity = snapshot.Velocity;
|
||||
var correction = ControlledPlayerCorrection.Resolve(
|
||||
_rigid.position,
|
||||
_rigid.rotation,
|
||||
snapshot.Position,
|
||||
snapshot.RotationQuaternion,
|
||||
new ControlledPlayerCorrectionSettings(kServerSimulationStepSeconds, _speed, TurnSpeedDegreesPerSecond),
|
||||
_activeVisualCorrection);
|
||||
|
||||
_activeVisualCorrection = correction.NextState;
|
||||
_rigid.position = correction.Position;
|
||||
_rigid.rotation = correction.Rotation;
|
||||
_rigid.velocity = correction.UsedHardSnap ? snapshot.Velocity : Vector3.zero;
|
||||
_rigid.angularVelocity = Vector3.zero;
|
||||
ReplayPendingInputs(replayInputs);
|
||||
}
|
||||
|
|
@ -305,7 +325,18 @@ public class MovementComponent : MonoBehaviour
|
|||
{
|
||||
foreach (var replayInput in replayInputs)
|
||||
{
|
||||
ApplyTankMovement(replayInput.Input.TurnInput, replayInput.Input.ThrottleInput, replayInput.SimulatedDurationSeconds);
|
||||
var remaining = replayInput.SimulatedDurationSeconds;
|
||||
while (remaining > 0f)
|
||||
{
|
||||
// Use the server's fixed cadence (50ms) as the substep size to ensure
|
||||
// replay trajectory matches live FixedUpdate prediction exactly.
|
||||
var step = Mathf.Min(remaining, kServerSimulationStepSeconds);
|
||||
ApplyTankMovement(
|
||||
replayInput.Input.TurnInput,
|
||||
replayInput.Input.ThrottleInput,
|
||||
step);
|
||||
remaining -= step;
|
||||
}
|
||||
}
|
||||
|
||||
if (_isControlled)
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ namespace Network.Defines {
|
|||
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.MoveInput), global::Network.Defines.MoveInput.Parser, new[]{ "PlayerId", "Tick", "TurnInput", "ThrottleInput" }, null, null, null, null),
|
||||
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.ShootInput), global::Network.Defines.ShootInput.Parser, new[]{ "PlayerId", "Tick", "DirX", "DirY", "TargetId" }, null, null, null, null),
|
||||
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.CombatEvent), global::Network.Defines.CombatEvent.Parser, new[]{ "Tick", "EventType", "AttackerId", "TargetId", "Damage", "HitPosition" }, null, null, null, null),
|
||||
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.PlayerState), global::Network.Defines.PlayerState.Parser, new[]{ "PlayerId", "Position", "Velocity", "Rotation", "Tick", "Hp" }, null, null, null, null),
|
||||
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.PlayerState), global::Network.Defines.PlayerState.Parser, new[]{ "PlayerId", "Position", "Velocity", "Rotation", "Tick", "Hp", "AcknowledgedMoveTick" }, null, null, null, null),
|
||||
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.Heartbeat), global::Network.Defines.Heartbeat.Parser, null, null, null, null, null),
|
||||
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.HeartbeatResponse), global::Network.Defines.HeartbeatResponse.Parser, new[]{ "ServerTick" }, null, null, null, null)
|
||||
}));
|
||||
|
|
@ -2679,6 +2679,7 @@ namespace Network.Defines {
|
|||
rotation_ = other.rotation_;
|
||||
tick_ = other.tick_;
|
||||
hp_ = other.hp_;
|
||||
acknowledgedMoveTick_ = other.acknowledgedMoveTick_;
|
||||
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
|
||||
}
|
||||
|
||||
|
|
@ -2760,6 +2761,18 @@ namespace Network.Defines {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>Field number for the "acknowledged_move_tick" field.</summary>
|
||||
public const int AcknowledgedMoveTickFieldNumber = 7;
|
||||
private long acknowledgedMoveTick_;
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public long AcknowledgedMoveTick {
|
||||
get { return acknowledgedMoveTick_; }
|
||||
set {
|
||||
acknowledgedMoveTick_ = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public override bool Equals(object other) {
|
||||
|
|
@ -2781,6 +2794,7 @@ namespace Network.Defines {
|
|||
if (!pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.Equals(Rotation, other.Rotation)) return false;
|
||||
if (Tick != other.Tick) return false;
|
||||
if (Hp != other.Hp) return false;
|
||||
if (AcknowledgedMoveTick != other.AcknowledgedMoveTick) return false;
|
||||
return Equals(_unknownFields, other._unknownFields);
|
||||
}
|
||||
|
||||
|
|
@ -2794,6 +2808,7 @@ namespace Network.Defines {
|
|||
if (Rotation != 0F) hash ^= pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.GetHashCode(Rotation);
|
||||
if (Tick != 0L) hash ^= Tick.GetHashCode();
|
||||
if (Hp != 0) hash ^= Hp.GetHashCode();
|
||||
if (AcknowledgedMoveTick != 0L) hash ^= AcknowledgedMoveTick.GetHashCode();
|
||||
if (_unknownFields != null) {
|
||||
hash ^= _unknownFields.GetHashCode();
|
||||
}
|
||||
|
|
@ -2836,6 +2851,10 @@ namespace Network.Defines {
|
|||
output.WriteRawTag(48);
|
||||
output.WriteInt32(Hp);
|
||||
}
|
||||
if (AcknowledgedMoveTick != 0L) {
|
||||
output.WriteRawTag(56);
|
||||
output.WriteInt64(AcknowledgedMoveTick);
|
||||
}
|
||||
if (_unknownFields != null) {
|
||||
_unknownFields.WriteTo(output);
|
||||
}
|
||||
|
|
@ -2870,6 +2889,10 @@ namespace Network.Defines {
|
|||
output.WriteRawTag(48);
|
||||
output.WriteInt32(Hp);
|
||||
}
|
||||
if (AcknowledgedMoveTick != 0L) {
|
||||
output.WriteRawTag(56);
|
||||
output.WriteInt64(AcknowledgedMoveTick);
|
||||
}
|
||||
if (_unknownFields != null) {
|
||||
_unknownFields.WriteTo(ref output);
|
||||
}
|
||||
|
|
@ -2898,6 +2921,9 @@ namespace Network.Defines {
|
|||
if (Hp != 0) {
|
||||
size += 1 + pb::CodedOutputStream.ComputeInt32Size(Hp);
|
||||
}
|
||||
if (AcknowledgedMoveTick != 0L) {
|
||||
size += 1 + pb::CodedOutputStream.ComputeInt64Size(AcknowledgedMoveTick);
|
||||
}
|
||||
if (_unknownFields != null) {
|
||||
size += _unknownFields.CalculateSize();
|
||||
}
|
||||
|
|
@ -2934,6 +2960,9 @@ namespace Network.Defines {
|
|||
if (other.Hp != 0) {
|
||||
Hp = other.Hp;
|
||||
}
|
||||
if (other.AcknowledgedMoveTick != 0L) {
|
||||
AcknowledgedMoveTick = other.AcknowledgedMoveTick;
|
||||
}
|
||||
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
|
||||
}
|
||||
|
||||
|
|
@ -2983,6 +3012,10 @@ namespace Network.Defines {
|
|||
Hp = input.ReadInt32();
|
||||
break;
|
||||
}
|
||||
case 56: {
|
||||
AcknowledgedMoveTick = input.ReadInt64();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
@ -3032,6 +3065,10 @@ namespace Network.Defines {
|
|||
Hp = input.ReadInt32();
|
||||
break;
|
||||
}
|
||||
case 56: {
|
||||
AcknowledgedMoveTick = input.ReadInt64();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ message PlayerState {
|
|||
float rotation = 4;
|
||||
int64 tick = 5;
|
||||
int32 hp = 6;
|
||||
int64 acknowledged_move_tick = 7;
|
||||
}
|
||||
|
||||
message Heartbeat {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
using System;
|
||||
using Network.Defines;
|
||||
|
||||
namespace Network.NetworkApplication
|
||||
{
|
||||
public readonly struct ClientMovementBootstrap
|
||||
{
|
||||
public ClientMovementBootstrap(int authoritativeMoveSpeed, long serverTick)
|
||||
{
|
||||
AuthoritativeMoveSpeed = authoritativeMoveSpeed;
|
||||
ServerTick = serverTick;
|
||||
}
|
||||
|
||||
public int AuthoritativeMoveSpeed { get; }
|
||||
|
||||
public long ServerTick { get; }
|
||||
|
||||
public static ClientMovementBootstrap FromLoginResponse(LoginResponse response)
|
||||
{
|
||||
if (response == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(response));
|
||||
}
|
||||
|
||||
return new ClientMovementBootstrap(response.Speed, response.ServerTick);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0f82d57bd033abb439004c7ad1b201b0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -24,6 +24,8 @@ namespace Network.NetworkApplication
|
|||
|
||||
public long? LastAuthoritativeTick { get; private set; }
|
||||
|
||||
public long? LastAcknowledgedMoveTick { get; private set; }
|
||||
|
||||
public IReadOnlyList<PredictedMoveStep> PendingInputs => pendingInputs;
|
||||
|
||||
public void Record(MoveInput input)
|
||||
|
|
@ -66,7 +68,8 @@ namespace Network.NetworkApplication
|
|||
}
|
||||
|
||||
LastAuthoritativeTick = state.Tick;
|
||||
pendingInputs.RemoveAll(input => input.Input.Tick <= state.Tick);
|
||||
LastAcknowledgedMoveTick = state.AcknowledgedMoveTick;
|
||||
pendingInputs.RemoveAll(input => input.Input.Tick <= state.AcknowledgedMoveTick);
|
||||
replayInputs = pendingInputs.ToArray();
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ namespace Network.NetworkHost
|
|||
|
||||
public float TurnSpeedDegreesPerSecond { get; set; } = 180f;
|
||||
|
||||
public TimeSpan SimulationInterval { get; set; } = TimeSpan.FromMilliseconds(50);
|
||||
|
||||
public TimeSpan BroadcastInterval { get; set; } = TimeSpan.FromMilliseconds(50);
|
||||
|
||||
public int DefaultHp { get; set; } = 100;
|
||||
|
|
@ -24,6 +26,11 @@ namespace Network.NetworkHost
|
|||
throw new ArgumentOutOfRangeException(nameof(TurnSpeedDegreesPerSecond), "Turn speed must be finite and non-negative.");
|
||||
}
|
||||
|
||||
if (SimulationInterval <= TimeSpan.Zero)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(SimulationInterval), "Simulation interval must be positive.");
|
||||
}
|
||||
|
||||
if (BroadcastInterval <= TimeSpan.Zero)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(BroadcastInterval), "Broadcast interval must be positive.");
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ namespace Network.NetworkHost
|
|||
private readonly IAuthoritativeMovementWorldValidator worldValidator;
|
||||
private readonly Dictionary<string, ServerAuthoritativeMovementState> statesByPeer = new();
|
||||
private long nextBroadcastTick = 1;
|
||||
private TimeSpan accumulatedSimulationTime;
|
||||
private TimeSpan accumulatedBroadcastTime;
|
||||
|
||||
public ServerAuthoritativeMovementCoordinator(
|
||||
|
|
@ -31,6 +32,8 @@ namespace Network.NetworkHost
|
|||
this.worldValidator = worldValidator ?? throw new ArgumentNullException(nameof(worldValidator));
|
||||
}
|
||||
|
||||
public TimeSpan SimulationInterval => configuration.SimulationInterval;
|
||||
|
||||
public IReadOnlyList<ServerAuthoritativeMovementState> States
|
||||
{
|
||||
get
|
||||
|
|
@ -153,25 +156,30 @@ namespace Network.NetworkHost
|
|||
|
||||
lock (gate)
|
||||
{
|
||||
foreach (var state in statesByPeer.Values)
|
||||
accumulatedSimulationTime += elapsed;
|
||||
while (accumulatedSimulationTime >= configuration.SimulationInterval)
|
||||
{
|
||||
IntegrateState(state, elapsed);
|
||||
}
|
||||
|
||||
accumulatedBroadcastTime += elapsed;
|
||||
while (accumulatedBroadcastTime >= configuration.BroadcastInterval)
|
||||
{
|
||||
accumulatedBroadcastTime -= configuration.BroadcastInterval;
|
||||
pendingBroadcasts ??= new List<PendingBroadcast>();
|
||||
accumulatedSimulationTime -= configuration.SimulationInterval;
|
||||
foreach (var state in statesByPeer.Values)
|
||||
{
|
||||
state.LastBroadcastTick = nextBroadcastTick;
|
||||
pendingBroadcasts.Add(new PendingBroadcast(
|
||||
state.RemoteEndPoint,
|
||||
BuildPlayerState(state, nextBroadcastTick)));
|
||||
IntegrateState(state, configuration.SimulationInterval);
|
||||
}
|
||||
|
||||
nextBroadcastTick++;
|
||||
accumulatedBroadcastTime += configuration.SimulationInterval;
|
||||
while (accumulatedBroadcastTime >= configuration.BroadcastInterval)
|
||||
{
|
||||
accumulatedBroadcastTime -= configuration.BroadcastInterval;
|
||||
pendingBroadcasts ??= new List<PendingBroadcast>();
|
||||
foreach (var state in statesByPeer.Values)
|
||||
{
|
||||
state.LastBroadcastTick = nextBroadcastTick;
|
||||
pendingBroadcasts.Add(new PendingBroadcast(
|
||||
state.RemoteEndPoint,
|
||||
BuildPlayerState(state, nextBroadcastTick)));
|
||||
}
|
||||
|
||||
nextBroadcastTick++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -299,6 +307,7 @@ namespace Network.NetworkHost
|
|||
lock (gate)
|
||||
{
|
||||
statesByPeer.Clear();
|
||||
accumulatedSimulationTime = TimeSpan.Zero;
|
||||
accumulatedBroadcastTime = TimeSpan.Zero;
|
||||
}
|
||||
}
|
||||
|
|
@ -460,7 +469,8 @@ namespace Network.NetworkHost
|
|||
Z = state.VelocityZ
|
||||
},
|
||||
Rotation = state.Rotation,
|
||||
Hp = state.Hp
|
||||
Hp = state.Hp,
|
||||
AcknowledgedMoveTick = state.LastAcceptedMoveTick
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ namespace Network.NetworkHost
|
|||
|
||||
public MultiSessionManager SessionCoordinator { get; }
|
||||
|
||||
public TimeSpan AuthoritativeMovementCadence => authoritativeMovementCoordinator.SimulationInterval;
|
||||
|
||||
public IReadOnlyList<ManagedNetworkSession> ManagedSessions => SessionCoordinator.Sessions;
|
||||
|
||||
public IReadOnlyList<ServerAuthoritativeMovementState> AuthoritativeMovementStates => authoritativeMovementCoordinator.States;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ namespace Network.NetworkHost
|
|||
|
||||
public bool IsRunning { get; private set; }
|
||||
|
||||
public TimeSpan AuthoritativeMovementCadence => host.AuthoritativeMovementCadence;
|
||||
|
||||
public IReadOnlyList<ManagedNetworkSession> ManagedSessions => host.ManagedSessions;
|
||||
|
||||
public IReadOnlyList<ServerAuthoritativeMovementState> AuthoritativeMovementStates => host.AuthoritativeMovementStates;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Network.Defines;
|
||||
using Network.NetworkApplication;
|
||||
using UnityEngine;
|
||||
|
||||
public class Player : MonoBehaviour
|
||||
|
|
@ -15,7 +16,7 @@ public class Player : MonoBehaviour
|
|||
[SerializeField] private bool _isControlled;
|
||||
private readonly ClientAuthoritativePlayerState _authoritativeState = new();
|
||||
|
||||
public void LocalInit(string playerId, int speed, long serverTick)
|
||||
public void LocalInit(string playerId, ClientMovementBootstrap bootstrap)
|
||||
{
|
||||
this.PlayerId = playerId;
|
||||
this._isControlled = true;
|
||||
|
|
@ -24,7 +25,7 @@ public class Player : MonoBehaviour
|
|||
_meshRenderer.material = _materials[idx];
|
||||
|
||||
_playerUI.Init(this);
|
||||
_movement.Init(true, this, speed, serverTick);
|
||||
_movement.Init(true, this, bootstrap);
|
||||
}
|
||||
|
||||
public void RemoteInit(string playerId, UnityEngine.Vector3 pos)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Net;
|
||||
using System.Reflection;
|
||||
using Network.Defines;
|
||||
using Network.NetworkApplication;
|
||||
using NUnit.Framework;
|
||||
|
|
@ -125,6 +126,71 @@ namespace Tests.EditMode.Network
|
|||
Assert.That(combatPresentation.IsDead, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClientGameplayFlow_ControlledPlayerReconciliation_ReplacesActiveCorrectionForConsecutiveSmallSnapshots()
|
||||
{
|
||||
var gameObject = new GameObject("controlled-player");
|
||||
try
|
||||
{
|
||||
var rigidbody = gameObject.AddComponent<Rigidbody>();
|
||||
rigidbody.useGravity = false;
|
||||
var movement = gameObject.AddComponent<MovementComponent>();
|
||||
typeof(MovementComponent)
|
||||
.GetField("_rigid", BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
.SetValue(movement, rigidbody);
|
||||
movement.Init(true, master: null, speed: 10, serverTick: 0);
|
||||
|
||||
movement.OnAuthoritativeState(new ClientAuthoritativePlayerStateSnapshot(
|
||||
GameplayFlowTestSupport.CreatePlayerState("player-1", 1, new Vector3(0.75f, 0f, 0f), acknowledgedMoveTick: 0)));
|
||||
InvokeControlledFixedUpdate(movement);
|
||||
Assert.That(rigidbody.position.x, Is.EqualTo(0.5f).Within(0.0001f));
|
||||
|
||||
movement.OnAuthoritativeState(new ClientAuthoritativePlayerStateSnapshot(
|
||||
GameplayFlowTestSupport.CreatePlayerState("player-1", 2, new Vector3(1f, 0f, 0f), acknowledgedMoveTick: 0)));
|
||||
InvokeControlledFixedUpdate(movement);
|
||||
Assert.That(rigidbody.position.x, Is.EqualTo(1f).Within(0.0001f));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClientGameplayFlow_ControlledPlayerReconciliation_EscalatesToSnapAfterFailedConvergence()
|
||||
{
|
||||
var gameObject = new GameObject("controlled-player");
|
||||
try
|
||||
{
|
||||
var rigidbody = gameObject.AddComponent<Rigidbody>();
|
||||
rigidbody.useGravity = false;
|
||||
var movement = gameObject.AddComponent<MovementComponent>();
|
||||
typeof(MovementComponent)
|
||||
.GetField("_rigid", BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
.SetValue(movement, rigidbody);
|
||||
movement.Init(true, master: null, speed: 10, serverTick: 0);
|
||||
|
||||
movement.OnAuthoritativeState(new ClientAuthoritativePlayerStateSnapshot(
|
||||
GameplayFlowTestSupport.CreatePlayerState("player-1", 1, new Vector3(0.75f, 0f, 0f), acknowledgedMoveTick: 0)));
|
||||
InvokeControlledFixedUpdate(movement);
|
||||
Assert.That(rigidbody.position.x, Is.EqualTo(0.5f).Within(0.0001f));
|
||||
|
||||
movement.OnAuthoritativeState(new ClientAuthoritativePlayerStateSnapshot(
|
||||
GameplayFlowTestSupport.CreatePlayerState("player-1", 2, new Vector3(1.25f, 0f, 0f), acknowledgedMoveTick: 0)));
|
||||
InvokeControlledFixedUpdate(movement);
|
||||
Assert.That(rigidbody.position.x, Is.EqualTo(1f).Within(0.0001f));
|
||||
|
||||
movement.OnAuthoritativeState(new ClientAuthoritativePlayerStateSnapshot(
|
||||
GameplayFlowTestSupport.CreatePlayerState("player-1", 3, new Vector3(1.75f, 0f, 0f), acknowledgedMoveTick: 0)));
|
||||
InvokeControlledFixedUpdate(movement);
|
||||
Assert.That(rigidbody.position.x, Is.EqualTo(1.75f).Within(0.0001f));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClientGameplayHarness_RemotePlayerStateFlow_RejectsStaleSnapshots_AndUsesInterpolationOrLatestClamp()
|
||||
{
|
||||
|
|
@ -157,5 +223,11 @@ namespace Tests.EditMode.Network
|
|||
Assert.That(clamped.LatestSnapshot.Tick, Is.EqualTo(11));
|
||||
Assert.That(clamped.Position, Is.EqualTo(new Vector3(10f, 0f, 0f)));
|
||||
}
|
||||
private static void InvokeControlledFixedUpdate(MovementComponent movement)
|
||||
{
|
||||
typeof(MovementComponent)
|
||||
.GetMethod("FixedUpdate", BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
.Invoke(movement, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ namespace Tests.EditMode.Network
|
|||
TransferBroadcastMessages(serverTransports[9001], clientSyncTransport, ServerSender);
|
||||
clientRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||
|
||||
Assert.That(serverRuntime.AuthoritativeMovementCadence, Is.EqualTo(TimeSpan.FromMilliseconds(50)));
|
||||
Assert.That(serverRuntime.TryGetAuthoritativeMovementState(ClientPeer, out var localServerState), Is.True);
|
||||
Assert.That(localServerState.PlayerId, Is.EqualTo("player-a"));
|
||||
Assert.That(localServerState.PositionX, Is.EqualTo(0.5f).Within(0.0001f));
|
||||
|
|
@ -105,6 +106,7 @@ namespace Tests.EditMode.Network
|
|||
|
||||
Assert.That(clientHarness.TryGetState("player-a", out var localClientState), Is.True);
|
||||
Assert.That(localClientState.Tick, Is.EqualTo(1));
|
||||
Assert.That(localClientState.AcknowledgedMoveTick, Is.EqualTo(1));
|
||||
Assert.That(localClientState.Position.x, Is.EqualTo(0.5f).Within(0.0001f));
|
||||
Assert.That(clientHarness.TryGetState("player-b", out var remoteClientState), Is.True);
|
||||
Assert.That(remoteClientState.Hp, Is.EqualTo(70));
|
||||
|
|
@ -177,6 +179,7 @@ namespace Tests.EditMode.Network
|
|||
|
||||
Assert.That(clientHarness.TryGetState("player-a", out var idleLocalState), Is.True);
|
||||
Assert.That(idleLocalState.Tick, Is.EqualTo(1));
|
||||
Assert.That(idleLocalState.AcknowledgedMoveTick, Is.EqualTo(0));
|
||||
Assert.That(idleLocalState.Position.x, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(idleLocalState.Position.z, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(idleLocalState.Hp, Is.EqualTo(100));
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ namespace Tests.EditMode.Network
|
|||
}.ToByteArray();
|
||||
}
|
||||
|
||||
public static PlayerState CreatePlayerState(string playerId, long tick, Vector3 position, int hp = 100, float rotation = 0f, Vector3? velocity = null)
|
||||
public static PlayerState CreatePlayerState(string playerId, long tick, Vector3 position, int hp = 100, float rotation = 0f, Vector3? velocity = null, long acknowledgedMoveTick = 0)
|
||||
{
|
||||
var resolvedVelocity = velocity ?? Vector3.zero;
|
||||
return new PlayerState
|
||||
|
|
@ -223,7 +223,8 @@ namespace Tests.EditMode.Network
|
|||
Position = new global::Network.Defines.Vector3 { X = position.x, Y = position.y, Z = position.z },
|
||||
Velocity = new global::Network.Defines.Vector3 { X = resolvedVelocity.x, Y = resolvedVelocity.y, Z = resolvedVelocity.z },
|
||||
Rotation = rotation,
|
||||
Hp = hp
|
||||
Hp = hp,
|
||||
AcknowledgedMoveTick = acknowledgedMoveTick
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,55 @@ namespace Tests.EditMode.Network
|
|||
private static readonly IPEndPoint PeerA = new(IPAddress.Loopback, 9101);
|
||||
private static readonly IPEndPoint PeerB = new(IPAddress.Loopback, 9102);
|
||||
|
||||
[Test]
|
||||
public void UpdateAuthoritativeMovement_UsesConfiguredSimulationCadence_AndExposesItOnRuntime()
|
||||
{
|
||||
var createdTransports = new Dictionary<int, FakeTransport>();
|
||||
var configuration = new ServerRuntimeConfiguration(9000)
|
||||
{
|
||||
Dispatcher = new MainThreadNetworkDispatcher(),
|
||||
TransportFactory = port => CreateTransport(createdTransports, port),
|
||||
AuthoritativeMovement = new ServerAuthoritativeMovementConfiguration
|
||||
{
|
||||
MoveSpeed = 4f,
|
||||
SimulationInterval = TimeSpan.FromMilliseconds(50),
|
||||
BroadcastInterval = TimeSpan.FromMilliseconds(100)
|
||||
}
|
||||
};
|
||||
|
||||
using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
|
||||
|
||||
runtime.Host.NotifyLoginStarted(PeerA);
|
||||
runtime.Host.NotifyLoginSucceeded(PeerA, "player-a");
|
||||
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput
|
||||
{
|
||||
PlayerId = "player-a",
|
||||
Tick = 1,
|
||||
TurnInput = 0f,
|
||||
ThrottleInput = 1f
|
||||
}), PeerA);
|
||||
|
||||
runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||
runtime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(49));
|
||||
|
||||
Assert.That(runtime.AuthoritativeMovementCadence, Is.EqualTo(TimeSpan.FromMilliseconds(50)));
|
||||
Assert.That(runtime.TryGetAuthoritativeMovementState(PeerA, out var stateBeforeCadence), Is.True);
|
||||
Assert.That(stateBeforeCadence.PositionX, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(createdTransports[9000].BroadcastMessages.Count, Is.EqualTo(0));
|
||||
|
||||
runtime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(1));
|
||||
|
||||
Assert.That(runtime.TryGetAuthoritativeMovementState(PeerA, out var stateAfterFirstStep), Is.True);
|
||||
Assert.That(stateAfterFirstStep.PositionX, Is.EqualTo(0.2f).Within(0.0001f));
|
||||
Assert.That(createdTransports[9000].BroadcastMessages.Count, Is.EqualTo(0));
|
||||
|
||||
runtime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50));
|
||||
|
||||
Assert.That(runtime.TryGetAuthoritativeMovementState(PeerA, out var stateAfterSecondStep), Is.True);
|
||||
Assert.That(stateAfterSecondStep.PositionX, Is.EqualTo(0.4f).Within(0.0001f));
|
||||
Assert.That(createdTransports[9000].BroadcastMessages.Count, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UpdateAuthoritativeMovement_AcceptsLatestTickPerPeer_AndKeepsStaleFilteringIndependent()
|
||||
{
|
||||
|
|
@ -118,6 +167,7 @@ namespace Tests.EditMode.Network
|
|||
var firstBroadcast = ParsePlayerState(createdTransports[9001].BroadcastMessages[0]);
|
||||
Assert.That(firstBroadcast.PlayerId, Is.EqualTo("player-a"));
|
||||
Assert.That(firstBroadcast.Tick, Is.EqualTo(1));
|
||||
Assert.That(firstBroadcast.AcknowledgedMoveTick, Is.EqualTo(1));
|
||||
Assert.That(firstBroadcast.Position.X, Is.EqualTo(1f).Within(0.0001f));
|
||||
Assert.That(firstBroadcast.Velocity.X, Is.EqualTo(10f).Within(0.0001f));
|
||||
Assert.That(firstBroadcast.Velocity.Z, Is.EqualTo(0f).Within(0.0001f));
|
||||
|
|
@ -141,6 +191,7 @@ namespace Tests.EditMode.Network
|
|||
|
||||
var secondBroadcast = ParsePlayerState(createdTransports[9001].BroadcastMessages[1]);
|
||||
Assert.That(secondBroadcast.Tick, Is.EqualTo(2));
|
||||
Assert.That(secondBroadcast.AcknowledgedMoveTick, Is.EqualTo(2));
|
||||
Assert.That(secondBroadcast.Position.X, Is.EqualTo(1f).Within(0.0001f));
|
||||
Assert.That(secondBroadcast.Velocity.X, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(secondBroadcast.Velocity.Z, Is.EqualTo(0f).Within(0.0001f));
|
||||
|
|
@ -186,6 +237,7 @@ namespace Tests.EditMode.Network
|
|||
var broadcast = ParsePlayerState(createdTransports[9001].BroadcastMessages[0]);
|
||||
Assert.That(broadcast.PlayerId, Is.EqualTo("player-a"));
|
||||
Assert.That(broadcast.Tick, Is.EqualTo(1));
|
||||
Assert.That(broadcast.AcknowledgedMoveTick, Is.EqualTo(0));
|
||||
Assert.That(broadcast.Position.X, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(broadcast.Position.Z, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(broadcast.Velocity.X, Is.EqualTo(0f).Within(0.0001f));
|
||||
|
|
@ -228,6 +280,7 @@ namespace Tests.EditMode.Network
|
|||
|
||||
var broadcast = ParsePlayerState(createdTransports[9000].BroadcastMessages[0]);
|
||||
Assert.That(broadcast.Tick, Is.EqualTo(1));
|
||||
Assert.That(broadcast.AcknowledgedMoveTick, Is.EqualTo(5));
|
||||
Assert.That(broadcast.Position.X, Is.EqualTo(-0.3f).Within(0.0001f));
|
||||
Assert.That(broadcast.Position.Z, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(broadcast.Velocity.X, Is.EqualTo(-6f).Within(0.0001f));
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using Google.Protobuf;
|
||||
using Network.Defines;
|
||||
using Network.NetworkApplication;
|
||||
|
|
@ -90,11 +91,12 @@ namespace Tests.EditMode.Network
|
|||
buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 12, ThrottleInput = 1f });
|
||||
|
||||
var accepted = buffer.TryApplyAuthoritativeState(
|
||||
new PlayerState { PlayerId = "player-1", Tick = 11 },
|
||||
new PlayerState { PlayerId = "player-1", Tick = 11, AcknowledgedMoveTick = 11 },
|
||||
out var replayInputs);
|
||||
|
||||
Assert.That(accepted, Is.True);
|
||||
Assert.That(buffer.LastAuthoritativeTick, Is.EqualTo(11));
|
||||
Assert.That(buffer.LastAcknowledgedMoveTick, Is.EqualTo(11));
|
||||
Assert.That(replayInputs.Count, Is.EqualTo(1));
|
||||
Assert.That(replayInputs[0].Input.Tick, Is.EqualTo(12));
|
||||
Assert.That(replayInputs[0].SimulatedDurationSeconds, Is.EqualTo(0f));
|
||||
|
|
@ -106,15 +108,121 @@ namespace Tests.EditMode.Network
|
|||
{
|
||||
var buffer = new ClientPredictionBuffer();
|
||||
buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 10, ThrottleInput = 1f });
|
||||
buffer.TryApplyAuthoritativeState(new PlayerState { PlayerId = "player-1", Tick = 10 }, out _);
|
||||
buffer.TryApplyAuthoritativeState(new PlayerState { PlayerId = "player-1", Tick = 10, AcknowledgedMoveTick = 10 }, out _);
|
||||
|
||||
var accepted = buffer.TryApplyAuthoritativeState(
|
||||
new PlayerState { PlayerId = "player-1", Tick = 9 },
|
||||
new PlayerState { PlayerId = "player-1", Tick = 9, AcknowledgedMoveTick = 9 },
|
||||
out var replayInputs);
|
||||
|
||||
Assert.That(accepted, Is.False);
|
||||
Assert.That(replayInputs, Is.Empty);
|
||||
Assert.That(buffer.LastAuthoritativeTick, Is.EqualTo(10));
|
||||
Assert.That(buffer.LastAcknowledgedMoveTick, Is.EqualTo(10));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClientMovementBootstrap_LoginResponse_UsesServerConfirmedMovementParameters()
|
||||
{
|
||||
var bootstrap = ClientMovementBootstrap.FromLoginResponse(new LoginResponse
|
||||
{
|
||||
Speed = 12,
|
||||
ServerTick = 34
|
||||
});
|
||||
|
||||
Assert.That(bootstrap.AuthoritativeMoveSpeed, Is.EqualTo(12));
|
||||
Assert.That(bootstrap.ServerTick, Is.EqualTo(34));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ControlledPlayerCorrection_SmallError_UsesBoundedCorrection()
|
||||
{
|
||||
var result = ControlledPlayerCorrection.Resolve(
|
||||
Vector3.zero,
|
||||
Quaternion.identity,
|
||||
new Vector3(0.75f, 0f, 0f),
|
||||
Quaternion.Euler(0f, 15f, 0f),
|
||||
new ControlledPlayerCorrectionSettings(0.05f, 10f, 180f));
|
||||
|
||||
Assert.That(result.UsedHardSnap, Is.False);
|
||||
Assert.That(result.Position, Is.EqualTo(new Vector3(0.5f, 0f, 0f)));
|
||||
Assert.That(result.Rotation.eulerAngles.y, Is.EqualTo(9f).Within(0.01f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ControlledPlayerCorrection_LargeError_UsesHardSnap()
|
||||
{
|
||||
var targetRotation = Quaternion.Euler(0f, 40f, 0f);
|
||||
var result = ControlledPlayerCorrection.Resolve(
|
||||
Vector3.zero,
|
||||
Quaternion.identity,
|
||||
new Vector3(2f, 0f, 0f),
|
||||
targetRotation,
|
||||
new ControlledPlayerCorrectionSettings(0.05f, 10f, 180f));
|
||||
|
||||
Assert.That(result.UsedHardSnap, Is.True);
|
||||
Assert.That(result.Position, Is.EqualTo(new Vector3(2f, 0f, 0f)));
|
||||
Assert.That(result.Rotation.eulerAngles.y, Is.EqualTo(targetRotation.eulerAngles.y).Within(0.01f));
|
||||
Assert.That(result.NextState.IsActive, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ControlledPlayerCorrection_RepeatedSmallCorrections_UpdateActiveCorrectionState()
|
||||
{
|
||||
var settings = new ControlledPlayerCorrectionSettings(0.05f, 10f, 180f);
|
||||
var first = ControlledPlayerCorrection.Resolve(
|
||||
Vector3.zero,
|
||||
Quaternion.identity,
|
||||
new Vector3(0.75f, 0f, 0f),
|
||||
Quaternion.identity,
|
||||
settings);
|
||||
var second = ControlledPlayerCorrection.Resolve(
|
||||
first.Position,
|
||||
first.Rotation,
|
||||
new Vector3(1f, 0f, 0f),
|
||||
Quaternion.identity,
|
||||
settings,
|
||||
first.NextState);
|
||||
|
||||
Assert.That(first.UsedHardSnap, Is.False);
|
||||
Assert.That(first.NextState.IsActive, Is.True);
|
||||
Assert.That(first.NextState.RemainingStepBudget, Is.EqualTo(2));
|
||||
Assert.That(second.UsedHardSnap, Is.False);
|
||||
Assert.That(second.Position.x, Is.EqualTo(1f).Within(0.0001f));
|
||||
Assert.That(second.NextState.IsActive, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ControlledPlayerCorrection_RepeatedNonConvergentSmallCorrections_EventuallyUseHardSnap()
|
||||
{
|
||||
var settings = new ControlledPlayerCorrectionSettings(0.05f, 10f, 180f);
|
||||
var first = ControlledPlayerCorrection.Resolve(
|
||||
Vector3.zero,
|
||||
Quaternion.identity,
|
||||
new Vector3(0.75f, 0f, 0f),
|
||||
Quaternion.identity,
|
||||
settings);
|
||||
var second = ControlledPlayerCorrection.Resolve(
|
||||
first.Position,
|
||||
first.Rotation,
|
||||
new Vector3(1.25f, 0f, 0f),
|
||||
Quaternion.identity,
|
||||
settings,
|
||||
first.NextState);
|
||||
var third = ControlledPlayerCorrection.Resolve(
|
||||
second.Position,
|
||||
second.Rotation,
|
||||
new Vector3(1.75f, 0f, 0f),
|
||||
Quaternion.identity,
|
||||
settings,
|
||||
second.NextState);
|
||||
|
||||
Assert.That(first.UsedHardSnap, Is.False);
|
||||
Assert.That(second.UsedHardSnap, Is.False);
|
||||
Assert.That(second.NextState.IsActive, Is.True);
|
||||
Assert.That(second.NextState.RemainingStepBudget, Is.EqualTo(1));
|
||||
Assert.That(third.UsedHardSnap, Is.True);
|
||||
Assert.That(third.Position.x, Is.EqualTo(1.75f).Within(0.0001f));
|
||||
Assert.That(third.NextState.IsActive, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
@ -129,7 +237,8 @@ namespace Tests.EditMode.Network
|
|||
Position = new global::Network.Defines.Vector3 { X = 5f, Y = 0f, Z = -3f },
|
||||
Velocity = new global::Network.Defines.Vector3 { X = 1.5f, Y = 0f, Z = 0.25f },
|
||||
Rotation = 90f,
|
||||
Hp = 73
|
||||
Hp = 73,
|
||||
AcknowledgedMoveTick = 9
|
||||
},
|
||||
out var snapshot);
|
||||
|
||||
|
|
@ -137,6 +246,7 @@ namespace Tests.EditMode.Network
|
|||
Assert.That(owner.Current, Is.SameAs(snapshot));
|
||||
Assert.That(snapshot.PlayerId, Is.EqualTo("player-1"));
|
||||
Assert.That(snapshot.Tick, Is.EqualTo(14));
|
||||
Assert.That(snapshot.AcknowledgedMoveTick, Is.EqualTo(9));
|
||||
Assert.That(snapshot.Position, Is.EqualTo(new Vector3(5f, 0f, -3f)));
|
||||
Assert.That(snapshot.Velocity, Is.EqualTo(new Vector3(1.5f, 0f, 0.25f)));
|
||||
Assert.That(snapshot.Rotation, Is.EqualTo(90f));
|
||||
|
|
@ -360,7 +470,7 @@ namespace Tests.EditMode.Network
|
|||
Assert.That(sample.HasValue, Is.True);
|
||||
Assert.That(sample.UsedInterpolation, Is.False);
|
||||
Assert.That(sample.Position, Is.EqualTo(new Vector3(2f, 0f, -1f)));
|
||||
Assert.That(sample.Rotation.eulerAngles.y, Is.EqualTo(15f).Within(0.01f));
|
||||
Assert.That(sample.Rotation.eulerAngles.y, Is.EqualTo(75f).Within(0.01f));
|
||||
Assert.That(sample.LatestSnapshot.Tick, Is.EqualTo(12));
|
||||
}
|
||||
|
||||
|
|
@ -392,6 +502,172 @@ namespace Tests.EditMode.Network
|
|||
Assert.That(runtime.ClockSync.CurrentServerTick, Is.EqualTo(88));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ReplayPendingInputs_StepByStepMatchesAccumulated_ForZeroTurnInput()
|
||||
{
|
||||
// Arrange: set up MovementComponent with initial state.
|
||||
var gameObject = new GameObject("replay-test");
|
||||
try
|
||||
{
|
||||
var rigidbody = gameObject.AddComponent<Rigidbody>();
|
||||
rigidbody.useGravity = false;
|
||||
var movement = gameObject.AddComponent<MovementComponent>();
|
||||
typeof(MovementComponent)
|
||||
.GetField("_rigid", BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
.SetValue(movement, rigidbody);
|
||||
movement.Init(true, master: null, speed: 10, serverTick: 0);
|
||||
|
||||
ResetMovementState(rigidbody, Vector3.zero, Quaternion.identity);
|
||||
|
||||
var turnInput = 0f;
|
||||
var throttleInput = 1f;
|
||||
var stepDuration = 0.05f;
|
||||
var totalDuration = stepDuration * 3; // 0.15s
|
||||
|
||||
// Act — step-by-step path (live prediction shape).
|
||||
ApplyTankMovementStepByStep(movement, turnInput, throttleInput, stepDuration, steps: 3);
|
||||
var stepByStepPosition = rigidbody.position;
|
||||
var stepByStepRotation = rigidbody.rotation;
|
||||
|
||||
// Reset to initial state.
|
||||
ResetMovementState(rigidbody, Vector3.zero, Quaternion.identity);
|
||||
|
||||
// Act — accumulated replay shape.
|
||||
var accumulatedReplayInputs = new List<PredictedMoveStep>
|
||||
{
|
||||
new PredictedMoveStep(
|
||||
new MoveInput { PlayerId = "player-1", Tick = 1, TurnInput = turnInput, ThrottleInput = throttleInput },
|
||||
totalDuration)
|
||||
};
|
||||
InvokeReplayPendingInputs(movement, accumulatedReplayInputs);
|
||||
var accumulatedPosition = rigidbody.position;
|
||||
var accumulatedRotation = rigidbody.rotation;
|
||||
|
||||
// Assert: for straight movement (turn=0), both paths should be identical.
|
||||
Assert.That(Vector3.Distance(accumulatedPosition, stepByStepPosition), Is.LessThan(0.0001f),
|
||||
"Accumulated replay produced a different position than step-by-step for straight movement.");
|
||||
Assert.That(Quaternion.Angle(accumulatedRotation, stepByStepRotation), Is.LessThan(0.01f),
|
||||
"Accumulated replay produced a different rotation than step-by-step for straight movement.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ReplayPendingInputs_StepByStepDiffersFromAccumulated_ForNonZeroTurnInput()
|
||||
{
|
||||
// Arrange: use many small steps and a large turn input to make non-linearity visible.
|
||||
var gameObject = new GameObject("replay-test");
|
||||
try
|
||||
{
|
||||
var rigidbody = gameObject.AddComponent<Rigidbody>();
|
||||
rigidbody.useGravity = false;
|
||||
var movement = gameObject.AddComponent<MovementComponent>();
|
||||
typeof(MovementComponent)
|
||||
.GetField("_rigid", BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
.SetValue(movement, rigidbody);
|
||||
movement.Init(true, master: null, speed: 10, serverTick: 0);
|
||||
|
||||
ResetMovementState(rigidbody, Vector3.zero, Quaternion.identity);
|
||||
|
||||
// Use 20 substeps of 0.05s (1 second total) with full turn.
|
||||
// This amplifies the non-linearity so the one-shot and step-by-step diverge.
|
||||
var turnInput = 1f;
|
||||
var throttleInput = 1f;
|
||||
var stepDuration = 0.05f;
|
||||
var steps = 20;
|
||||
var totalDuration = stepDuration * steps;
|
||||
|
||||
// Act — step-by-step (correct approach).
|
||||
ApplyTankMovementStepByStep(movement, turnInput, throttleInput, stepDuration, steps);
|
||||
var stepByStepPosition = rigidbody.position;
|
||||
|
||||
// Reset.
|
||||
ResetMovementState(rigidbody, Vector3.zero, Quaternion.identity);
|
||||
|
||||
// Act — ONE big step simulating the old buggy accumulated behavior.
|
||||
ApplyTankMovementStepByStep(movement, turnInput, throttleInput, totalDuration, steps: 1);
|
||||
var oneShotPosition = rigidbody.position;
|
||||
|
||||
// Assert: for non-zero turn with many steps, the old one-shot and correct step-by-step MUST differ.
|
||||
Assert.That(Vector3.Distance(oneShotPosition, stepByStepPosition), Is.GreaterThan(0.001f),
|
||||
"One-shot accumulated and step-by-step produced the same result for turn input — " +
|
||||
"the non-linearity should cause a visible divergence with many steps.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ReplayPendingInputs_NonMultipleOfCadence_HandlesRemainingDuration()
|
||||
{
|
||||
// Arrange: simulate 0.12s — not a multiple of 50ms.
|
||||
var gameObject = new GameObject("replay-test");
|
||||
try
|
||||
{
|
||||
var rigidbody = gameObject.AddComponent<Rigidbody>();
|
||||
rigidbody.useGravity = false;
|
||||
var movement = gameObject.AddComponent<MovementComponent>();
|
||||
typeof(MovementComponent)
|
||||
.GetField("_rigid", BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
.SetValue(movement, rigidbody);
|
||||
movement.Init(true, master: null, speed: 10, serverTick: 0);
|
||||
|
||||
ResetMovementState(rigidbody, Vector3.zero, Quaternion.identity);
|
||||
|
||||
var turnInput = 0f;
|
||||
var throttleInput = 1f;
|
||||
var totalDuration = 0.12f; // 0.05 + 0.05 + 0.02
|
||||
|
||||
var replayInputs = new List<PredictedMoveStep>
|
||||
{
|
||||
new PredictedMoveStep(
|
||||
new MoveInput { PlayerId = "player-1", Tick = 1, TurnInput = turnInput, ThrottleInput = throttleInput },
|
||||
totalDuration)
|
||||
};
|
||||
InvokeReplayPendingInputs(movement, replayInputs);
|
||||
var finalPosition = rigidbody.position;
|
||||
|
||||
// Expected: 0.12s at speed=10 → 1.2 units forward.
|
||||
var expectedPosition = new Vector3(0f, 0f, 1.2f);
|
||||
Assert.That(finalPosition.z, Is.EqualTo(expectedPosition.z).Within(0.0001f),
|
||||
"Non-multiple of cadence (0.12s) had remaining duration lost or misapplied.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyTankMovementStepByStep(MovementComponent movement, float turnInput, float throttleInput, float stepDuration, int steps)
|
||||
{
|
||||
var method = typeof(MovementComponent)
|
||||
.GetMethod("ApplyTankMovement", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
for (var i = 0; i < steps; i++)
|
||||
{
|
||||
method.Invoke(movement, new object[] { turnInput, throttleInput, stepDuration });
|
||||
}
|
||||
}
|
||||
|
||||
private static void ResetMovementState(Rigidbody rigidbody, Vector3 position, Quaternion rotation)
|
||||
{
|
||||
rigidbody.position = position;
|
||||
rigidbody.rotation = rotation;
|
||||
rigidbody.velocity = Vector3.zero;
|
||||
rigidbody.angularVelocity = Vector3.zero;
|
||||
}
|
||||
|
||||
private static void InvokeReplayPendingInputs(MovementComponent movement, IReadOnlyList<PredictedMoveStep> inputs)
|
||||
{
|
||||
typeof(MovementComponent)
|
||||
.GetMethod("ReplayPendingInputs", BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
.Invoke(movement, new object[] { inputs });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ServerNetworkHost_RejectsStaleMoveInputPerPeerWithoutCrossPeerInterference()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a Unity MOBA game with a custom RUDP (Reliable UDP) networking layer built on KCP. The architecture follows a **server-authoritative** hybrid sync model:
|
||||
|
||||
- **Client**: captures input, sends `MoveInput`/`ShootInput`, predicts local movement, interpolates remote players
|
||||
- **Server**: owns gameplay truth (position, HP, combat resolution), broadcasts authoritative `PlayerState`/`CombatEvent`
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Build test assemblies
|
||||
dotnet build Network.EditMode.Tests.csproj -v minimal
|
||||
|
||||
# Run regression suite
|
||||
dotnet test Network.EditMode.Tests.csproj --no-build -v minimal
|
||||
```
|
||||
|
||||
Set `DOTNET_CLI_HOME=.dotnet-home` and `DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1` if needed.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Dual-Lane Transport
|
||||
|
||||
The transport layer uses two distinct lanes with different delivery semantics:
|
||||
|
||||
| Lane | Policy | Messages |
|
||||
|------|--------|----------|
|
||||
| **Sync lane** (`HighFrequencySync`) | Latest-wins, stale-drop | `MoveInput`, `PlayerState` |
|
||||
| **Reliable lane** (`ReliableOrdered`) | Ordered, guaranteed delivery | `ShootInput`, `CombatEvent`, login/heartbeat |
|
||||
|
||||
Never mix messages with different delivery requirements into the same lane.
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
Assets/Scripts/Network/
|
||||
├── Defines/ # MessageType enum, protobuf message definitions
|
||||
├── NetworkTransport/ # KcpTransport, ReliableUdpTransport, ITransport
|
||||
├── NetworkApplication/# MessageManager, SessionManager, DeliveryPolicy, dispatchers
|
||||
└── NetworkHost/ # ServerNetworkHost, ServerAuthoritativeMovementCoordinator, ServerRuntimeHandle
|
||||
|
||||
Assets/Scripts/Extensions/ # Unity-specific helpers (protobuf-to-Unity conversions)
|
||||
Assets/Tests/EditMode/Network/ # NUnit edit-mode regression tests
|
||||
openspec/ # Specs and change artifacts
|
||||
```
|
||||
|
||||
### Key Types
|
||||
|
||||
- `MessageManager`: routes all gameplay messages through `Envelope`, maps `MessageType` → `DeliveryPolicy`
|
||||
- `ServerNetworkHost`: server lifecycle, session state, hosts authoritative coordinators
|
||||
- `ServerAuthoritativeMovementCoordinator`: server-side movement validation and state broadcast
|
||||
- `ServerAuthoritativeCombatCoordinator`: server-side combat resolution
|
||||
- `SyncSequenceTracker`: stale-packet filtering for sync lane (keyed by `playerId + tick`)
|
||||
- `ClientPredictionBuffer`: stores pending inputs for local player prediction/replay
|
||||
- `RemotePlayerSnapshotInterpolator`: buffers remote `PlayerState` for smooth interpolation
|
||||
|
||||
### Client Prediction Flow
|
||||
|
||||
1. Client sends `MoveInput` on sync lane
|
||||
2. Client immediately applies predicted movement locally
|
||||
3. Server receives, validates, updates authoritative state
|
||||
4. Server broadcasts `PlayerState` on sync lane
|
||||
5. Client compares authoritative state against predicted; corrects if divergence exceeds threshold
|
||||
|
||||
### OpenSpec Workflow
|
||||
|
||||
Use `openspec` commands for substantial changes:
|
||||
- `openspec status --change "<name>"` — check progress
|
||||
- `openspec instructions apply --change "<name>" --json` — read current tasks before editing
|
||||
|
||||
## Code Rules
|
||||
|
||||
- **No Unity dependencies in `Assets/Scripts/Network/`** — shared networking must remain engine-agnostic
|
||||
- **Server owns gameplay truth** — clients submit intent only, never finalize position/HP/combat
|
||||
- **Tick required on all gameplay messages** — enables stale filtering and reconciliation
|
||||
- **4-space indentation, `PascalCase` public APIs, `_camelCase` private fields**
|
||||
- **Add NUnit tests** for any network-layer behavior change; use explicit regression-style names
|
||||
213
TODO.md
213
TODO.md
|
|
@ -1,200 +1,35 @@
|
|||
# Network MVP TODO
|
||||
## Follow-up: Local Controlled-Player Jitter
|
||||
|
||||
## Goal
|
||||
Current assessment:
|
||||
|
||||
Make the current project actually satisfy the MVP described in [MobaSyncMVP.md](./MobaSyncMVP.md):
|
||||
- Loopback repro means transport delay is not the primary cause of the remaining local-player jitter.
|
||||
- The next round should focus on deterministic prediction/reconciliation timing before adding more local smoothing.
|
||||
|
||||
- Client sends only `MoveInput` and `ShootInput`
|
||||
- Server owns gameplay truth for position, HP, combat resolution, and validation
|
||||
- Server sends authoritative `PlayerState` and `CombatEvent`
|
||||
- Client predicts only local movement
|
||||
- Client reconciles local state and interpolates remote state for presentation
|
||||
Step-by-step plan:
|
||||
|
||||
## Current Audit Summary
|
||||
1. Align replay integration granularity with live prediction
|
||||
- Replace one-shot replay of an accumulated input duration with fixed substeps.
|
||||
- Ensure replay uses the same movement integration shape as the normal `FixedUpdate` prediction path, especially for turn-and-move input.
|
||||
|
||||
Already in place:
|
||||
2. Align client prediction cadence with server authoritative cadence
|
||||
- Introduce an explicit local prediction/replay cadence derived from the authoritative movement cadence.
|
||||
- Avoid mixing client-side `Time.fixedDeltaTime` prediction with server-side fixed-cadence authoritative integration in reconciliation-sensitive paths.
|
||||
|
||||
- [x] `MoveInput` / `ShootInput` / `CombatEvent` protocol split is done
|
||||
- [x] Delivery policy mapping is aligned with sync lane vs reliable lane
|
||||
- [x] High-frequency stale filtering is limited to `MoveInput` and `PlayerState`
|
||||
- [x] Client prediction buffer is narrowed to movement
|
||||
- [x] Dual-transport runtime wiring exists in the shared network layer
|
||||
- [x] Network-layer regression tests exist for routing and stale filtering
|
||||
3. Stabilize or remove send-rate oscillation driven by server tick offset
|
||||
- Revisit `MovementComponent.SetServerTick(...)` and stop toggling `_sendInterval` directly between nearby values when the offset crosses zero.
|
||||
- If clock correction is still needed, add hysteresis or filtering so the send cadence does not bounce frame-to-frame.
|
||||
|
||||
Still missing for MVP:
|
||||
4. Re-measure controlled-player correction after timing fixes
|
||||
- Keep remote-player interpolation as-is; do not treat local-player jitter as a remote interpolation problem.
|
||||
- Only refine local visual correction further if meaningful residual error remains after steps 1-3.
|
||||
|
||||
- [ ] Client-side `ShootInput` send path
|
||||
- [ ] Client-side `CombatEvent` receive/apply path
|
||||
- [x] Server startup path that actually uses `ServerNetworkHost`
|
||||
- [x] Server-authoritative movement/state loop
|
||||
- [x] Server-authoritative shooting/combat resolution loop
|
||||
- [ ] Full `PlayerState` field application for rotation / HP / velocity
|
||||
- [ ] Remote-player snapshot buffering and interpolation strategy
|
||||
- [x] Explicit movement-stop handling via zero-input `MoveInput`
|
||||
- [x] End-to-end gameplay regression coverage
|
||||
- [x] Re-run build/test in an environment with the required .NET runtime installed
|
||||
|
||||
## Checklist
|
||||
|
||||
### 1. Keep The Shared Networking Foundation Stable
|
||||
|
||||
- [x] Keep `MoveInput`, `ShootInput`, `PlayerState`, and `CombatEvent` as the MVP gameplay messages
|
||||
- [x] Keep `MoveInput` and `PlayerState` on `HighFrequencySync`
|
||||
- [x] Keep `ShootInput` and `CombatEvent` on `ReliableOrdered`
|
||||
- [x] Keep stale-drop logic only for `MoveInput` and `PlayerState`
|
||||
- [x] Keep client prediction buffering limited to `MoveInput`
|
||||
- [x] Keep dual-transport runtime construction in [`Assets/Scripts/Network/NetworkApplication/NetworkIntegrationFactory.cs`](./Assets/Scripts/Network/NetworkApplication/NetworkIntegrationFactory.cs)
|
||||
5. Add regression coverage and diagnostics for the remaining jitter path
|
||||
- Add tests that compare live prediction and replayed prediction under the same turn/throttle sequence.
|
||||
- Add tests for server tick offset calibration so small offset sign changes do not continuously retarget send cadence.
|
||||
- Add or expose diagnostics for acknowledged move tick, predicted pose, authoritative pose, and correction magnitude per snapshot.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- [x] Network-layer message routing still matches the MVP transport mapping
|
||||
- [x] Sequence filtering still matches the MVP tick rules
|
||||
- [x] Shared runtime and host still support separate reliable and sync transports
|
||||
|
||||
### 2. Align Client Input Flow With MVP
|
||||
|
||||
- [x] Update [`Assets/Scripts/MovementComponent.cs`](./Assets/Scripts/MovementComponent.cs) so movement intent can send an explicit zero-vector stop message when the player releases input
|
||||
- [x] Keep local prediction immediate for the controlled player
|
||||
- [x] Add a client shooting input capture path
|
||||
- [x] Add `NetworkManager.SendShootInput(...)`
|
||||
- [x] Ensure the client sends only `MoveInput` and `ShootInput` for gameplay actions
|
||||
- [x] Keep local shooting presentation optional and purely cosmetic
|
||||
|
||||
Acceptance:
|
||||
|
||||
- [x] Releasing movement input produces a final `MoveInput` that stops authoritative movement
|
||||
- [x] Firing produces a `ShootInput` sent on the reliable lane
|
||||
- [x] No MVP gameplay action depends on legacy broad messages such as `PlayerAction`
|
||||
|
||||
### 3. Apply Full Authoritative `PlayerState` On The Client
|
||||
|
||||
- [x] Extend the player-side presentation model to consume authoritative `position`, `rotation`, `hp`, and optional `velocity`
|
||||
- [x] Keep local-player reconciliation driven by authoritative `PlayerState.Tick`
|
||||
- [x] Use authoritative HP instead of any local guesswork
|
||||
- [x] Decide where authoritative player state lives on the client side and keep that ownership explicit
|
||||
- [x] Update UI or diagnostics so authoritative HP/state changes are observable during development
|
||||
|
||||
Acceptance:
|
||||
|
||||
- [x] Local player corrects to server truth for position and rotation
|
||||
- [x] Local and remote players expose authoritative HP from `PlayerState`
|
||||
- [x] The client does not finalize gameplay truth outside authoritative messages
|
||||
|
||||
### 4. Replace Ad-Hoc Remote Movement Smoothing With Snapshot Interpolation
|
||||
|
||||
- [x] Add a small `PlayerState` snapshot buffer for remote players
|
||||
- [x] Interpolate between buffered snapshots instead of lerping directly to the latest state
|
||||
- [x] Discard stale snapshots by tick
|
||||
- [x] Keep remote players non-predicted
|
||||
- [x] Document the interpolation delay / sample strategy in code comments or docs if it is non-obvious
|
||||
|
||||
Acceptance:
|
||||
|
||||
- [x] Remote movement is based on buffered authoritative snapshots
|
||||
- [x] Out-of-order remote `PlayerState` packets do not corrupt presentation
|
||||
- [x] Remote players are smoothed without becoming locally authoritative
|
||||
|
||||
### 5. Add Client-Side `CombatEvent` Handling
|
||||
|
||||
- [x] Register a `CombatEvent` handler in [`Assets/Scripts/NetworkManager.cs`](./Assets/Scripts/NetworkManager.cs)
|
||||
- [x] Route combat results to the relevant player or combat presentation components
|
||||
- [x] Apply hit / damage / death / shoot-rejected results from server truth
|
||||
- [x] Keep local fire FX separate from authoritative damage and death resolution
|
||||
- [x] Add UI or debug output for combat-result visibility during MVP development
|
||||
|
||||
Acceptance:
|
||||
|
||||
- [x] `CombatEvent` updates HP, death state, or hit feedback on clients
|
||||
- [x] `ShootRejected` can be surfaced without client-side authoritative rollback logic
|
||||
- [x] Combat results are driven by server messages, not speculative client outcomes
|
||||
|
||||
### 6. Add A Real Server Startup / Integration Entry Point
|
||||
|
||||
- [x] Add or update the runtime server bootstrap so production code actually constructs [`ServerNetworkHost`](./Assets/Scripts/Network/NetworkHost/ServerNetworkHost.cs) via [`ServerRuntimeEntryPoint`](./Assets/Scripts/Network/NetworkHost/ServerRuntimeEntryPoint.cs)
|
||||
- [x] Start both reliable and sync transports from the server integration layer
|
||||
- [x] Drain server pending messages on a regular loop through [`ServerRuntimeHandle`](./Assets/Scripts/Network/NetworkHost/ServerRuntimeHandle.cs)
|
||||
- [x] Preserve server lifecycle diagnostics and visibility through the existing `ServerNetworkHost` lifecycle surface and metrics hooks
|
||||
- [x] Make the startup path easy to locate and test
|
||||
|
||||
Acceptance:
|
||||
|
||||
- [x] There is a concrete server startup path in production code, not only shared infrastructure and tests
|
||||
- [x] Server runtime uses two distinct transport instances when sync port is configured
|
||||
- [x] Server can receive gameplay traffic on both lanes
|
||||
|
||||
### 7. Implement Server-Authoritative Movement And State Broadcast
|
||||
|
||||
- [x] Register `MoveInput` handling on the server
|
||||
- [x] Maintain authoritative per-player movement state on the server
|
||||
- [x] Validate and apply move input before mutating authoritative state
|
||||
- [x] Use tick-aware stale filtering per peer without cross-peer interference
|
||||
- [x] Broadcast authoritative `PlayerState` snapshots on the sync lane at a fixed cadence
|
||||
- [x] Ensure zero-vector movement input stops authoritative movement
|
||||
|
||||
Acceptance:
|
||||
|
||||
- [x] Server owns final position and movement resolution
|
||||
- [x] Clients receive authoritative `PlayerState` snapshots for reconciliation/interpolation
|
||||
- [x] Movement stop is reflected by server-authoritative state, not just local client visuals
|
||||
|
||||
### 8. Implement Server-Authoritative Shooting And Combat Resolution
|
||||
|
||||
- [x] Register `ShootInput` handling on the server
|
||||
- [x] Validate shoot requests before accepting them
|
||||
- [x] Resolve hit, damage, death, and rejection on the server
|
||||
- [x] Broadcast `CombatEvent` on the reliable lane
|
||||
- [x] Reflect authoritative HP changes in subsequent `PlayerState` snapshots
|
||||
- [x] Keep server combat resolution independent from cosmetic client preplay
|
||||
|
||||
Acceptance:
|
||||
|
||||
- [x] Server decides whether shooting is valid
|
||||
- [x] Server emits authoritative `CombatEvent` for damage/death/rejection
|
||||
- [x] Clients update combat state from server truth
|
||||
|
||||
### 9. Expand Regression Coverage From Network Layer To Gameplay Flow
|
||||
|
||||
- [x] Extend [`Assets/Tests/EditMode/Network/MessageManagerTests.cs`](./Assets/Tests/EditMode/Network/MessageManagerTests.cs) only as needed for lane policy regressions
|
||||
- [x] Add tests that cover explicit zero-input movement stop behavior
|
||||
- [x] Add tests for client `ShootInput` send routing
|
||||
- [x] Add tests for `CombatEvent` receive/apply behavior
|
||||
- [x] Add tests for remote `PlayerState` buffering / interpolation decisions where practical
|
||||
- [x] Add tests for server-authoritative movement processing
|
||||
- [x] Add tests for server-authoritative shooting/combat result generation
|
||||
- [x] Add at least one end-to-end fake-transport test that covers `MoveInput -> PlayerState` and `ShootInput -> CombatEvent`
|
||||
|
||||
Acceptance:
|
||||
|
||||
- [x] MVP gameplay flow is covered beyond transport-only assertions
|
||||
- [x] Both client single-session and server multi-session behaviors remain protected
|
||||
- [x] Regression tests fail if movement/combat authority accidentally drifts back to the client
|
||||
|
||||
### 10. Re-Verify Build And Test
|
||||
|
||||
- [x] Install or use an environment that contains the required .NET runtime for this repository
|
||||
- [x] Run `dotnet build Network.EditMode.Tests.csproj -v minimal`
|
||||
- [x] Run `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal`
|
||||
- [x] Record the actual result after the environment issue is resolved
|
||||
|
||||
Acceptance:
|
||||
|
||||
- [x] Build succeeds in a runnable local environment
|
||||
- [x] Edit-mode network tests succeed
|
||||
- [x] New MVP gameplay regression tests succeed
|
||||
|
||||
Recorded result:
|
||||
|
||||
- [x] Verified on 2026-03-29 in a local environment with .NET SDK 10.0.201 installed
|
||||
- [x] `dotnet build Network.EditMode.Tests.csproj -v minimal` succeeded with 4 non-fatal MSB3277 warning groups about `System.Net.Http` and `System.Security.Cryptography.Algorithms` assembly-version conflicts in Unity dependencies
|
||||
- [x] `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal` succeeded, covering the edit-mode network and MVP gameplay regression suite
|
||||
|
||||
## Recommended Order
|
||||
|
||||
1. Keep the shared networking foundation unchanged
|
||||
2. Fix client input flow, especially stop movement and `ShootInput`
|
||||
3. Add real server startup and authoritative movement/state broadcast
|
||||
4. Add authoritative shooting/combat resolution and `CombatEvent`
|
||||
5. Apply full authoritative state and combat results on the client
|
||||
6. Upgrade remote interpolation from direct lerp to snapshot buffering
|
||||
7. Add gameplay-flow regression tests
|
||||
8. Re-run build and test once the .NET runtime issue is resolved
|
||||
- Controlled-player loopback movement no longer shows repeated small pull-back under steady turn-and-move input.
|
||||
- Replay after authoritative reconciliation produces the same trajectory shape as forward local prediction for the same input sequence.
|
||||
- Small server tick offset fluctuations do not cause visible local cadence oscillation.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-04-05
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
## Context
|
||||
|
||||
当前客户端本地预测和服务端权威同步存在两个 P0 级错位。第一,客户端把 `PlayerState.Tick` 当成“服务端已确认到哪个 `MoveInput.Tick`”来修剪 prediction buffer,但服务端实际把它作为广播快照序号生成,导致客户端错误移除或错误重放输入。第二,客户端移动速度来源于 UI / 登录返回链路,服务端权威速度来源于 `ServerAuthoritativeMovementConfiguration`,两边没有统一的真相来源。
|
||||
|
||||
这次改动跨越 protobuf 契约、共享网络同步语义、客户端本地预测初始化、服务端登录后移动 bootstrap,以及回归测试,因此需要显式设计文档先固定决策。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 明确分离权威快照 tick 和已确认移动输入 tick。
|
||||
- 让客户端 reconciliation 只依赖显式 ack move tick。
|
||||
- 建立服务器确认的移动参数启动流程,让客户端预测参数与服务端权威参数共享同一真相来源。
|
||||
- 用回归测试保护上述行为,避免再次回到“一个字段承载两种语义”的状态。
|
||||
|
||||
**Non-Goals:**
|
||||
- 不在本次 P0 中解决本地 controlled player 的视觉平滑策略。
|
||||
- 不在本次 P0 中重构完整的服务端 movement cadence 管理,只要求现有语义能正确对账。
|
||||
- 不扩展新的移动玩法或额外状态字段,除非它们是承载 ack tick / 权威参数所必需的最小改动。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. `PlayerState.Tick` 保留为权威快照序号,新增显式 `AcknowledgedMoveTick`
|
||||
|
||||
`PlayerState.Tick` 现在已经被客户端和服务端用作“最新快照 / 最新同步状态”的序号。直接把它改成 ack tick 会让 remote interpolation、stale rejection、日志语义全部混乱。更稳妥的做法是保留 `PlayerState.Tick` 作为快照序号,并新增 `AcknowledgedMoveTick` 用于本地 controlled player reconciliation。
|
||||
|
||||
备选方案:
|
||||
- 把 `PlayerState.Tick` 改成 ack tick,并额外引入 snapshot tick。这个方案会让已有 stale rejection 和 remote snapshot buffer 全部迁移到新字段,破坏面更大。
|
||||
- 继续复用单字段并靠注释区分。这个方案无法阻止后续实现再次误用,直接排除。
|
||||
|
||||
### 2. 服务器在生成每个 `PlayerState` 时回填该玩家最后接受的 `MoveInput.Tick`
|
||||
|
||||
ack tick 属于每个玩家独立的服务器权威状态,而不是整个广播循环共享状态。服务端应当从玩家的权威移动状态中读取 `LastAcceptedMoveTick`,并在构造该玩家 `PlayerState` 时填入 `AcknowledgedMoveTick`。这样客户端拿到同一条快照时,既能用 `Tick` 做 stale rejection / 插值排序,也能用 `AcknowledgedMoveTick` 做 prediction buffer 修剪。
|
||||
|
||||
备选方案:
|
||||
- 发送独立 ack 消息。这样会增加协议复杂度和时序耦合,本次只需要在已有 `PlayerState` 中补充字段即可满足需求。
|
||||
|
||||
### 3. 客户端本地预测参数必须在登录成功后切换到服务器确认值
|
||||
|
||||
本地预测只要继续使用 UI 本地速度,而服务端继续使用配置速度,就算 ack tick 语义正确也会不断被回拉。P0 需要把移动参数所有权收回到服务器,客户端只允许在登录前临时持有候选值,登录成功后必须切换到服务器确认的移动参数后再继续长期预测。
|
||||
|
||||
备选方案:
|
||||
- 彻底删除客户端可配置速度。这个方向可行,但会扩大 MVP 工作面;当前先保留输入渠道,只是不再允许它绕过服务器确认值成为长期预测真相。
|
||||
- 仅在代码里假定两边常量相等。这个方案没有可验证的契约,不能接受。
|
||||
|
||||
### 4. 回归测试以“语义分离”而不是“视觉无抖动”作为 P0 验收目标
|
||||
|
||||
P0 的核心是让协议与 reconciliation 语义正确。测试应精确验证:
|
||||
- 服务器广播 tick 增长时,ack tick 仍保持玩家最后确认输入值。
|
||||
- 客户端 reconciliation 只修剪 `<= AcknowledgedMoveTick` 的输入。
|
||||
- 客户端在登录成功后使用服务器确认的移动参数建立或刷新本地预测配置。
|
||||
|
||||
视觉平滑与误差阈值可以在后续 P2 再处理,不应混入本次验收。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [风险] protobuf 字段变更会影响生成代码与现有消息构造路径 -> 缓解:把改动限制在 `PlayerState` 最小增量字段,并补齐协议层回归测试。
|
||||
- [风险] 客户端仍可能在登录前使用本地默认速度开始预测 -> 缓解:在 bootstrap 规范中明确“长期预测必须切换到服务器确认参数”,实现阶段为未确认参数增加显式初始化路径。
|
||||
- [风险] 某些现有测试默认把 `PlayerState.Tick` 视作 ack tick -> 缓解:把这些测试改成分别断言 snapshot tick 与 `AcknowledgedMoveTick`。
|
||||
- [风险] 只修复语义不修复 cadence 后,极端情况下仍会看到少量位置纠正 -> 缓解:将 cadence 统一保留在后续 P1,并确保本次不会再由错账语义导致持续拉扯。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 更新 OpenSpec 契约与任务清单,固定 `PlayerState` 双 tick 语义与权威参数 bootstrap 规则。
|
||||
2. 实现协议字段与服务端状态生成逻辑,确保服务器能填充 `AcknowledgedMoveTick`。
|
||||
3. 更新客户端 reconciliation 与本地预测初始化逻辑。
|
||||
4. 补齐回归测试后运行 `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal`。
|
||||
5. 如果发现客户端初始化阶段需要兼容旧字段,使用默认值路径临时兜底,但不改变最终权威参数所有权。
|
||||
|
||||
## Open Questions
|
||||
|
||||
- 服务器确认的移动参数是否只需要 `MoveSpeed`,还是要同时把旋转速度也纳入同一 bootstrap 契约。如果现有客户端和服务端旋转速度并非单一常量来源,实现时应一并纳入。
|
||||
- 登录成功消息是否已经稳定承载移动参数;如果没有,是否需要通过初始 `PlayerState` 或其他启动消息承载该参数。该问题在实现前需要结合现有消息结构做最小改动决策。
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
## Why
|
||||
|
||||
客户端当前把 `PlayerState.Tick` 同时当作权威快照序号和已确认输入 tick 使用,而服务端广播的 `PlayerState.Tick` 实际上代表广播序号。这会让本地 prediction buffer 错误修剪与重放,持续制造可见抖动。与此同时,客户端本地预测速度与服务端权威速度来自不同来源,导致即使 tick 语义修正后,客户端轨迹仍可能系统性偏离。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 为权威移动同步引入明确的已确认输入 tick 语义,禁止继续复用 `PlayerState.Tick` 同时表达广播序号和输入确认序号。
|
||||
- 调整 `PlayerState` 消息契约,使权威快照同时携带快照 tick 与已确认移动输入 tick。
|
||||
- 定义客户端权威移动参数启动能力,要求客户端本地预测使用服务器确认的移动参数,而不是独立 UI 本地值。
|
||||
- 更新客户端 reconciliation 规则,使 prediction buffer 只按已确认移动输入 tick 修剪。
|
||||
- 补充编辑模式回归覆盖,保护 ack tick / broadcast tick 分离以及权威移动参数启动流程。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `authoritative-movement-bootstrap`: 定义客户端在开始本地预测前如何接收并应用服务器确认的权威移动参数。
|
||||
|
||||
### Modified Capabilities
|
||||
- `client-authoritative-player-state`: 本地 reconciliation 从按 `PlayerState.Tick` 对账改为按显式 ack move tick 对账。
|
||||
- `network-gameplay-message-types`: `PlayerState` 消息契约新增显式已确认移动输入 tick 字段。
|
||||
- `network-sync-strategy`: prediction history 修剪规则从快照 tick 改为 ack move tick。
|
||||
- `server-authoritative-movement`: 服务器广播的权威状态同时暴露快照序号与最后确认的移动输入 tick。
|
||||
- `gameplay-flow-regression-coverage`: 回归测试新增 ack/broadcast tick 分离与权威移动参数启动覆盖。
|
||||
|
||||
## Impact
|
||||
|
||||
- 影响共享协议与生成消息代码,包括 `PlayerState` 的字段契约。
|
||||
- 影响客户端 `MovementComponent`、`ClientPredictionBuffer` 与本地预测初始化流程。
|
||||
- 影响服务端权威移动协调器、登录成功后的移动参数建立、以及权威状态广播逻辑。
|
||||
- 影响 edit-mode 网络回归测试与假传输端到端测试。
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Client prediction bootstraps from server-confirmed movement parameters
|
||||
The client SHALL establish controlled-player prediction parameters from server-confirmed authoritative movement settings before treating local prediction as steady-state truth. Client-local candidate values MAY exist before login succeeds, but long-lived prediction MUST switch to the server-confirmed parameters for the controlled player.
|
||||
|
||||
#### Scenario: Login success provides authoritative movement parameters for prediction
|
||||
- **WHEN** the controlled client completes login and receives the server-confirmed movement bootstrap data
|
||||
- **THEN** the client stores the authoritative movement parameters for that controlled player
|
||||
- **THEN** subsequent local movement prediction uses those server-confirmed parameters instead of continuing to rely on an unrelated local UI value
|
||||
|
||||
### Requirement: Server-owned movement parameters remain the single gameplay authority
|
||||
The server SHALL keep authoritative ownership of movement tuning used for authoritative movement resolution, and any client-visible movement parameters used for prediction MUST be derived from that server-owned configuration rather than from an independent client-only truth source.
|
||||
|
||||
#### Scenario: Client candidate speed does not override server movement authority
|
||||
- **WHEN** a client proposes or locally configures a movement speed that differs from the server-owned movement speed
|
||||
- **THEN** the server-owned movement configuration remains authoritative for gameplay resolution
|
||||
- **THEN** the client's steady-state prediction parameters converge to the server-confirmed value instead of preserving the divergent local candidate
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Local player reconciliation applies the full authoritative state by tick
|
||||
The controlled client SHALL continue reconciling local prediction from authoritative `PlayerState` updates, but it MUST distinguish between the authoritative snapshot tick and the acknowledged movement-input tick carried by that snapshot. Reconciliation MUST use the acknowledged movement-input tick to prune and replay predicted movement, while continuing to apply the accepted authoritative `position` and `rotation` and keeping authoritative HP and optional velocity synchronized with the owned player-state snapshot.
|
||||
|
||||
#### Scenario: Local authoritative state corrects predicted presentation
|
||||
- **WHEN** the controlled player accepts an authoritative `PlayerState` snapshot with snapshot tick `S` and acknowledged movement-input tick `N`
|
||||
- **THEN** local reconciliation prunes or replays predicted movement using acknowledged tick `N` according to the sync strategy
|
||||
- **THEN** stale rejection or snapshot ordering for authoritative state continues to use snapshot tick `S`
|
||||
- **THEN** the local player's visible transform is corrected toward authoritative `position` and `rotation`
|
||||
- **THEN** the local player's authoritative HP on the client matches the accepted `PlayerState`
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Gameplay-flow regressions include a fake-transport authoritative round trip
|
||||
The edit-mode regression suite SHALL include at least one deterministic fake-transport test that spans client send behavior, server-authoritative processing, and outgoing authoritative results. That round-trip regression MUST cover `MoveInput -> PlayerState` and `ShootInput -> CombatEvent` within the same MVP gameplay-flow suite. Movement round-trip coverage MUST also prove that authoritative `PlayerState` snapshots preserve a distinct snapshot tick and acknowledged movement-input tick, and that controlled-client prediction bootstraps from server-confirmed movement parameters rather than a divergent local-only value.
|
||||
|
||||
#### Scenario: Fake-transport round trip preserves server authority across movement and combat
|
||||
- **WHEN** an edit-mode regression test drives gameplay input through fake client/server transports and advances the server authority loop
|
||||
- **THEN** the authoritative server path emits `PlayerState` snapshots in response to movement input
|
||||
- **THEN** the authoritative server path emits `CombatEvent` results in response to shooting input
|
||||
- **THEN** the movement assertions prove snapshot ordering and acknowledged-input reconciliation use distinct `PlayerState` fields
|
||||
- **THEN** the combined test protects both client single-session input flow and server multi-session authoritative behavior from regression
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Gameplay messages expose explicit MVP payload fields
|
||||
The shared networking contract SHALL define the MVP payload fields for gameplay messages explicitly in the source protobuf schema and generated C# messages. `MoveInput` MUST expose `playerId`, `tick`, `moveX`, and `moveY`; `ShootInput` MUST expose `playerId`, `tick`, `dirX`, `dirY`, and an optional `targetId`; `PlayerState` MUST expose `playerId`, `tick`, `acknowledgedMoveTick`, `position`, `rotation`, `hp`, and an optional `velocity`; `CombatEvent` MUST expose `tick`, `eventType`, `attackerId`, `targetId`, `damage`, and an optional `hitPosition`. The shared contract MUST also provide `CombatEventType` so combat results use explicit event categories rather than ad hoc integer payload conventions.
|
||||
|
||||
#### Scenario: Movement input carries explicit movement fields
|
||||
- **WHEN** client or server code constructs or parses `MoveInput`
|
||||
- **THEN** the message exposes `playerId`, `tick`, `moveX`, and `moveY`
|
||||
- **THEN** movement intent does not rely on an overloaded payload extension
|
||||
|
||||
#### Scenario: Shooting input carries explicit aim fields
|
||||
- **WHEN** client or server code constructs or parses `ShootInput`
|
||||
- **THEN** the message exposes `playerId`, `tick`, `dirX`, `dirY`, and `targetId`
|
||||
- **THEN** shooting direction and optional target selection are represented directly in the message contract
|
||||
|
||||
#### Scenario: Authoritative player state carries explicit gameplay state fields
|
||||
- **WHEN** client or server code constructs or parses `PlayerState`
|
||||
- **THEN** the message exposes `playerId`, `tick`, `acknowledgedMoveTick`, `position`, `rotation`, `hp`, and `velocity`
|
||||
- **THEN** snapshot ordering and acknowledged-input reconciliation are both expressed without ad hoc payload extensions or overloaded tick semantics
|
||||
|
||||
#### Scenario: Combat events carry explicit result fields and event categories
|
||||
- **WHEN** client or server code constructs or parses `CombatEvent`
|
||||
- **THEN** the message exposes `tick`, `eventType`, `attackerId`, `targetId`, `damage`, and `hitPosition`
|
||||
- **THEN** `CombatEventType` provides explicit combat-result categories for interpreting that event payload
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Authoritative correction prunes acknowledged prediction history
|
||||
The client sync strategy SHALL reconcile local prediction against authoritative player-state updates by pruning acknowledged movement inputs at or before the acknowledged movement-input tick carried by the authoritative snapshot and only reapplying newer pending `MoveInput` messages. The snapshot tick used for stale rejection or remote interpolation MUST NOT be reused as the local prediction-acknowledgement boundary.
|
||||
|
||||
#### Scenario: Reconciliation removes already acknowledged movement inputs
|
||||
- **WHEN** the client accepts an authoritative `PlayerState` update whose acknowledged movement-input tick is `N`
|
||||
- **THEN** locally buffered predicted `MoveInput` messages with tick less than or equal to `N` are removed from the replay buffer
|
||||
- **THEN** only `MoveInput` messages newer than `N` remain eligible for re-simulation
|
||||
- **THEN** the client does not infer the acknowledgement boundary solely from the snapshot tick
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Server broadcasts authoritative `PlayerState` snapshots on the sync cadence
|
||||
The shared server networking path SHALL emit authoritative `PlayerState` snapshots for managed peers at a fixed cadence using the existing sync-lane message contract. Each snapshot MUST be derived from the server-owned authoritative player state and include both the authoritative snapshot tick for client stale rejection or interpolation and the last acknowledged `MoveInput.Tick` for client reconciliation. Authoritative HP changes produced by server-side combat resolution MUST be reflected in later snapshots for the affected peer.
|
||||
|
||||
#### Scenario: Authority update step emits sync-lane player snapshots
|
||||
- **WHEN** the server reaches a configured authority broadcast cadence while one or more managed peers have authoritative player state
|
||||
- **THEN** it sends `PlayerState` snapshots using the sync-lane delivery policy when a distinct sync transport exists
|
||||
- **THEN** each snapshot includes the authoritative position, rotation, velocity, HP, snapshot tick, and acknowledged movement-input tick from server-owned state
|
||||
|
||||
#### Scenario: Combat-driven HP changes appear in later player snapshots
|
||||
- **WHEN** the server applies authoritative combat damage or death to a managed peer
|
||||
- **THEN** later `PlayerState` snapshots for that peer carry the updated authoritative HP value
|
||||
- **THEN** clients do not need to invent or persist a separate HP truth outside authoritative server snapshots
|
||||
|
||||
#### Scenario: Reliable transport remains fallback when no sync transport exists
|
||||
- **WHEN** the server broadcasts authoritative `PlayerState` snapshots without a dedicated sync transport
|
||||
- **THEN** the shared routing path still emits `PlayerState` through the existing fallback lane behavior
|
||||
- **THEN** the authoritative snapshot contract remains unchanged
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
## 1. Protocol And Spec Alignment
|
||||
|
||||
- [x] 1.1 Update the shared gameplay message schema and generated code so `PlayerState` carries an explicit acknowledged movement-input tick.
|
||||
- [x] 1.2 Align OpenSpec-linked message construction and parsing paths with the new `PlayerState` field semantics.
|
||||
- [x] 1.3 Define or wire the server-confirmed movement bootstrap data used by the controlled client after login succeeds.
|
||||
|
||||
## 2. Authoritative Movement Runtime
|
||||
|
||||
- [x] 2.1 Update the server authoritative movement state and broadcast builder so each `PlayerState` includes both snapshot tick and last accepted `MoveInput.Tick`.
|
||||
- [x] 2.2 Update client reconciliation and prediction-buffer pruning to use the acknowledged movement-input tick instead of `PlayerState.Tick`.
|
||||
- [x] 2.3 Switch controlled-client steady-state prediction parameters to the server-confirmed authoritative movement values.
|
||||
|
||||
## 3. Regression Coverage
|
||||
|
||||
- [x] 3.1 Add or update edit-mode tests that prove snapshot tick and acknowledged movement-input tick remain distinct in authoritative movement broadcasts.
|
||||
- [x] 3.2 Add or update client reconciliation tests so only inputs at or before the acknowledged tick are pruned.
|
||||
- [x] 3.3 Add or update gameplay-flow round-trip coverage for server-confirmed movement bootstrap and authoritative movement convergence.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-04-05
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
## Context
|
||||
|
||||
P0 separated snapshot tick from acknowledged movement-input tick and moved steady-state client prediction onto server-confirmed movement parameters. The remaining visible jitter comes from two implementation gaps: the server authoritative movement path still accepts arbitrary elapsed values from its outer loop, and the controlled client still rewrites rigidbody state immediately on every accepted authoritative snapshot. Those behaviors are individually acceptable for correctness, but together they amplify small cadence drift into visible pull-back.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Make authoritative movement cadence an explicit shared runtime contract instead of an accidental property of whichever loop calls into server movement.
|
||||
- Keep server authoritative movement deterministic enough that client prediction can be compared against a known cadence during debugging and regression tests.
|
||||
- Replace unconditional hard rewrites for small controlled-player divergence with a bounded correction path that preserves server authority while reducing visible jitter.
|
||||
- Preserve the existing remote-player interpolation path and the P0 snapshot-tick versus acknowledged-move-tick contract.
|
||||
|
||||
**Non-Goals:**
|
||||
- Do not redesign transport lanes, login flow, or session ownership.
|
||||
- Do not add a new prediction model or speculative physics stack beyond the existing movement inputs and authoritative snapshots.
|
||||
- Do not change remote-player interpolation into extrapolation.
|
||||
- Do not remove the ability to hard snap when local state diverges materially from authoritative truth.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision: Introduce a dedicated authoritative movement cadence contract
|
||||
The server runtime will expose a single configured movement step interval that drives authoritative movement simulation and state emission. The coordinator will consume that fixed cadence rather than arbitrary elapsed values supplied by callers.
|
||||
|
||||
Why:
|
||||
- A fixed cadence makes server movement behavior testable and comparable against client prediction.
|
||||
- It eliminates one major source of drift where identical movement constants still produce different trajectories because integration step sizes differ.
|
||||
|
||||
Alternative considered:
|
||||
- Keep variable elapsed integration and only document recommended server loop timing. Rejected because the problem is not documentation; it is the lack of an enforceable contract.
|
||||
|
||||
### Decision: Surface cadence diagnostics through existing movement/sync plumbing
|
||||
The runtime will expose enough information for logs, tests, and reconciliation code to know the authoritative movement cadence and the authoritative tick carried by snapshots.
|
||||
|
||||
Why:
|
||||
- Debugging convergence problems requires seeing both the snapshot identity and the cadence under which it was produced.
|
||||
- Tests need a stable way to assert cadence-aligned behavior without relying on wall-clock timing.
|
||||
|
||||
Alternative considered:
|
||||
- Keep cadence entirely internal to the server runtime. Rejected because that hides the exact value the client needs to compare against when diagnosing jitter.
|
||||
|
||||
### Decision: Apply bounded correction for small controlled-player error
|
||||
The controlled client will classify accepted authoritative corrections into two paths: small error remains server-authoritative but is corrected through bounded convergence over subsequent local updates; large error still snaps immediately.
|
||||
|
||||
Why:
|
||||
- Most visible jitter now comes from repeated tiny rewrites, not giant desync.
|
||||
- A bounded correction path reduces visual pull-back while preserving the ability to recover quickly from true divergence.
|
||||
|
||||
Alternative considered:
|
||||
- Continue snapping on every accepted state. Rejected because it preserves correctness but also preserves the visible jitter this change is meant to reduce.
|
||||
|
||||
### Decision: Keep remote-player presentation rules unchanged
|
||||
Remote players will continue using buffered interpolation/clamp over accepted authoritative snapshots. This change only modifies the local controlled-player reconciliation path.
|
||||
|
||||
Why:
|
||||
- Remote presentation already has a distinct smoothing strategy with a different trade-off surface.
|
||||
- Mixing the two concerns would broaden the change without addressing the current problem source.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Bounded local correction can hide a real divergence for too long] -> Keep a hard snap threshold and assert it in regression coverage.
|
||||
- [Fixed authoritative cadence may require updates to server runtime callers] -> Centralize cadence ownership in runtime configuration and keep the public integration seam narrow.
|
||||
- [Cadence diagnostics may be misread as a new network contract] -> Keep diagnostics descriptive and avoid introducing a second movement truth outside authoritative snapshots.
|
||||
- [Unity rigidbody timing may still differ from deterministic server stepping] -> Tie correction policy to authoritative cadence and document that the client still converges to server truth instead of matching every substep exactly.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Introduce the authoritative movement cadence configuration and route server movement stepping through it.
|
||||
2. Update controlled-player reconciliation to classify small versus large error using the cadence-aware correction policy.
|
||||
3. Extend edit-mode regression coverage for cadence-aligned convergence and snap fallback.
|
||||
4. Verify the sample still preserves server-authoritative outcomes and archive the change after tests pass.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Whether cadence diagnostics belong in an explicit debug/state object or can remain as properties on existing runtime helpers.
|
||||
- The exact positional/rotational error thresholds that should trigger hard snap versus bounded correction for the sample scene.
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
## Why
|
||||
|
||||
P0 fixed tick semantics and server-confirmed movement bootstrap, but the sample still allows client prediction and server authority to advance on different cadences. That keeps controlled-player reconciliation noisy even when both sides share the same movement constants, so the next change needs to make server movement cadence explicit and tighten the client correction path against that contract.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Define a dedicated authoritative movement cadence contract for the shared server runtime, including a fixed simulation/update interval and explicit diagnostics that let the client compare its local prediction step against server authority timing.
|
||||
- Tighten the existing server authoritative movement spec so movement simulation and `PlayerState` production are driven by the configured cadence instead of arbitrary caller-provided elapsed values.
|
||||
- Tighten the client sync strategy so controlled-player reconciliation distinguishes between small cadence-aligned error and large divergence, using bounded correction for the former and hard snap only for the latter.
|
||||
- Tighten the client authoritative player-state contract so local controlled-player presentation applies cadence-aware correction without changing remote interpolation rules.
|
||||
- Extend regression coverage to protect cadence alignment, bounded local correction, and large-error snap fallback.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `authoritative-movement-cadence`: Defines the shared contract for fixed authoritative movement cadence, cadence diagnostics, and server/client observability.
|
||||
|
||||
### Modified Capabilities
|
||||
- `server-authoritative-movement`: Require server-side movement simulation and snapshot production to follow the configured authoritative cadence.
|
||||
- `network-sync-strategy`: Require cadence-aware local reconciliation with bounded correction for small error and snap fallback for large divergence.
|
||||
- `client-authoritative-player-state`: Require the controlled-player presentation path to consume cadence-aware correction results without changing remote authoritative ownership.
|
||||
- `gameplay-flow-regression-coverage`: Require edit-mode regressions for cadence alignment and controlled-player correction behavior.
|
||||
|
||||
## Impact
|
||||
|
||||
Affected areas include `ServerRuntimeHandle`, `ServerAuthoritativeMovementCoordinator`, client movement/reconciliation code in `MovementComponent` and related sync helpers, and edit-mode network regression tests. No new transport or session-lifecycle capability is introduced, but movement diagnostics and correction policy become more explicit.
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Server authoritative movement uses a fixed cadence contract
|
||||
The shared server runtime SHALL define a fixed authoritative movement cadence for simulation and snapshot production. Authoritative movement updates MUST be stepped from that configured cadence instead of arbitrary caller-provided elapsed values.
|
||||
|
||||
#### Scenario: Runtime advances movement using configured cadence
|
||||
- **WHEN** the server runtime advances authoritative movement while one or more managed peers have movement state
|
||||
- **THEN** the authoritative movement coordinator steps simulation using the configured cadence interval
|
||||
- **THEN** the same cadence governs later authoritative `PlayerState` production for that runtime
|
||||
|
||||
### Requirement: Cadence information is observable for diagnostics and regression tests
|
||||
The shared runtime SHALL expose the active authoritative movement cadence through diagnostics or runtime state that tests and debugging tools can read without inspecting private loop internals.
|
||||
|
||||
#### Scenario: Tests can read active movement cadence
|
||||
- **WHEN** an edit-mode regression or debugging path inspects the server runtime after movement setup
|
||||
- **THEN** it can observe the authoritative movement cadence configured for that runtime
|
||||
- **THEN** the observed value matches the cadence used by authoritative movement stepping
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Local player reconciliation applies the full authoritative state by tick
|
||||
The controlled client SHALL continue reconciling local prediction from authoritative `PlayerState` snapshots while keeping authoritative HP and optional velocity synchronized with the owned player-state snapshot. Reconciliation MUST use the acknowledged movement-input tick defined by the sync strategy, and the visible controlled-player transform MUST apply cadence-aware bounded correction for small divergence while preserving immediate authoritative snap for large divergence.
|
||||
|
||||
#### Scenario: Local authoritative state corrects predicted presentation
|
||||
- **WHEN** the controlled player accepts an authoritative `PlayerState` whose acknowledged movement-input tick is `N`
|
||||
- **THEN** local reconciliation prunes or replays predicted movement using tick `N` according to the sync strategy
|
||||
- **THEN** the local player's visible transform converges toward authoritative `position` and `rotation` through cadence-aware correction when the remaining error is small
|
||||
- **THEN** the local player's authoritative HP on the client matches the accepted `PlayerState`
|
||||
|
||||
#### Scenario: Large local divergence bypasses bounded correction
|
||||
- **WHEN** the controlled player accepts an authoritative `PlayerState` and the remaining transform error exceeds the configured snap threshold
|
||||
- **THEN** the controlled player's visible transform snaps immediately to authoritative `position` and `rotation`
|
||||
- **THEN** later local prediction resumes from that authoritative baseline instead of continuing from stale local presentation
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Gameplay-flow regressions include a fake-transport authoritative round trip
|
||||
The edit-mode regression suite SHALL include at least one deterministic fake-transport test that spans client send behavior, server-authoritative processing, and outgoing authoritative results. That round-trip regression MUST cover `MoveInput -> PlayerState` and `ShootInput -> CombatEvent` within the same MVP gameplay-flow suite, and it MUST assert that authoritative movement stepping follows the configured cadence contract.
|
||||
|
||||
#### Scenario: Fake-transport round trip preserves server authority across movement and combat
|
||||
- **WHEN** an edit-mode regression test drives gameplay input through fake client/server transports and advances the server authority loop
|
||||
- **THEN** the authoritative server path emits `PlayerState` snapshots in response to movement input using the configured authoritative movement cadence
|
||||
- **THEN** the authoritative server path emits `CombatEvent` results in response to shooting input
|
||||
- **THEN** the combined test protects both client single-session input flow and server multi-session authoritative behavior from regression
|
||||
|
||||
### Requirement: Gameplay-flow regressions cover controlled-player correction decisions
|
||||
The edit-mode regression suite SHALL cover the controlled-player reconciliation path after authoritative movement replay, including bounded correction for small cadence-aligned error and hard snap fallback for large divergence.
|
||||
|
||||
#### Scenario: Controlled-player reconciliation uses bounded correction for small error
|
||||
- **WHEN** an edit-mode regression test applies an authoritative local `PlayerState` that leaves only small post-replay divergence
|
||||
- **THEN** the controlled-player path keeps authoritative ownership of the snapshot
|
||||
- **THEN** visible correction converges without an immediate hard snap on the acceptance frame
|
||||
|
||||
#### Scenario: Controlled-player reconciliation snaps on large divergence
|
||||
- **WHEN** an edit-mode regression test applies an authoritative local `PlayerState` that leaves divergence beyond the configured snap threshold
|
||||
- **THEN** the controlled-player path immediately applies the authoritative transform state
|
||||
- **THEN** later prediction resumes from that authoritative baseline
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Authoritative correction prunes acknowledged prediction history
|
||||
The client sync strategy SHALL reconcile local prediction against authoritative player-state updates by pruning acknowledged movement inputs at or before the authoritative acknowledged movement tick and only reapplying newer pending `MoveInput` messages. For the controlled player, reconciliation MUST classify authoritative error after replay into a bounded-correction path for small cadence-aligned divergence and an immediate snap path for large divergence.
|
||||
|
||||
#### Scenario: Reconciliation removes already acknowledged movement inputs
|
||||
- **WHEN** the client accepts an authoritative `PlayerState` update that acknowledges movement tick `N`
|
||||
- **THEN** locally buffered predicted `MoveInput` messages with tick less than or equal to `N` are removed from the replay buffer
|
||||
- **THEN** only `MoveInput` messages newer than `N` remain eligible for re-simulation
|
||||
|
||||
#### Scenario: Small post-replay error uses bounded correction
|
||||
- **WHEN** the controlled client finishes replay after accepting an authoritative `PlayerState` and the remaining position or rotation error stays within the configured bounded-correction threshold
|
||||
- **THEN** the client keeps authoritative ownership of the accepted snapshot
|
||||
- **THEN** local presentation converges through bounded correction instead of an immediate hard snap on that frame
|
||||
|
||||
#### Scenario: Large divergence snaps immediately
|
||||
- **WHEN** the controlled client finishes replay after accepting an authoritative `PlayerState` and the remaining error exceeds the configured snap threshold
|
||||
- **THEN** the client immediately applies the authoritative transform state
|
||||
- **THEN** later local prediction continues from that authoritative baseline
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Server owns authoritative movement resolution
|
||||
The shared server networking path SHALL own the final movement state for each managed peer, including position, rotation, velocity, and stop state. Zero-vector movement input MUST stop authoritative movement rather than leaving the peer in its previous moving state. The authoritative movement integrator MUST advance using the runtime's configured authoritative movement cadence so that movement resolution and later `PlayerState` snapshots are produced from the same server-side stepping contract.
|
||||
|
||||
#### Scenario: Non-zero input advances authoritative movement state
|
||||
- **WHEN** the server processes an accepted non-zero `MoveInput` for a managed peer during an authority update step
|
||||
- **THEN** the server updates that peer's authoritative position, rotation, and velocity from server-side movement resolution using the configured authoritative movement cadence
|
||||
- **THEN** the resulting state becomes the source of truth for later `PlayerState` broadcast
|
||||
|
||||
#### Scenario: Zero-vector input stops authoritative movement
|
||||
- **WHEN** the server processes an accepted zero-vector `MoveInput` for a managed peer
|
||||
- **THEN** the peer's authoritative velocity becomes zero
|
||||
- **THEN** subsequent authoritative state snapshots reflect that stopped state until a newer movement input is accepted
|
||||
|
||||
### Requirement: Server broadcasts authoritative `PlayerState` snapshots on the sync cadence
|
||||
The shared server networking path SHALL emit authoritative `PlayerState` snapshots for managed peers at a fixed cadence using the existing sync-lane message contract. Each snapshot MUST be derived from the server-owned authoritative player state and include the authoritative tick for client reconciliation and interpolation. Authoritative HP changes produced by server-side combat resolution MUST be reflected in later snapshots for the affected peer.
|
||||
|
||||
#### Scenario: Authority update step emits sync-lane player snapshots
|
||||
- **WHEN** the server reaches the configured authoritative movement cadence while one or more managed peers have authoritative player state
|
||||
- **THEN** it sends `PlayerState` snapshots using the sync-lane delivery policy when a distinct sync transport exists
|
||||
- **THEN** each snapshot includes the authoritative position, rotation, velocity, HP, and tick from server-owned state
|
||||
|
||||
#### Scenario: Combat-driven HP changes appear in later player snapshots
|
||||
- **WHEN** the server applies authoritative combat damage or death to a managed peer
|
||||
- **THEN** later `PlayerState` snapshots for that peer carry the updated authoritative HP value
|
||||
- **THEN** clients do not need to invent or persist a separate HP truth outside authoritative server snapshots
|
||||
|
||||
#### Scenario: Reliable transport remains fallback when no sync transport exists
|
||||
- **WHEN** the server broadcasts authoritative `PlayerState` snapshots without a dedicated sync transport
|
||||
- **THEN** the shared routing path still emits `PlayerState` through the existing fallback lane behavior
|
||||
- **THEN** the authoritative snapshot contract remains unchanged
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
## 1. Cadence Contract
|
||||
|
||||
- [x] 1.1 Introduce a fixed authoritative movement cadence configuration/runtime surface that server movement stepping can read and tests can observe.
|
||||
- [x] 1.2 Update the server authoritative movement loop and snapshot emission path to use the configured cadence instead of arbitrary elapsed input.
|
||||
|
||||
## 2. Controlled Reconciliation
|
||||
|
||||
- [x] 2.1 Refactor controlled-player reconciliation to distinguish bounded correction from hard snap after authoritative replay.
|
||||
- [x] 2.2 Wire cadence-aware correction thresholds through the local movement/sync path without changing remote-player interpolation rules.
|
||||
|
||||
## 3. Regression Coverage
|
||||
|
||||
- [x] 3.1 Add or update edit-mode tests that prove authoritative movement stepping and snapshot output follow the configured cadence.
|
||||
- [x] 3.2 Add or update edit-mode tests that prove controlled-player reconciliation uses bounded correction for small error and hard snap for large divergence.
|
||||
- [x] 3.3 Re-run OpenSpec status and confirm the change is apply-ready.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-04-05
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
## Context
|
||||
|
||||
P0 separated authoritative snapshot identity from movement-input acknowledgement, and P1 made server cadence explicit while introducing an initial bounded-correction path for controlled-player reconciliation. That leaves one remaining UX-focused gap: repeated small authoritative corrections can still look noisy because the client only decides "bounded correction or snap" at the moment a snapshot is accepted. The implementation does not yet define how bounded correction persists, gets replaced, or escalates when multiple authoritative snapshots arrive while the local player is still converging.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Define a stable controlled-player visual-correction policy that survives across multiple accepted authoritative snapshots.
|
||||
- Keep authoritative gameplay truth separate from temporary visual smoothing state so local presentation can converge without weakening server authority.
|
||||
- Specify when a new small correction replaces, merges with, or escalates an existing bounded correction.
|
||||
- Add regression requirements that prove repeated small corrections converge and large divergence still snaps immediately.
|
||||
|
||||
**Non-Goals:**
|
||||
- Do not change server-authoritative movement cadence, tick semantics, or movement parameter ownership.
|
||||
- Do not modify remote-player interpolation rules.
|
||||
- Do not introduce extrapolation, rollback beyond existing local replay, or a second gameplay-truth state on the client.
|
||||
- Do not turn bounded correction into an unbounded smoothing layer that can hide persistent divergence.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision: Represent local visual convergence as explicit correction state
|
||||
The controlled-player path will keep authoritative transform truth separate from a short-lived visual correction state that tracks the remaining offset being paid down after reconciliation.
|
||||
|
||||
Why:
|
||||
- Repeated small authoritative updates need continuity; otherwise each accepted snapshot effectively restarts smoothing from scratch.
|
||||
- An explicit correction state makes the contract testable and prevents presentation code from quietly mixing visual offset with authoritative gameplay truth.
|
||||
|
||||
Alternative considered:
|
||||
- Recompute a one-frame bounded correction on every accepted snapshot without storing correction state. Rejected because it does not define behavior across consecutive snapshots and tends to produce visible jitter under sustained updates.
|
||||
|
||||
### Decision: New authoritative snapshots update or replace active correction by policy
|
||||
When the controlled player already has active bounded correction and another authoritative snapshot arrives, the client will either fold the new residual error into the active correction, replace it with a fresher target, or escalate to hard snap when the combined error breaches the snap threshold.
|
||||
|
||||
Why:
|
||||
- The sample needs deterministic behavior when correction is still in flight and another snapshot arrives.
|
||||
- Replacement rules are necessary to keep the visual path responsive to newer authoritative truth without accumulating stale offsets forever.
|
||||
|
||||
Alternative considered:
|
||||
- Queue multiple corrections independently. Rejected because it increases latency and can create laggy visual tails after authority has already advanced.
|
||||
|
||||
### Decision: Bound correction by convergence budget, not by indefinite smoothing
|
||||
Bounded correction will have an explicit convergence budget derived from cadence-aware limits so the visual path either settles quickly or escalates to snap when authority keeps diverging.
|
||||
|
||||
Why:
|
||||
- P2 is intended to reduce visible twitching, not to hide desync.
|
||||
- A bounded budget preserves the principle that authoritative truth must win quickly under sustained mismatch.
|
||||
|
||||
Alternative considered:
|
||||
- Smooth indefinitely with a low-pass presentation filter. Rejected because it can mask real divergence and make the controlled player feel floaty.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Explicit correction state increases local presentation complexity] -> Keep the state narrow, owned by the controlled-player path only, and cover it with regression tests.
|
||||
- [Aggressive replacement rules can reintroduce visible twitching] -> Define deterministic merge/replace thresholds and verify them with multi-snapshot regressions.
|
||||
- [Overly permissive smoothing can hide divergence too long] -> Keep a hard snap threshold and convergence budget that force recovery to authoritative truth.
|
||||
- [Unity update timing can still expose frame-rate-specific artifacts] -> Express requirements in terms of convergence behavior and authoritative ownership rather than exact frame counts.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Extend the controlled-player reconciliation contract to expose explicit visual correction state and replacement rules.
|
||||
2. Update the local sync strategy requirements so consecutive authoritative snapshots interact predictably with bounded correction.
|
||||
3. Add regression coverage for repeated small corrections, correction replacement, and snap escalation.
|
||||
4. Implement and verify the new policy before archiving the change.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Whether the correction state should be fully encapsulated inside `MovementComponent` or extracted into a dedicated helper/state holder.
|
||||
- The exact convergence budget values that feel stable in the sample scene without making the controlled player feel detached from input.
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
## Why
|
||||
|
||||
P0 and P1 fixed the correctness side of controlled-player reconciliation: acknowledged movement tick is explicit, steady-state prediction uses server-confirmed movement parameters, and authoritative movement cadence is no longer accidental. The sample still has one remaining gap: the local controlled player can look busy or twitchy under repeated small corrections because the current bounded-correction path is only a first-pass clamp, not a fully specified visual convergence policy.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Tighten the controlled-player reconciliation requirements so small authoritative corrections accumulate through an explicit visual-correction state instead of repeatedly restarting ad hoc per accepted snapshot.
|
||||
- Require the local presentation path to separate authoritative gameplay truth from short-lived visual correction state, preserving hard snap only for material divergence.
|
||||
- Extend the sync-strategy contract so bounded correction defines convergence behavior across consecutive snapshots instead of only classifying a single snapshot as small or large error.
|
||||
- Extend regression coverage to prove multi-snapshot convergence, correction replacement rules, and hard-snap fallback still hold.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
<!-- None. -->
|
||||
|
||||
### Modified Capabilities
|
||||
- `client-authoritative-player-state`: Tighten the controlled-player presentation contract so authoritative truth and temporary visual correction state remain distinct during local convergence.
|
||||
- `network-sync-strategy`: Tighten local reconciliation so bounded correction has explicit replacement, convergence, and snap-escalation rules across consecutive authoritative snapshots.
|
||||
- `gameplay-flow-regression-coverage`: Require edit-mode regressions that cover multi-snapshot convergence and repeated local correction behavior for the controlled player.
|
||||
|
||||
## Impact
|
||||
|
||||
Affected areas include controlled-player reconciliation and presentation code in `MovementComponent` plus any helper types that own local correction state, along with edit-mode regression tests for sync strategy and gameplay-flow round trips. No transport, session-lifecycle, or server authoritative movement protocol changes are expected in this phase.
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Local player reconciliation applies the full authoritative state by tick
|
||||
The controlled client SHALL continue reconciling local prediction from authoritative `PlayerState` snapshots while keeping authoritative HP and optional velocity synchronized with the owned player-state snapshot. Reconciliation MUST use the acknowledged movement-input tick defined by the sync strategy, and the visible controlled-player transform MUST keep authoritative gameplay truth separate from short-lived visual correction state. Small divergence after replay MUST converge through explicit bounded correction state, while large divergence or failed convergence MUST still snap immediately to authoritative `position` and `rotation`.
|
||||
|
||||
#### Scenario: Local authoritative state corrects predicted presentation
|
||||
- **WHEN** the controlled player accepts an authoritative `PlayerState` whose acknowledged movement-input tick is `N`
|
||||
- **THEN** local reconciliation prunes or replays predicted movement using tick `N` according to the sync strategy
|
||||
- **THEN** the controlled player's authoritative gameplay state updates immediately to the accepted `position`, `rotation`, HP, and optional velocity
|
||||
- **THEN** the local player's visible transform may temporarily differ only through bounded visual correction state that converges back to the authoritative baseline
|
||||
|
||||
#### Scenario: Consecutive small corrections replace or fold into active visual correction
|
||||
- **WHEN** the controlled player accepts a newer authoritative `PlayerState` while a bounded visual correction is still active and the new residual error remains inside the configured bounded-correction limits
|
||||
- **THEN** the client updates the active visual correction state according to the sync strategy instead of preserving stale correction targets indefinitely
|
||||
- **THEN** the controlled player's authoritative gameplay state still reflects only the newest accepted `PlayerState`
|
||||
|
||||
#### Scenario: Large local divergence bypasses bounded correction
|
||||
- **WHEN** the controlled player accepts an authoritative `PlayerState` and the remaining transform error exceeds the configured snap threshold or the active bounded correction can no longer converge within its budget
|
||||
- **THEN** the controlled player's visible transform snaps immediately to authoritative `position` and `rotation`
|
||||
- **THEN** any temporary visual correction state is cleared before later local prediction resumes from that authoritative baseline
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Gameplay-flow regressions cover controlled-player correction decisions
|
||||
The edit-mode regression suite SHALL cover the controlled-player reconciliation path after authoritative movement replay, including bounded correction for small cadence-aligned error, correction replacement under consecutive authoritative snapshots, and hard snap fallback for large or non-convergent divergence.
|
||||
|
||||
#### Scenario: Controlled-player reconciliation uses bounded correction for small error
|
||||
- **WHEN** an edit-mode regression test applies an authoritative local `PlayerState` that leaves only small post-replay divergence
|
||||
- **THEN** the controlled-player path keeps authoritative ownership of the snapshot
|
||||
- **THEN** visible correction converges without an immediate hard snap on the acceptance frame
|
||||
|
||||
#### Scenario: Controlled-player reconciliation updates active correction on repeated small snapshots
|
||||
- **WHEN** an edit-mode regression test feeds multiple authoritative local `PlayerState` updates whose residual divergence remains inside bounded-correction limits while a prior correction is still active
|
||||
- **THEN** the controlled-player path replaces or folds the active correction according to the sync strategy
|
||||
- **THEN** the test proves the client does not accumulate multiple stale correction tails
|
||||
|
||||
#### Scenario: Controlled-player reconciliation snaps on large divergence
|
||||
- **WHEN** an edit-mode regression test applies an authoritative local `PlayerState` that leaves divergence beyond the configured snap threshold
|
||||
- **THEN** the controlled-player path immediately applies the authoritative transform state
|
||||
- **THEN** later prediction resumes from that authoritative baseline
|
||||
|
||||
#### Scenario: Controlled-player reconciliation snaps after failed convergence
|
||||
- **WHEN** an edit-mode regression test feeds consecutive authoritative local `PlayerState` updates that keep bounded correction from converging within the configured budget
|
||||
- **THEN** the controlled-player path escalates to a hard snap
|
||||
- **THEN** the active correction state is cleared before later local prediction continues
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Authoritative correction prunes acknowledged prediction history
|
||||
The client sync strategy SHALL reconcile local prediction against authoritative player-state updates by pruning acknowledged movement inputs at or before the authoritative acknowledged movement tick and only reapplying newer pending `MoveInput` messages. For the controlled player, reconciliation MUST classify authoritative error after replay into a bounded-correction path for small cadence-aligned divergence and an immediate snap path for large divergence. When bounded correction is already active, later authoritative snapshots MUST deterministically replace, fold into, or escalate that correction based on the newest residual error instead of stacking unbounded visual offsets.
|
||||
|
||||
#### Scenario: Reconciliation removes already acknowledged movement inputs
|
||||
- **WHEN** the client accepts an authoritative `PlayerState` update that acknowledges movement tick `N`
|
||||
- **THEN** locally buffered predicted `MoveInput` messages with tick less than or equal to `N` are removed from the replay buffer
|
||||
- **THEN** only `MoveInput` messages newer than `N` remain eligible for re-simulation
|
||||
|
||||
#### Scenario: Small post-replay error uses bounded correction
|
||||
- **WHEN** the controlled client finishes replay after accepting an authoritative `PlayerState` and the remaining position or rotation error stays within the configured bounded-correction threshold
|
||||
- **THEN** the client keeps authoritative ownership of the accepted snapshot
|
||||
- **THEN** local presentation converges through bounded correction instead of an immediate hard snap on that frame
|
||||
|
||||
#### Scenario: New small error updates active bounded correction
|
||||
- **WHEN** the controlled client accepts another authoritative `PlayerState` before the previous bounded correction has finished and the new residual error still stays within bounded-correction limits
|
||||
- **THEN** the sync strategy updates the active bounded correction state using the newest authoritative residual error
|
||||
- **THEN** the client does not queue multiple independent correction tails for the same controlled player
|
||||
|
||||
#### Scenario: Failed bounded correction escalates to snap
|
||||
- **WHEN** the controlled client detects that the residual error from consecutive authoritative updates exceeds the snap threshold or remains non-convergent beyond the configured correction budget
|
||||
- **THEN** the client immediately applies the authoritative transform state
|
||||
- **THEN** any active bounded correction state is discarded before later prediction continues from the authoritative baseline
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
## 1. Controlled Correction State
|
||||
|
||||
- [x] 1.1 Introduce an explicit controlled-player visual correction state that stays separate from authoritative gameplay truth.
|
||||
- [x] 1.2 Route `MovementComponent` reconciliation so accepted authoritative snapshots update or clear the visual correction state instead of restarting ad hoc per-frame correction.
|
||||
|
||||
## 2. Consecutive Snapshot Policy
|
||||
|
||||
- [x] 2.1 Implement deterministic rules for folding, replacing, or snapping active bounded correction when newer authoritative snapshots arrive before convergence completes.
|
||||
- [x] 2.2 Add convergence-budget and snap-escalation handling so repeated non-convergent small corrections cannot accumulate indefinitely.
|
||||
|
||||
## 3. Regression Coverage
|
||||
|
||||
- [x] 3.1 Add sync-strategy unit tests that cover repeated small corrections updating the active correction state.
|
||||
- [x] 3.2 Add gameplay-flow regression coverage for multi-snapshot controlled-player convergence and snap escalation after failed convergence.
|
||||
- [x] 3.3 Run the edit-mode network regression suite, or document the blocking environment issue if the runtime remains unavailable.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-04-05
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
## Context
|
||||
|
||||
本地回环测试中受控玩家出现小幅抖动。抖动根源之一是 `ReplayPendingInputs()` 中回放时对每个 `PredictedMoveStep` 的一次性大时长积分,与实时预测路径中 FixedUpdate 按 `Time.fixedDeltaTime` 逐步积分的形状不一致。
|
||||
|
||||
当前 `ReplayPendingInputs()` 实现:
|
||||
```csharp
|
||||
foreach (var replayInput in replayInputs)
|
||||
{
|
||||
ApplyTankMovementToPredictedState(
|
||||
replayInput.Input.TurnInput,
|
||||
replayInput.Input.ThrottleInput,
|
||||
replayInput.SimulatedDurationSeconds); // 一次性传入总时长
|
||||
}
|
||||
```
|
||||
|
||||
Tank 运动学中旋转影响前进方向:`heading(t+dt) = heading(t) + turnInput * turnSpeed * dt`,`position(t+dt) = position(t) + forward(heading(t+dt)) * throttleSpeed * dt`。逐步积分和一次性积分在 dt 较大时产生分歧。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- `ReplayPendingInputs()` 按固定步长逐步积分,与 FixedUpdate 预测路径完全一致
|
||||
- 回放结果与逐步实时预测的轨迹一致,消除因积分形状不同导致的残余误差
|
||||
- 不改变外部接口,只修改内部积分方式
|
||||
|
||||
**Non-Goals:**
|
||||
- 不修改服务端的 50ms cadence
|
||||
- 不解决 send interval 摆动问题(Step 3 范畴)
|
||||
- 不修改 visual correction 逻辑(Step 4 范畴)
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision: 步长取服务端的 SimulationInterval(50ms),而非客户端的 Time.fixedDeltaTime(20ms)
|
||||
|
||||
**选择**:按服务端 `SimulationInterval`(50ms)作为回放步长。
|
||||
|
||||
**理由**:
|
||||
- 服务端以 50ms 步长积分产生 authoritative state,客户端回放必须与其一致才能消除偏差
|
||||
- 客户端 FixedUpdate 20ms 是渲染/物理步长,不代表服务端模拟粒度
|
||||
- 每个 `PredictedMoveStep` 的 `SimulatedDurationSeconds` 可能是 50ms、100ms 等,按 50ms 步长逐次推进即可
|
||||
|
||||
**替代方案**:
|
||||
- 用 20ms 步长回放:与客户端 FixedUpdate 一致,但与服务端不同步,仍会产生偏差
|
||||
- 用 `SimulatedDurationSeconds` 作为单步:即当前行为,会导致非线性分歧
|
||||
|
||||
### Decision: 循环内部分步模拟,不引入新的状态累积
|
||||
|
||||
**选择**:在 `ReplayPendingInputs` 循环内按 50ms 步长迭代调用 `ApplyTankMovementToPredictedState`。
|
||||
|
||||
**实现方式**:
|
||||
```csharp
|
||||
private void ReplayPendingInputs(IReadOnlyList<PredictedMoveStep> replayInputs)
|
||||
{
|
||||
const float serverStepSeconds = 0.05f; // 50ms,服务端 SimulationInterval
|
||||
foreach (var replayInput in replayInputs)
|
||||
{
|
||||
var remaining = replayInput.SimulatedDurationSeconds;
|
||||
while (remaining > 0f)
|
||||
{
|
||||
var step = Mathf.Min(remaining, serverStepSeconds);
|
||||
ApplyTankMovementToPredictedState(
|
||||
replayInput.Input.TurnInput,
|
||||
replayInput.Input.ThrottleInput,
|
||||
step);
|
||||
remaining -= step;
|
||||
}
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 不改变 `PredictedMoveStep` 结构体接口,只修改消费方式
|
||||
- 无需新增临时状态变量
|
||||
- 逻辑清晰,与实时预测路径的积分形状完全一致
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[风险]** 如果 `SimulatedDurationSeconds` 累计值有浮点误差,循环可能产生多一步或少一步的小偏差
|
||||
- **缓解**:使用 `Mathf.Min(remaining, step)` 保护,最后一步自然截断;或对 `remaining -= step` 后加 epsilon 比较
|
||||
- **[风险]** 50ms 步长对极短的输入(比如只有一帧的输入)会产生额外计算
|
||||
- **可接受**:额外一次函数调用,代价可忽略
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
## Why
|
||||
|
||||
本地回环测试中出现受控玩家(controlled player)持续小幅抖动。经分析,问题根源之一是回滚重放(replay)时积分粒度与实时预测不一致:实时预测在 FixedUpdate 中按 `Time.fixedDeltaTime`(20ms)逐步积分,而回放时把某个输入的累计时长一次性喂给 `ApplyTankMovementToPredictedState()`。Tank 运动学是非线性的——边转向边前进时,每步的旋转角影响下一步的前进方向,导致逐步积分和一次性积分的轨迹不同。这种偏差在每次对账后出现,被 visual correction 反复拉回,表现为细碎抖动。
|
||||
|
||||
## What Changes
|
||||
|
||||
1. **修改 `ReplayPendingInputs()` 的积分方式**:将一次性大时长积分改为固定步长的逐步积分,与 `FixedUpdate` 预测路径的积分形状完全一致
|
||||
2. **`PredictedMoveStep.SimulatedDurationSeconds` 的处理语义变更**:`SimulatedDurationSeconds` 仍记录该输入的总模拟时长,但 replay 时按服务端的 50ms 步长(`ServerAuthoritativeMovementConfiguration.SimulationInterval`)进行分步模拟
|
||||
3. **添加测试**:比较相同输入序列下逐步预测和回放预测的轨迹一致性,验证修复效果
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `client-prediction-replay-granularity`: 定义客户端回放预测与实时预测使用相同积分步长的行为约束和验证方式
|
||||
|
||||
### Modified Capabilities
|
||||
- `client-gameplay-input`: 扩展 `ReplayPendingInputs` 的实现要求,明确回放必须使用固定步长逐步积分而非一次性累积积分
|
||||
|
||||
## Impact
|
||||
|
||||
- **涉及代码**:`Assets/Scripts/MovementComponent.cs` 中的 `ReplayPendingInputs()` 方法
|
||||
- **涉及测试**:`Assets/Tests/EditMode/Network/GameplayFlowRoundTripTests.cs` 或新建回归测试
|
||||
- **其他系统**:`PredictedMoveStep` 结构体(`ClientPredictionBuffer.cs`)的语义略有调整,但接口不变
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# client-gameplay-input Specification
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Controlled client movement input preserves immediate prediction and explicit stop signaling
|
||||
|
||||
The MVP client SHALL capture movement intent for the controlled player in Unity-side input code, apply local movement prediction immediately, and send `MoveInput` updates through the networking boundary. When movement input transitions from non-zero to idle, the client MUST send one final zero-vector `MoveInput` so authoritative movement can stop cleanly. When the client reconciles against authoritative state and replays pending `MoveInput` messages, the replay path MUST apply each pending input in fixed-duration substeps matching the server authoritative movement cadence, so that replay trajectory matches live prediction trajectory for the same input sequence.
|
||||
|
||||
#### Scenario: Controlled player moves locally without waiting for the network
|
||||
- **WHEN** the controlled player provides non-zero movement input
|
||||
- **THEN** the client applies local movement prediction immediately for presentation
|
||||
- **THEN** the client submits a `MoveInput` carrying the current player id, tick, and planar movement vector through the networking send path
|
||||
|
||||
#### Scenario: Releasing movement emits an explicit stop update
|
||||
- **WHEN** the controlled player releases movement input after previously providing non-zero movement
|
||||
- **THEN** the client sends exactly one final `MoveInput` whose movement vector is zero
|
||||
- **THEN** local predicted movement also stops immediately without waiting for authoritative correction
|
||||
|
||||
#### Scenario: Replay uses fixed-step substeps matching server cadence
|
||||
- **WHEN** the client accepts an authoritative `PlayerState` and replays pending `MoveInput` messages
|
||||
- **THEN** each `PredictedMoveStep` is consumed by applying its input in fixed-duration substeps equal to the server authoritative movement cadence
|
||||
- **THEN** the replay accumulation shape is identical to the live FixedUpdate prediction path for the same input values
|
||||
- **THEN** non-linear trajectories (e.g. simultaneous turn-and-move) produce the same result in both replay and live prediction
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
# client-prediction-replay-granularity Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
Define the contract that client-side replay of pending movement inputs MUST use fixed-step accumulation matching the server authoritative movement cadence, not a single accumulated duration, so that replay trajectory matches live prediction trajectory for the same input sequence.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Replay uses fixed-step accumulation matching server cadence
|
||||
|
||||
The controlled-client prediction replay path SHALL consume each pending `PredictedMoveStep` by applying its input in fixed-duration substeps equal to the server authoritative movement cadence, regardless of the step's total `SimulatedDurationSeconds`. The replay accumulation shape MUST be identical to the live `FixedUpdate` prediction path for the same input values.
|
||||
|
||||
#### Scenario: Replay produces same trajectory as live prediction for steady input
|
||||
- **WHEN** the client replays a `PredictedMoveStep` with turn=0, throttle=1, duration=0.15s using a 0.05s server cadence
|
||||
- **THEN** the replay applies 0.05s + 0.05s + 0.05s substeps in sequence
|
||||
- **THEN** the final predicted position matches the position that would result from three consecutive FixedUpdate predictions of 0.05s each with the same input
|
||||
|
||||
#### Scenario: Replay produces same trajectory as live prediction for turn-and-move input
|
||||
- **WHEN** the client replays a `PredictedMoveStep` with turn=0.5, throttle=1, duration=0.10s using a 0.05s server cadence
|
||||
- **THEN** the replay applies two 0.05s substeps where each substep's heading affects the next substep's forward direction
|
||||
- **THEN** the final predicted heading and position match the live prediction path for the same input sequence
|
||||
|
||||
#### Scenario: Replay handles non-multiples of cadence interval
|
||||
- **WHEN** the client replays a `PredictedMoveStep` with duration=0.12s using a 0.05s cadence
|
||||
- **THEN** the replay applies 0.05s + 0.05s + 0.02s substeps sequentially
|
||||
- **THEN** no remaining duration is lost or double-counted
|
||||
|
||||
### Requirement: Replay trajectory determinism is verifiable
|
||||
|
||||
The client prediction system SHOULD provide a deterministic way to verify that replay and live prediction produce identical trajectories for a given input sequence, enabling regression coverage.
|
||||
|
||||
#### Scenario: Replay and live prediction produce identical results
|
||||
- **WHEN** a controlled client records a `MoveInput` sequence during live play
|
||||
- **AND** the client triggers reconciliation and replays those same inputs
|
||||
- **THEN** the final predicted pose after replay equals the predicted pose that would result from live FixedUpdate simulation for the same input sequence
|
||||
- **THEN** the result is stable across multiple replays of the same input sequence
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
## 1. 理解和实现
|
||||
|
||||
- [x] 1.1 理解 `ApplyTankMovementToPredictedState` 的积分逻辑(旋转 → 前进方向的依赖关系)
|
||||
- [x] 1.2 理解当前 `ReplayPendingInputs` 的一次性积分行为与问题
|
||||
- [x] 1.3 在 `MovementComponent` 中引入服务端 `SimulationInterval` 的引用(50ms 步长常量)
|
||||
|
||||
## 2. 修改 `ReplayPendingInputs` 实现
|
||||
|
||||
- [x] 2.1 修改 `ReplayPendingInputs` 循环,将每个 `PredictedMoveStep` 的总时长按 50ms 步长分步模拟
|
||||
- [x] 2.2 添加浮点截断保护,确保所有时长都被消耗而无遗失
|
||||
- [x] 2.3 验证修改后的实现与 `FixedUpdate` 预测路径的积分形状一致
|
||||
|
||||
## 3. 添加回归测试
|
||||
|
||||
- [x] 3.1 在 `GameplayFlowRoundTripTests.cs` 或新建测试文件中添加轨迹一致性测试
|
||||
- [x] 3.2 测试用例:相同 turn+throttle 输入序列,逐步预测 vs 回放预测的最终位置和旋转相等
|
||||
- [x] 3.3 测试用例:非线性运动(同时转向和前进),验证逐步积分与一次性积分的结果不同
|
||||
- [x] 3.4 测试用例:非 50ms 倍数的总时长(如 0.12s),验证分步后无遗失
|
||||
|
||||
## 4. 验证
|
||||
|
||||
- [ ] 4.1 运行所有 EditMode 测试确保无回归(在 Unity Editor 内执行)
|
||||
- [ ] 4.2 本地回环验证抖动是否改善
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# authoritative-movement-bootstrap Specification
|
||||
|
||||
## Purpose
|
||||
Define how the controlled client bootstraps steady-state movement prediction from server-confirmed authoritative movement parameters.
|
||||
|
||||
## Requirements
|
||||
### Requirement: Client prediction bootstraps from server-confirmed movement parameters
|
||||
The client SHALL establish controlled-player prediction parameters from server-confirmed authoritative movement settings before treating local prediction as steady-state truth. Client-local candidate values MAY exist before login succeeds, but long-lived prediction MUST switch to the server-confirmed parameters for the controlled player.
|
||||
|
||||
#### Scenario: Login success provides authoritative movement parameters for prediction
|
||||
- **WHEN** the controlled client completes login and receives the server-confirmed movement bootstrap data
|
||||
- **THEN** the client stores the authoritative movement parameters for that controlled player
|
||||
- **THEN** subsequent local movement prediction uses those server-confirmed parameters instead of continuing to rely on an unrelated local UI value
|
||||
|
||||
### Requirement: Server-owned movement parameters remain the single gameplay authority
|
||||
The server SHALL keep authoritative ownership of movement tuning used for authoritative movement resolution, and any client-visible movement parameters used for prediction MUST be derived from that server-owned configuration rather than from an independent client-only truth source.
|
||||
|
||||
#### Scenario: Client candidate speed does not override server movement authority
|
||||
- **WHEN** a client proposes or locally configures a movement speed that differs from the server-owned movement speed
|
||||
- **THEN** the server-owned movement configuration remains authoritative for gameplay resolution
|
||||
- **THEN** the client's steady-state prediction parameters converge to the server-confirmed value instead of preserving the divergent local candidate
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# authoritative-movement-cadence Specification
|
||||
|
||||
## Purpose
|
||||
Define the fixed authoritative movement cadence contract and its observability for server runtime diagnostics and regression coverage.
|
||||
|
||||
## Requirements
|
||||
### Requirement: Server authoritative movement uses a fixed cadence contract
|
||||
The shared server runtime SHALL define a fixed authoritative movement cadence for simulation and snapshot production. Authoritative movement updates MUST be stepped from that configured cadence instead of arbitrary caller-provided elapsed values.
|
||||
|
||||
#### Scenario: Runtime advances movement using configured cadence
|
||||
- **WHEN** the server runtime advances authoritative movement while one or more managed peers have movement state
|
||||
- **THEN** the authoritative movement coordinator steps simulation using the configured cadence interval
|
||||
- **THEN** the same cadence governs later authoritative `PlayerState` production for that runtime
|
||||
|
||||
### Requirement: Cadence information is observable for diagnostics and regression tests
|
||||
The shared runtime SHALL expose the active authoritative movement cadence through diagnostics or runtime state that tests and debugging tools can read without inspecting private loop internals.
|
||||
|
||||
#### Scenario: Tests can read active movement cadence
|
||||
- **WHEN** an edit-mode regression or debugging path inspects the server runtime after movement setup
|
||||
- **THEN** it can observe the authoritative movement cadence configured for that runtime
|
||||
- **THEN** the observed value matches the cadence used by authoritative movement stepping
|
||||
|
|
@ -13,13 +13,23 @@ The client SHALL keep one explicit owned authoritative `PlayerState` snapshot fo
|
|||
- **THEN** presentation and diagnostics read authoritative `position`, `rotation`, `hp`, and optional `velocity` from that owned snapshot
|
||||
|
||||
### Requirement: Local player reconciliation applies the full authoritative state by tick
|
||||
The controlled client SHALL continue reconciling local prediction from authoritative `PlayerState.Tick`, and that reconciliation MUST apply the accepted authoritative `position` and `rotation` while keeping authoritative HP and optional velocity synchronized with the owned player-state snapshot.
|
||||
The controlled client SHALL continue reconciling local prediction from authoritative `PlayerState` snapshots while keeping authoritative HP and optional velocity synchronized with the owned player-state snapshot. Reconciliation MUST use the acknowledged movement-input tick defined by the sync strategy, and the visible controlled-player transform MUST keep authoritative gameplay truth separate from short-lived visual correction state. Small divergence after replay MUST converge through explicit bounded correction state, while large divergence or failed convergence MUST still snap immediately to authoritative `position` and `rotation`.
|
||||
|
||||
#### Scenario: Local authoritative state corrects predicted presentation
|
||||
- **WHEN** the controlled player accepts an authoritative `PlayerState` for tick `N`
|
||||
- **WHEN** the controlled player accepts an authoritative `PlayerState` whose acknowledged movement-input tick is `N`
|
||||
- **THEN** local reconciliation prunes or replays predicted movement using tick `N` according to the sync strategy
|
||||
- **THEN** the local player's visible transform is corrected toward authoritative `position` and `rotation`
|
||||
- **THEN** the local player's authoritative HP on the client matches the accepted `PlayerState`
|
||||
- **THEN** the controlled player's authoritative gameplay state updates immediately to the accepted `position`, `rotation`, HP, and optional velocity
|
||||
- **THEN** the local player's visible transform may temporarily differ only through bounded visual correction state that converges back to the authoritative baseline
|
||||
|
||||
#### Scenario: Consecutive small corrections replace or fold into active visual correction
|
||||
- **WHEN** the controlled player accepts a newer authoritative `PlayerState` while a bounded visual correction is still active and the new residual error remains inside the configured bounded-correction limits
|
||||
- **THEN** the client updates the active visual correction state according to the sync strategy instead of preserving stale correction targets indefinitely
|
||||
- **THEN** the controlled player's authoritative gameplay state still reflects only the newest accepted `PlayerState`
|
||||
|
||||
#### Scenario: Large local divergence bypasses bounded correction
|
||||
- **WHEN** the controlled player accepts an authoritative `PlayerState` and the remaining transform error exceeds the configured snap threshold or the active bounded correction can no longer converge within its budget
|
||||
- **THEN** the controlled player's visible transform snaps immediately to authoritative `position` and `rotation`
|
||||
- **THEN** any temporary visual correction state is cleared before later local prediction resumes from that authoritative baseline
|
||||
|
||||
### Requirement: Remote players apply authoritative state without inventing gameplay truth
|
||||
Remote player presentation SHALL consume the accepted authoritative player-state snapshot owned by the client and MUST NOT invent HP or final gameplay state locally. Remote movement presentation MUST smooth authoritative position and rotation through a small buffered snapshot interpolation path instead of applying only the latest snapshot directly. Stale remote `PlayerState` packets that are older than the latest accepted authoritative tick for that player MUST NOT overwrite the owned snapshot or enter the interpolation buffer.
|
||||
|
|
|
|||
|
|
@ -26,10 +26,33 @@ The edit-mode regression suite SHALL cover the client path that buffers and cons
|
|||
- **THEN** the test verifies the resulting interpolation or latest-snapshot clamp decision matches the MVP remote-presentation rules
|
||||
|
||||
### Requirement: Gameplay-flow regressions include a fake-transport authoritative round trip
|
||||
The edit-mode regression suite SHALL include at least one deterministic fake-transport test that spans client send behavior, server-authoritative processing, and outgoing authoritative results. That round-trip regression MUST cover `MoveInput -> PlayerState` and `ShootInput -> CombatEvent` within the same MVP gameplay-flow suite.
|
||||
The edit-mode regression suite SHALL include at least one deterministic fake-transport test that spans client send behavior, server-authoritative processing, and outgoing authoritative results. That round-trip regression MUST cover `MoveInput -> PlayerState` and `ShootInput -> CombatEvent` within the same MVP gameplay-flow suite, and it MUST assert that authoritative movement stepping follows the configured cadence contract.
|
||||
|
||||
#### Scenario: Fake-transport round trip preserves server authority across movement and combat
|
||||
- **WHEN** an edit-mode regression test drives gameplay input through fake client/server transports and advances the server authority loop
|
||||
- **THEN** the authoritative server path emits `PlayerState` snapshots in response to movement input
|
||||
- **THEN** the authoritative server path emits `PlayerState` snapshots in response to movement input using the configured authoritative movement cadence
|
||||
- **THEN** the authoritative server path emits `CombatEvent` results in response to shooting input
|
||||
- **THEN** the combined test protects both client single-session input flow and server multi-session authoritative behavior from regression
|
||||
|
||||
### Requirement: Gameplay-flow regressions cover controlled-player correction decisions
|
||||
The edit-mode regression suite SHALL cover the controlled-player reconciliation path after authoritative movement replay, including bounded correction for small cadence-aligned error, correction replacement under consecutive authoritative snapshots, and hard snap fallback for large or non-convergent divergence.
|
||||
|
||||
#### Scenario: Controlled-player reconciliation uses bounded correction for small error
|
||||
- **WHEN** an edit-mode regression test applies an authoritative local `PlayerState` that leaves only small post-replay divergence
|
||||
- **THEN** the controlled-player path keeps authoritative ownership of the snapshot
|
||||
- **THEN** visible correction converges without an immediate hard snap on the acceptance frame
|
||||
|
||||
#### Scenario: Controlled-player reconciliation updates active correction on repeated small snapshots
|
||||
- **WHEN** an edit-mode regression test feeds multiple authoritative local `PlayerState` updates whose residual divergence remains inside bounded-correction limits while a prior correction is still active
|
||||
- **THEN** the controlled-player path replaces or folds the active correction according to the sync strategy
|
||||
- **THEN** the test proves the client does not accumulate multiple stale correction tails
|
||||
|
||||
#### Scenario: Controlled-player reconciliation snaps on large divergence
|
||||
- **WHEN** an edit-mode regression test applies an authoritative local `PlayerState` that leaves divergence beyond the configured snap threshold
|
||||
- **THEN** the controlled-player path immediately applies the authoritative transform state
|
||||
- **THEN** later prediction resumes from that authoritative baseline
|
||||
|
||||
#### Scenario: Controlled-player reconciliation snaps after failed convergence
|
||||
- **WHEN** an edit-mode regression test feeds consecutive authoritative local `PlayerState` updates that keep bounded correction from converging within the configured budget
|
||||
- **THEN** the controlled-player path escalates to a hard snap
|
||||
- **THEN** the active correction state is cleared before later local prediction continues
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ The repository SHALL keep the source protobuf schema that defines gameplay netwo
|
|||
- **THEN** the checked-in generated code matches the schema contract used by client and server hosts
|
||||
|
||||
### Requirement: Gameplay messages expose explicit MVP payload fields
|
||||
The shared networking contract SHALL define the MVP payload fields for gameplay messages explicitly in the source protobuf schema and generated C# messages. `MoveInput` MUST expose `playerId`, `tick`, `moveX`, and `moveY`; `ShootInput` MUST expose `playerId`, `tick`, `dirX`, `dirY`, and an optional `targetId`; `PlayerState` MUST expose `playerId`, `tick`, `position`, `rotation`, `hp`, and an optional `velocity`; `CombatEvent` MUST expose `tick`, `eventType`, `attackerId`, `targetId`, `damage`, and an optional `hitPosition`. The shared contract MUST also provide `CombatEventType` so combat results use explicit event categories rather than ad hoc integer payload conventions.
|
||||
The shared networking contract SHALL define the MVP payload fields for gameplay messages explicitly in the source protobuf schema and generated C# messages. `MoveInput` MUST expose `playerId`, `tick`, `moveX`, and `moveY`; `ShootInput` MUST expose `playerId`, `tick`, `dirX`, `dirY`, and an optional `targetId`; `PlayerState` MUST expose `playerId`, `tick`, `acknowledgedMoveTick`, `position`, `rotation`, `hp`, and an optional `velocity`; `CombatEvent` MUST expose `tick`, `eventType`, `attackerId`, `targetId`, `damage`, and an optional `hitPosition`. The shared contract MUST also provide `CombatEventType` so combat results use explicit event categories rather than ad hoc integer payload conventions.
|
||||
|
||||
#### Scenario: Movement input carries explicit movement fields
|
||||
- **WHEN** client or server code constructs or parses `MoveInput`
|
||||
|
|
@ -31,8 +31,8 @@ The shared networking contract SHALL define the MVP payload fields for gameplay
|
|||
|
||||
#### Scenario: Authoritative player state carries explicit gameplay state fields
|
||||
- **WHEN** client or server code constructs or parses `PlayerState`
|
||||
- **THEN** the message exposes `playerId`, `tick`, `position`, `rotation`, `hp`, and `velocity`
|
||||
- **THEN** authoritative movement and health state are expressed without ad hoc payload extensions
|
||||
- **THEN** the message exposes `playerId`, `tick`, `acknowledgedMoveTick`, `position`, `rotation`, `hp`, and `velocity`
|
||||
- **THEN** snapshot ordering and acknowledged-input reconciliation are both expressed without ad hoc payload extensions or overloaded tick semantics
|
||||
|
||||
#### Scenario: Combat events carry explicit result fields and event categories
|
||||
- **WHEN** client or server code constructs or parses `CombatEvent`
|
||||
|
|
|
|||
|
|
@ -41,13 +41,28 @@ The high-frequency sync strategy SHALL tag gameplay synchronization messages wit
|
|||
- **THEN** reliable ordered handling remains responsible for preserving event delivery semantics
|
||||
|
||||
### Requirement: Authoritative correction prunes acknowledged prediction history
|
||||
The client sync strategy SHALL reconcile local prediction against authoritative player-state updates by pruning acknowledged movement inputs at or before the authoritative tick and only reapplying newer pending `MoveInput` messages.
|
||||
The client sync strategy SHALL reconcile local prediction against authoritative player-state updates by pruning acknowledged movement inputs at or before the authoritative acknowledged movement tick and only reapplying newer pending `MoveInput` messages. For the controlled player, reconciliation MUST classify authoritative error after replay into a bounded-correction path for small cadence-aligned divergence and an immediate snap path for large divergence. When bounded correction is already active, later authoritative snapshots MUST deterministically replace, fold into, or escalate that correction based on the newest residual error instead of stacking unbounded visual offsets.
|
||||
|
||||
#### Scenario: Reconciliation removes already acknowledged movement inputs
|
||||
- **WHEN** the client accepts an authoritative `PlayerState` update for tick `N`
|
||||
- **WHEN** the client accepts an authoritative `PlayerState` update that acknowledges movement tick `N`
|
||||
- **THEN** locally buffered predicted `MoveInput` messages with tick less than or equal to `N` are removed from the replay buffer
|
||||
- **THEN** only `MoveInput` messages newer than `N` remain eligible for re-simulation
|
||||
|
||||
#### Scenario: Small post-replay error uses bounded correction
|
||||
- **WHEN** the controlled client finishes replay after accepting an authoritative `PlayerState` and the remaining position or rotation error stays within the configured bounded-correction threshold
|
||||
- **THEN** the client keeps authoritative ownership of the accepted snapshot
|
||||
- **THEN** local presentation converges through bounded correction instead of an immediate hard snap on that frame
|
||||
|
||||
#### Scenario: New small error updates active bounded correction
|
||||
- **WHEN** the controlled client accepts another authoritative `PlayerState` before the previous bounded correction has finished and the new residual error still stays within bounded-correction limits
|
||||
- **THEN** the sync strategy updates the active bounded correction state using the newest authoritative residual error
|
||||
- **THEN** the client does not queue multiple independent correction tails for the same controlled player
|
||||
|
||||
#### Scenario: Failed bounded correction escalates to snap
|
||||
- **WHEN** the controlled client detects that the residual error from consecutive authoritative updates exceeds the snap threshold or remains non-convergent beyond the configured correction budget
|
||||
- **THEN** the client immediately applies the authoritative transform state
|
||||
- **THEN** any active bounded correction state is discarded before later prediction continues from the authoritative baseline
|
||||
|
||||
### Requirement: Clock synchronization is a separate sync-policy concern
|
||||
The shared networking core SHALL process server-tick or clock-synchronization samples through a dedicated sync-policy component rather than storing clock-sync ownership inside `SessionManager`.
|
||||
|
||||
|
|
|
|||
|
|
@ -23,11 +23,11 @@ The shared server networking path SHALL register `MoveInput` handling through th
|
|||
- **THEN** authoritative movement state for other managed peers remains unchanged
|
||||
|
||||
### Requirement: Server owns authoritative movement resolution
|
||||
The shared server networking path SHALL own the final movement state for each managed peer, including position, rotation, velocity, and stop state. Zero-vector movement input MUST stop authoritative movement rather than leaving the peer in its previous moving state.
|
||||
The shared server networking path SHALL own the final movement state for each managed peer, including position, rotation, velocity, and stop state. Zero-vector movement input MUST stop authoritative movement rather than leaving the peer in its previous moving state. The authoritative movement integrator MUST advance using the runtime's configured authoritative movement cadence so that movement resolution and later `PlayerState` snapshots are produced from the same server-side stepping contract.
|
||||
|
||||
#### Scenario: Non-zero input advances authoritative movement state
|
||||
- **WHEN** the server processes an accepted non-zero `MoveInput` for a managed peer during an authority update step
|
||||
- **THEN** the server updates that peer's authoritative position, rotation, and velocity from server-side movement resolution
|
||||
- **THEN** the server updates that peer's authoritative position, rotation, and velocity from server-side movement resolution using the configured authoritative movement cadence
|
||||
- **THEN** the resulting state becomes the source of truth for later `PlayerState` broadcast
|
||||
|
||||
#### Scenario: Zero-vector input stops authoritative movement
|
||||
|
|
@ -39,7 +39,7 @@ The shared server networking path SHALL own the final movement state for each ma
|
|||
The shared server networking path SHALL emit authoritative `PlayerState` snapshots for managed peers at a fixed cadence using the existing sync-lane message contract. Each snapshot MUST be derived from the server-owned authoritative player state and include the authoritative tick for client reconciliation and interpolation. Authoritative HP changes produced by server-side combat resolution MUST be reflected in later snapshots for the affected peer.
|
||||
|
||||
#### Scenario: Authority update step emits sync-lane player snapshots
|
||||
- **WHEN** the server reaches a configured authority broadcast cadence while one or more managed peers have authoritative player state
|
||||
- **WHEN** the server reaches the configured authoritative movement cadence while one or more managed peers have authoritative player state
|
||||
- **THEN** it sends `PlayerState` snapshots using the sync-lane delivery policy when a distinct sync transport exists
|
||||
- **THEN** each snapshot includes the authoritative position, rotation, velocity, HP, and tick from server-owned state
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue