Skill: Implement opsx-render Command
Overview
The opsx-render command renders OpenSpec artifact files (spec.md and tasks.md) from the current state of td task management. This is a critical component that enables agents to work in td while keeping the filesystem synchronized.
Architecture
Input
- •Change name (e.g.,
opsx-render agent-task-management) - •Change must exist and have proposal, specs, and tasks artifacts
Processing Pipeline
1. Load Change Metadata ├── Get proposal_feature_id from proposal.md frontmatter └── Validate td state has proposal feature/bug 2. Load Spec Metadata ├── Read all specs/**/*.md files ├── Extract spec_feature_id from each spec's frontmatter └── Validate td state has corresponding spec features/bugs 3. Query TD State ├── td list --parent <proposal_feature_id> → all spec features ├── For each spec: │ └── td list --parent <spec_feature_id> --labels requirement → all requirements └── Extract task status (open/in_progress/done) 4. Render spec.md Files ├── For each spec file: │ ├── Keep existing requirement content (don't regenerate) │ ├── Preserve all scenarios and descriptions │ ├── Verify all requirement task IDs exist in td │ └── Update frontmatter with spec_feature_id └── Write to specs/<capability>/spec.md 5. Render tasks.md ├── Group requirement tasks by parent spec ├── Create section for each spec │ ├── Section name and status (Not Started/In Progress/Done) │ ├── List each requirement task with checkbox state │ └── Calculate phase status from task states └── Write summary and progress information 6. Stage Files (Optional) └── git add openspec/changes/<change>/specs/**/*.md openspec/changes/<change>/tasks.md
Key Design Decisions
- •
Specs are NOT regenerated: The spec.md content comes from the files themselves, not from td. Rendering only updates frontmatter and preserves all content.
- •
Requirement IDs are preserved: Task IDs in requirement markdown are read from the spec files, not regenerated. This maintains traceability.
- •
Task status comes from td: Only the checkbox state [ ] vs [x] in tasks.md changes based on td state. Everything else in tasks.md is preserved.
- •
Idempotent operation: Running render multiple times without td changes produces identical output. No file timestamp churn.
- •
Atomic writes: All files are written atomically - no partial updates. If any file fails, the entire operation fails and files remain unchanged.
Implementation Requirements
Requirement 1: Generate spec.md from td state (td-87cb57)
Acceptance Criteria:
- •✅ Read spec.md frontmatter to get spec_feature_id
- •✅ Verify spec_feature_id exists in td
- •✅ Keep all requirement content from existing spec.md
- •✅ Verify requirement task IDs exist in td
- •✅ Output spec.md with preserved content
Implementation Details:
- •Parse spec.md frontmatter YAML
- •Extract
spec_feature_id - •Query td:
td show <spec_feature_id>to verify it exists - •Read entire spec.md content
- •For each requirement section:
- •Extract the requirement name and td-id from markdown
- •Query td:
td show <td-id>to verify it exists and has label=requirement - •Keep all content between requirement header and next section
- •Write updated spec.md with same content but current frontmatter
Scenarios:
- •Spec with valid td ids: render succeeds, file unchanged
- •Spec with missing td id: error message explaining which task is missing
- •Spec with corrupted frontmatter: error with hint to run
/opsx-continue
Requirement 2: Generate tasks.md from td state (td-65f257)
Acceptance Criteria:
- •✅ Group requirement tasks by parent spec feature
- •✅ Show task status from td (open/in_progress/done)
- •✅ Create checkbox entries from task status
- •✅ Calculate phase completion percentage
- •✅ Output tasks.md with correct grouping and status
Implementation Details:
- •
For each spec feature (queried from td):
- •Get spec name and feature id
- •Create a section header with spec name
- •
For each requirement task under that spec:
- •Get task status from td
- •Map: open → [ ], in_progress/done → [x]
- •Include task ID in description
- •Include requirement title from task description
- •
Calculate phase status:
- •If all tasks done: "Done"
- •If any tasks in_progress: "In Progress"
- •If all tasks open: "Not Started"
- •If mixed: "In Progress"
- •
Include summary section:
- •Total requirements
- •Complete count
- •Remaining count
- •Per-phase breakdown
Scenarios:
- •No requirements: tasks.md shows 0 total tasks with guidance
- •Mixed completion: tasks.md shows correct checkbox state for each
- •New phase (no requirements yet): phase section shows "Not Started"
Requirement 3: Preserve task IDs and links (td-b16425)
Acceptance Criteria:
- •✅ Every requirement in spec.md includes its td task ID
- •✅ Every task in tasks.md includes its td task ID
- •✅ IDs are in consistent format (td-xxxxxx)
- •✅ IDs match actual td tasks
- •✅ Traceability maintained across files
Implementation Details:
- •
In spec.md rendering:
- •Extract td-id from requirement markdown
- •Verify format: matches
td-[a-f0-9]{6} - •Keep in requirement header:
### Requirement: <name> - td-xxxxx
- •
In tasks.md rendering:
- •Include td-id in task description/comment
- •Format:
- [x] td-xxxxx: requirement name - •Maintain consistent placement (always after checkbox)
- •
Validation:
- •Every requirement must have a td-id in spec.md
- •Every task checkbox must have a td-id in tasks.md
- •All ids must exist in td and have label=requirement
Scenarios:
- •Valid ids: render preserves them exactly as-is
- •Missing id in spec: error with clear message
- •Mismatched id (doesn't exist in td): error with hint to check td
Requirement 4: Follow openspec templates (td-13ec31)
Acceptance Criteria:
- •✅ Generated spec.md matches openspec template structure
- •✅ Generated tasks.md matches openspec template structure
- •✅ Frontmatter is valid YAML
- •✅ Markdown formatting is consistent
- •✅ Sections use correct heading levels
Implementation Details:
- •
spec.md template structure:
code--- spec_feature_id: td-xxxxx --- # Specification: [Name] ## Overview [existing content] ## ADDED Requirements [existing content] ## MODIFIED Requirements [existing content] ## REMOVED Requirements [existing content]
- •
tasks.md template structure:
code# Tasks ## 1. [Spec Name] - Status: [Not Started/In Progress/Done] - Type: Spec Feature (td-xxxxx) - Assignee: [from tasks.md] - Label: [from tasks.md] - Tasks: - [x] td-xxxxx: requirement name - [ ] td-xxxxx: requirement name ## Summary [progress information]
- •
Validation:
- •All required sections present
- •Frontmatter is valid YAML (parseable)
- •Headings use correct levels (# ## ###)
- •Markdown is valid and renders correctly
Scenarios:
- •Valid structure: render succeeds
- •Missing frontmatter: add it
- •Malformed YAML: error with hint about frontmatter
Requirement 5: Handle edge cases gracefully (td-509158)
Acceptance Criteria:
- •✅ Missing spec feature: clear error message
- •✅ Missing requirement task: clear error message
- •✅ Corrupted td state: diagnostic message
- •✅ No partial writes: atomicity guaranteed
- •✅ All edge cases fail safely
Implementation Details:
- •
Error handling:
- •Missing spec_feature_id → "spec.md missing spec_feature_id. Run: td create -t feature..."
- •Missing requirement task → "Requirement task td-xxxxx not found in td. Create it with: td create..."
- •Corrupted td data → "Failed to query td for spec feature td-xxxxx: [error details]"
- •Filesystem error → "Failed to write specs/foo/spec.md: [error details]"
- •
Atomicity:
- •Don't update files one-by-one
- •Render all in-memory first
- •Validate all files can be written
- •Then write all files together
- •If any write fails, leave filesystem unchanged
- •
Recovery:
- •Don't delete original files on error
- •Leave originals intact, show error
- •Agent can fix issue and retry
- •
Special cases:
- •First time rendering (files don't exist): create them
- •Only proposal/design changed (no specs): skip rendering, success
- •No td changes (state unchanged): render succeeds, files unchanged
- •Partial specs (some have td ids, some don't): error identifying missing ids
Scenarios:
- •First render: creates all spec and tasks files
- •Missing required ids: error with actionable fix
- •No changes: succeeds, files unchanged (idempotent)
- •Filesystem full: error, original files preserved
Integration with Pre-Commit Hooks
The opsx-render command is invoked automatically by the pre-commit hook when files in openspec/changes/* change:
.git/hooks/pre-commit:
if [[ changed_files contain openspec/changes/* ]]; then
change_name=$(extract_change_name_from_path)
opsx-render "$change_name"
git add openspec/changes/$change_name/specs/**/*.md openspec/changes/$change_name/tasks.md
fi
This ensures documents are always in sync with td state at commit boundaries.
Testing Strategy
Unit Tests
- •Test each spec.md rendering scenario
- •Test tasks.md grouping logic
- •Test td query parsing
- •Test YAML frontmatter handling
- •Test edge case error messages
Integration Tests
- •Test with real td state (create change, specs, requirements, then render)
- •Test idempotency (render twice, files identical)
- •Test atomicity (simulate write failure, verify no partial updates)
- •Test with agent-task-management change (real-world data)
Smoke Tests
- •Create new change, specs, requirements
- •Run opsx-render
- •Verify spec.md and tasks.md are correct
- •Modify requirement status in td
- •Run opsx-render again
- •Verify tasks.md checkbox state changed
Next Steps
- •Implement opsx-render command in CLI
- •Add to openspec command registry
- •Integrate with pre-commit hook
- •Test with agent-task-management change
- •Move to opsx-* commands enhancement (Phase 3)