@paths.design/caws-cli 10.1.0 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -374
- package/dist/index.js +43 -756
- package/dist/shell/binding/resolve-binding.d.ts +4 -0
- package/dist/shell/binding/resolve-binding.d.ts.map +1 -0
- package/dist/shell/binding/resolve-binding.js +228 -0
- package/dist/shell/binding/resolve-binding.js.map +1 -0
- package/dist/shell/binding/types.d.ts +42 -0
- package/dist/shell/binding/types.d.ts.map +1 -0
- package/dist/shell/binding/types.js +21 -0
- package/dist/shell/binding/types.js.map +1 -0
- package/dist/shell/commands/claim.d.ts +14 -0
- package/dist/shell/commands/claim.d.ts.map +1 -0
- package/dist/shell/commands/claim.js +197 -0
- package/dist/shell/commands/claim.js.map +1 -0
- package/dist/shell/commands/doctor.d.ts +13 -0
- package/dist/shell/commands/doctor.d.ts.map +1 -0
- package/dist/shell/commands/doctor.js +97 -0
- package/dist/shell/commands/doctor.js.map +1 -0
- package/dist/shell/commands/evidence.d.ts +28 -0
- package/dist/shell/commands/evidence.d.ts.map +1 -0
- package/dist/shell/commands/evidence.js +166 -0
- package/dist/shell/commands/evidence.js.map +1 -0
- package/dist/shell/commands/gates.d.ts +19 -0
- package/dist/shell/commands/gates.d.ts.map +1 -0
- package/dist/shell/commands/gates.js +181 -0
- package/dist/shell/commands/gates.js.map +1 -0
- package/dist/shell/commands/init.d.ts +8 -0
- package/dist/shell/commands/init.d.ts.map +1 -0
- package/dist/shell/commands/init.js +64 -0
- package/dist/shell/commands/init.js.map +1 -0
- package/dist/shell/commands/scope.d.ts +11 -0
- package/dist/shell/commands/scope.d.ts.map +1 -0
- package/dist/shell/commands/scope.js +92 -0
- package/dist/shell/commands/scope.js.map +1 -0
- package/dist/shell/commands/status.d.ts +15 -0
- package/dist/shell/commands/status.d.ts.map +1 -0
- package/dist/shell/commands/status.js +106 -0
- package/dist/shell/commands/status.js.map +1 -0
- package/dist/shell/commands/waiver.d.ts +38 -0
- package/dist/shell/commands/waiver.d.ts.map +1 -0
- package/dist/shell/commands/waiver.js +240 -0
- package/dist/shell/commands/waiver.js.map +1 -0
- package/dist/shell/gates/disposition.d.ts +23 -0
- package/dist/shell/gates/disposition.d.ts.map +1 -0
- package/dist/shell/gates/disposition.js +87 -0
- package/dist/shell/gates/disposition.js.map +1 -0
- package/dist/shell/gates/gate-result-contract.d.ts +39 -0
- package/dist/shell/gates/gate-result-contract.d.ts.map +1 -0
- package/dist/shell/gates/gate-result-contract.js +150 -0
- package/dist/shell/gates/gate-result-contract.js.map +1 -0
- package/dist/shell/gates/quality-gates-adapter.d.ts +55 -0
- package/dist/shell/gates/quality-gates-adapter.d.ts.map +1 -0
- package/dist/shell/gates/quality-gates-adapter.js +161 -0
- package/dist/shell/gates/quality-gates-adapter.js.map +1 -0
- package/dist/shell/gates/waiver-filter.d.ts +58 -0
- package/dist/shell/gates/waiver-filter.d.ts.map +1 -0
- package/dist/shell/gates/waiver-filter.js +119 -0
- package/dist/shell/gates/waiver-filter.js.map +1 -0
- package/dist/shell/index.d.ts +50 -0
- package/dist/shell/index.d.ts.map +1 -0
- package/dist/shell/index.js +73 -0
- package/dist/shell/index.js.map +1 -0
- package/dist/shell/register.d.ts +11 -0
- package/dist/shell/register.d.ts.map +1 -0
- package/dist/shell/register.js +274 -0
- package/dist/shell/register.js.map +1 -0
- package/dist/shell/render/claim.d.ts +22 -0
- package/dist/shell/render/claim.d.ts.map +1 -0
- package/dist/shell/render/claim.js +75 -0
- package/dist/shell/render/claim.js.map +1 -0
- package/dist/shell/render/decision.d.ts +15 -0
- package/dist/shell/render/decision.d.ts.map +1 -0
- package/dist/shell/render/decision.js +66 -0
- package/dist/shell/render/decision.js.map +1 -0
- package/dist/shell/render/diagnostic.d.ts +19 -0
- package/dist/shell/render/diagnostic.d.ts.map +1 -0
- package/dist/shell/render/diagnostic.js +76 -0
- package/dist/shell/render/diagnostic.js.map +1 -0
- package/dist/shell/render/finding.d.ts +15 -0
- package/dist/shell/render/finding.d.ts.map +1 -0
- package/dist/shell/render/finding.js +57 -0
- package/dist/shell/render/finding.js.map +1 -0
- package/dist/shell/render/gates.d.ts +3 -0
- package/dist/shell/render/gates.d.ts.map +1 -0
- package/dist/shell/render/gates.js +56 -0
- package/dist/shell/render/gates.js.map +1 -0
- package/dist/shell/render/init.d.ts +11 -0
- package/dist/shell/render/init.d.ts.map +1 -0
- package/dist/shell/render/init.js +32 -0
- package/dist/shell/render/init.js.map +1 -0
- package/dist/shell/render/status.d.ts +26 -0
- package/dist/shell/render/status.d.ts.map +1 -0
- package/dist/shell/render/status.js +143 -0
- package/dist/shell/render/status.js.map +1 -0
- package/dist/shell/render/waiver.d.ts +21 -0
- package/dist/shell/render/waiver.d.ts.map +1 -0
- package/dist/shell/render/waiver.js +94 -0
- package/dist/shell/render/waiver.js.map +1 -0
- package/dist/shell/rules.d.ts +37 -0
- package/dist/shell/rules.d.ts.map +1 -0
- package/dist/shell/rules.js +51 -0
- package/dist/shell/rules.js.map +1 -0
- package/dist/shell/session/actor.d.ts +14 -0
- package/dist/shell/session/actor.d.ts.map +1 -0
- package/dist/shell/session/actor.js +34 -0
- package/dist/shell/session/actor.js.map +1 -0
- package/dist/shell/session/resolve-session.d.ts +5 -0
- package/dist/shell/session/resolve-session.d.ts.map +1 -0
- package/dist/shell/session/resolve-session.js +239 -0
- package/dist/shell/session/resolve-session.js.map +1 -0
- package/dist/shell/session/types.d.ts +56 -0
- package/dist/shell/session/types.d.ts.map +1 -0
- package/dist/shell/session/types.js +15 -0
- package/dist/shell/session/types.js.map +1 -0
- package/dist/store/agents-store.d.ts +3 -0
- package/dist/store/agents-store.d.ts.map +1 -0
- package/dist/store/agents-store.js +63 -0
- package/dist/store/agents-store.js.map +1 -0
- package/dist/store/apply-patch.d.ts +16 -0
- package/dist/store/apply-patch.d.ts.map +1 -0
- package/dist/store/apply-patch.js +191 -0
- package/dist/store/apply-patch.js.map +1 -0
- package/dist/store/atomic-write.d.ts +16 -0
- package/dist/store/atomic-write.d.ts.map +1 -0
- package/dist/store/atomic-write.js +132 -0
- package/dist/store/atomic-write.js.map +1 -0
- package/dist/store/doctor-snapshot.d.ts +20 -0
- package/dist/store/doctor-snapshot.d.ts.map +1 -0
- package/dist/store/doctor-snapshot.js +176 -0
- package/dist/store/doctor-snapshot.js.map +1 -0
- package/dist/store/events-store.d.ts +33 -0
- package/dist/store/events-store.d.ts.map +1 -0
- package/dist/store/events-store.js +297 -0
- package/dist/store/events-store.js.map +1 -0
- package/dist/store/index.d.ts +21 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +47 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/init-store.d.ts +21 -0
- package/dist/store/init-store.d.ts.map +1 -0
- package/dist/store/init-store.js +295 -0
- package/dist/store/init-store.js.map +1 -0
- package/dist/store/json-store.d.ts +3 -0
- package/dist/store/json-store.d.ts.map +1 -0
- package/dist/store/json-store.js +65 -0
- package/dist/store/json-store.js.map +1 -0
- package/dist/store/policy-store.d.ts +3 -0
- package/dist/store/policy-store.d.ts.map +1 -0
- package/dist/store/policy-store.js +65 -0
- package/dist/store/policy-store.js.map +1 -0
- package/dist/store/repo-root.d.ts +46 -0
- package/dist/store/repo-root.d.ts.map +1 -0
- package/dist/store/repo-root.js +145 -0
- package/dist/store/repo-root.js.map +1 -0
- package/dist/store/rules.d.ts +53 -0
- package/dist/store/rules.d.ts.map +1 -0
- package/dist/store/rules.js +78 -0
- package/dist/store/rules.js.map +1 -0
- package/dist/store/specs-store.d.ts +3 -0
- package/dist/store/specs-store.d.ts.map +1 -0
- package/dist/store/specs-store.js +131 -0
- package/dist/store/specs-store.js.map +1 -0
- package/dist/store/types.d.ts +84 -0
- package/dist/store/types.d.ts.map +1 -0
- package/dist/store/types.js +14 -0
- package/dist/store/types.js.map +1 -0
- package/dist/store/waivers-store.d.ts +25 -0
- package/dist/store/waivers-store.d.ts.map +1 -0
- package/dist/store/waivers-store.js +232 -0
- package/dist/store/waivers-store.js.map +1 -0
- package/dist/store/worktrees-store.d.ts +3 -0
- package/dist/store/worktrees-store.d.ts.map +1 -0
- package/dist/store/worktrees-store.js +62 -0
- package/dist/store/worktrees-store.js.map +1 -0
- package/dist/store/yaml-store.d.ts +9 -0
- package/dist/store/yaml-store.d.ts.map +1 -0
- package/dist/store/yaml-store.js +121 -0
- package/dist/store/yaml-store.js.map +1 -0
- package/package.json +15 -13
- package/dist/budget-derivation.js +0 -751
- package/dist/cicd-optimizer.js +0 -504
- package/dist/commands/archive.js +0 -500
- package/dist/commands/burnup.js +0 -198
- package/dist/commands/diagnose.js +0 -525
- package/dist/commands/evaluate.js +0 -314
- package/dist/commands/gates.js +0 -149
- package/dist/commands/init.js +0 -857
- package/dist/commands/iterate.js +0 -417
- package/dist/commands/mode.js +0 -269
- package/dist/commands/parallel.js +0 -242
- package/dist/commands/plan.js +0 -438
- package/dist/commands/provenance.js +0 -1143
- package/dist/commands/quality-monitor.js +0 -284
- package/dist/commands/scope.js +0 -264
- package/dist/commands/session.js +0 -312
- package/dist/commands/sidecar.js +0 -74
- package/dist/commands/specs.js +0 -1448
- package/dist/commands/status.js +0 -1151
- package/dist/commands/templates.js +0 -237
- package/dist/commands/tool.js +0 -136
- package/dist/commands/tutorial.js +0 -480
- package/dist/commands/validate.js +0 -357
- package/dist/commands/verify-acs.js +0 -443
- package/dist/commands/waivers.js +0 -599
- package/dist/commands/workflow.js +0 -243
- package/dist/commands/worktree.js +0 -386
- package/dist/config/lite-scope.js +0 -158
- package/dist/config/modes.js +0 -347
- package/dist/constants/spec-types.js +0 -65
- package/dist/gates/budget-limit.js +0 -121
- package/dist/gates/feedback.js +0 -260
- package/dist/gates/format.js +0 -179
- package/dist/gates/god-object.js +0 -117
- package/dist/gates/pipeline.js +0 -167
- package/dist/gates/scope-boundary.js +0 -93
- package/dist/gates/spec-completeness.js +0 -109
- package/dist/gates/todo-detection.js +0 -205
- package/dist/generators/jest-config-generator.js +0 -242
- package/dist/generators/working-spec.js +0 -237
- package/dist/minimal-cli.js +0 -88
- package/dist/parallel/parallel-manager.js +0 -433
- package/dist/policy/PolicyManager.js +0 -465
- package/dist/scaffold/claude-hooks.js +0 -443
- package/dist/scaffold/cursor-hooks.js +0 -177
- package/dist/scaffold/git-hooks.js +0 -928
- package/dist/scaffold/index.js +0 -794
- package/dist/session/session-manager.js +0 -653
- package/dist/sidecars/index.js +0 -33
- package/dist/sidecars/listeners.js +0 -40
- package/dist/sidecars/provenance-summary.js +0 -238
- package/dist/sidecars/quality-gaps.js +0 -258
- package/dist/sidecars/schema.js +0 -149
- package/dist/sidecars/spec-drift.js +0 -151
- package/dist/sidecars/waiver-draft.js +0 -176
- package/dist/spec/SpecFileManager.js +0 -419
- package/dist/templates/.caws/schemas/policy.schema.json +0 -112
- package/dist/templates/.caws/schemas/scope.schema.json +0 -52
- package/dist/templates/.caws/schemas/waivers.schema.json +0 -106
- package/dist/templates/.caws/schemas/working-spec.schema.json +0 -340
- package/dist/templates/.caws/schemas/worktrees.schema.json +0 -38
- package/dist/templates/.caws/templates/working-spec.template.yml +0 -80
- package/dist/templates/.caws/tools/README.md +0 -18
- package/dist/templates/.caws/tools/scope-guard.js +0 -203
- package/dist/templates/.caws/tools-allow.json +0 -331
- package/dist/templates/.caws/waivers.yml +0 -19
- package/dist/templates/.claude/README.md +0 -190
- package/dist/templates/.claude/hooks/audit.sh +0 -121
- package/dist/templates/.claude/hooks/block-dangerous.sh +0 -203
- package/dist/templates/.claude/hooks/classify_command.py +0 -592
- package/dist/templates/.claude/hooks/doc-frontmatter-check.sh +0 -173
- package/dist/templates/.claude/hooks/lite-sprawl-check.sh +0 -145
- package/dist/templates/.claude/hooks/naming-check.sh +0 -100
- package/dist/templates/.claude/hooks/protected-paths.sh +0 -39
- package/dist/templates/.claude/hooks/quality-check.sh +0 -81
- package/dist/templates/.claude/hooks/scan-secrets.sh +0 -85
- package/dist/templates/.claude/hooks/scope-guard.sh +0 -381
- package/dist/templates/.claude/hooks/session-caws-status.sh +0 -117
- package/dist/templates/.claude/hooks/session-log.sh +0 -634
- package/dist/templates/.claude/hooks/simplification-guard.sh +0 -92
- package/dist/templates/.claude/hooks/stop-worktree-check.sh +0 -46
- package/dist/templates/.claude/hooks/test_classify_command.py +0 -370
- package/dist/templates/.claude/hooks/test_wrapper_smoke.sh +0 -96
- package/dist/templates/.claude/hooks/validate-spec.sh +0 -76
- package/dist/templates/.claude/hooks/worktree-guard.sh +0 -220
- package/dist/templates/.claude/hooks/worktree-write-guard.sh +0 -190
- package/dist/templates/.claude/rules/git-safety.md +0 -26
- package/dist/templates/.claude/rules/worktree-isolation.md +0 -83
- package/dist/templates/.claude/settings.json +0 -141
- package/dist/templates/.cursor/README.md +0 -299
- package/dist/templates/.cursor/hooks/audit.sh +0 -55
- package/dist/templates/.cursor/hooks/block-dangerous.sh +0 -84
- package/dist/templates/.cursor/hooks/caws-quality-check.sh +0 -52
- package/dist/templates/.cursor/hooks/caws-scope-guard.sh +0 -130
- package/dist/templates/.cursor/hooks/format.sh +0 -38
- package/dist/templates/.cursor/hooks/naming-check.sh +0 -64
- package/dist/templates/.cursor/hooks/scan-secrets.sh +0 -51
- package/dist/templates/.cursor/hooks/scope-guard.sh +0 -52
- package/dist/templates/.cursor/hooks/session-log.sh +0 -924
- package/dist/templates/.cursor/hooks/validate-spec.sh +0 -83
- package/dist/templates/.cursor/hooks.json +0 -76
- package/dist/templates/.cursor/rules/00-claims-verification.mdc +0 -144
- package/dist/templates/.cursor/rules/01-working-style.mdc +0 -50
- package/dist/templates/.cursor/rules/02-quality-gates.mdc +0 -368
- package/dist/templates/.cursor/rules/03-naming-and-refactor.mdc +0 -33
- package/dist/templates/.cursor/rules/04-logging-language-style.mdc +0 -23
- package/dist/templates/.cursor/rules/05-safe-defaults-guards.mdc +0 -23
- package/dist/templates/.cursor/rules/06-typescript-conventions.mdc +0 -36
- package/dist/templates/.cursor/rules/07-process-ops.mdc +0 -20
- package/dist/templates/.cursor/rules/08-solid-and-architecture.mdc +0 -16
- package/dist/templates/.cursor/rules/09-docstrings.mdc +0 -89
- package/dist/templates/.cursor/rules/10-documentation-quality-standards.mdc +0 -385
- package/dist/templates/.cursor/rules/11-scope-management-waivers.mdc +0 -381
- package/dist/templates/.cursor/rules/12-implementation-completeness.mdc +0 -516
- package/dist/templates/.cursor/rules/13-language-agnostic-standards.mdc +0 -578
- package/dist/templates/.cursor/rules/README.md +0 -148
- package/dist/templates/.github/copilot-instructions.md +0 -82
- package/dist/templates/.idea/runConfigurations/CAWS_Evaluate.xml +0 -5
- package/dist/templates/.idea/runConfigurations/CAWS_Validate.xml +0 -5
- package/dist/templates/.junie/guidelines.md +0 -73
- package/dist/templates/.vscode/launch.json +0 -17
- package/dist/templates/.vscode/settings.json +0 -95
- package/dist/templates/.windsurf/rules/caws-quality-standards.md +0 -54
- package/dist/templates/.windsurf/workflows/caws-guided-development.md +0 -92
- package/dist/templates/CLAUDE.md +0 -174
- package/dist/templates/COMMIT_CONVENTIONS.md +0 -86
- package/dist/templates/OIDC_SETUP.md +0 -300
- package/dist/templates/agents.md +0 -145
- package/dist/templates/codemod/README.md +0 -1
- package/dist/templates/codemod/test.js +0 -93
- package/dist/templates/docs/README.md +0 -151
- package/dist/templates/scripts/new_feature.sh +0 -80
- package/dist/templates/scripts/quality-gates/check-god-objects.js +0 -146
- package/dist/templates/scripts/quality-gates/run-quality-gates.js +0 -50
- package/dist/templates/scripts/v3/analysis/todo_analyzer.py +0 -1997
- package/dist/test-analysis.js +0 -786
- package/dist/tool-interface.js +0 -314
- package/dist/tool-loader.js +0 -303
- package/dist/tool-validator.js +0 -393
- package/dist/utils/agent-session.js +0 -202
- package/dist/utils/async-utils.js +0 -188
- package/dist/utils/command-wrapper.js +0 -200
- package/dist/utils/event-log.js +0 -584
- package/dist/utils/event-renderer.js +0 -521
- package/dist/utils/finalization.js +0 -230
- package/dist/utils/git-lock.js +0 -119
- package/dist/utils/gitignore-updater.js +0 -158
- package/dist/utils/ide-detection.js +0 -133
- package/dist/utils/lifecycle-events.js +0 -94
- package/dist/utils/project-analysis.js +0 -367
- package/dist/utils/promise-utils.js +0 -72
- package/dist/utils/quality-gates-errors.js +0 -520
- package/dist/utils/quality-gates-utils.js +0 -387
- package/dist/utils/schema-validator.js +0 -50
- package/dist/utils/spec-resolver.js +0 -711
- package/dist/utils/typescript-detector.js +0 -369
- package/dist/utils/working-state.js +0 -530
- package/dist/utils/yaml-validation.js +0 -156
- package/dist/validation/spec-validation.js +0 -921
- package/dist/waivers-manager.js +0 -732
- package/dist/worktree/worktree-manager.js +0 -1374
- package/templates/.caws/schemas/policy.schema.json +0 -112
- package/templates/.caws/schemas/scope.schema.json +0 -52
- package/templates/.caws/schemas/waivers.schema.json +0 -106
- package/templates/.caws/schemas/working-spec.schema.json +0 -340
- package/templates/.caws/schemas/worktrees.schema.json +0 -38
- package/templates/.caws/templates/working-spec.template.yml +0 -80
- package/templates/.caws/tools/README.md +0 -18
- package/templates/.caws/tools/scope-guard.js +0 -203
- package/templates/.caws/tools-allow.json +0 -331
- package/templates/.caws/waivers.yml +0 -19
- package/templates/.claude/README.md +0 -190
- package/templates/.claude/hooks/audit.sh +0 -121
- package/templates/.claude/hooks/block-dangerous.sh +0 -203
- package/templates/.claude/hooks/classify_command.py +0 -592
- package/templates/.claude/hooks/doc-frontmatter-check.sh +0 -173
- package/templates/.claude/hooks/lite-sprawl-check.sh +0 -145
- package/templates/.claude/hooks/naming-check.sh +0 -100
- package/templates/.claude/hooks/protected-paths.sh +0 -39
- package/templates/.claude/hooks/quality-check.sh +0 -81
- package/templates/.claude/hooks/scan-secrets.sh +0 -85
- package/templates/.claude/hooks/scope-guard.sh +0 -381
- package/templates/.claude/hooks/session-caws-status.sh +0 -117
- package/templates/.claude/hooks/session-log.sh +0 -634
- package/templates/.claude/hooks/simplification-guard.sh +0 -92
- package/templates/.claude/hooks/stop-worktree-check.sh +0 -46
- package/templates/.claude/hooks/test_classify_command.py +0 -370
- package/templates/.claude/hooks/test_wrapper_smoke.sh +0 -96
- package/templates/.claude/hooks/validate-spec.sh +0 -76
- package/templates/.claude/hooks/worktree-guard.sh +0 -220
- package/templates/.claude/hooks/worktree-write-guard.sh +0 -190
- package/templates/.claude/rules/git-safety.md +0 -26
- package/templates/.claude/rules/worktree-isolation.md +0 -83
- package/templates/.claude/settings.json +0 -141
- package/templates/.cursor/README.md +0 -299
- package/templates/.cursor/hooks/audit.sh +0 -55
- package/templates/.cursor/hooks/block-dangerous.sh +0 -84
- package/templates/.cursor/hooks/caws-quality-check.sh +0 -52
- package/templates/.cursor/hooks/caws-scope-guard.sh +0 -130
- package/templates/.cursor/hooks/format.sh +0 -38
- package/templates/.cursor/hooks/naming-check.sh +0 -64
- package/templates/.cursor/hooks/scan-secrets.sh +0 -51
- package/templates/.cursor/hooks/scope-guard.sh +0 -52
- package/templates/.cursor/hooks/session-log.sh +0 -924
- package/templates/.cursor/hooks/validate-spec.sh +0 -83
- package/templates/.cursor/hooks.json +0 -76
- package/templates/.cursor/rules/00-claims-verification.mdc +0 -144
- package/templates/.cursor/rules/01-working-style.mdc +0 -50
- package/templates/.cursor/rules/02-quality-gates.mdc +0 -368
- package/templates/.cursor/rules/03-naming-and-refactor.mdc +0 -33
- package/templates/.cursor/rules/04-logging-language-style.mdc +0 -23
- package/templates/.cursor/rules/05-safe-defaults-guards.mdc +0 -23
- package/templates/.cursor/rules/06-typescript-conventions.mdc +0 -36
- package/templates/.cursor/rules/07-process-ops.mdc +0 -20
- package/templates/.cursor/rules/08-solid-and-architecture.mdc +0 -16
- package/templates/.cursor/rules/09-docstrings.mdc +0 -89
- package/templates/.cursor/rules/10-documentation-quality-standards.mdc +0 -385
- package/templates/.cursor/rules/11-scope-management-waivers.mdc +0 -381
- package/templates/.cursor/rules/12-implementation-completeness.mdc +0 -516
- package/templates/.cursor/rules/13-language-agnostic-standards.mdc +0 -578
- package/templates/.cursor/rules/README.md +0 -148
- package/templates/.github/copilot-instructions.md +0 -82
- package/templates/.idea/runConfigurations/CAWS_Evaluate.xml +0 -5
- package/templates/.idea/runConfigurations/CAWS_Validate.xml +0 -5
- package/templates/.junie/guidelines.md +0 -73
- package/templates/.vscode/launch.json +0 -17
- package/templates/.vscode/settings.json +0 -95
- package/templates/.windsurf/rules/caws-quality-standards.md +0 -54
- package/templates/.windsurf/workflows/caws-guided-development.md +0 -92
- package/templates/CLAUDE.md +0 -174
- package/templates/COMMIT_CONVENTIONS.md +0 -86
- package/templates/OIDC_SETUP.md +0 -300
- package/templates/agents.md +0 -145
- package/templates/codemod/README.md +0 -1
- package/templates/codemod/test.js +0 -93
- package/templates/docs/README.md +0 -151
- package/templates/scripts/new_feature.sh +0 -80
- package/templates/scripts/quality-gates/check-god-objects.js +0 -146
- package/templates/scripts/quality-gates/run-quality-gates.js +0 -50
- package/templates/scripts/v3/analysis/todo_analyzer.py +0 -1997
package/dist/commands/specs.js
DELETED
|
@@ -1,1448 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview CAWS Specs Command
|
|
3
|
-
* Manage multiple spec files for better organization and discoverability
|
|
4
|
-
* @author @darianrosebrook
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const fs = require('fs-extra');
|
|
8
|
-
const path = require('path');
|
|
9
|
-
const yaml = require('js-yaml');
|
|
10
|
-
const chalk = require('chalk');
|
|
11
|
-
const { safeAsync, outputResult } = require('../error-handler');
|
|
12
|
-
const { question, closeReadline } = require('../utils/promise-utils');
|
|
13
|
-
const { SPEC_TYPES } = require('../constants/spec-types');
|
|
14
|
-
|
|
15
|
-
// Import suggestFeatureBreakdown from spec-resolver
|
|
16
|
-
const { suggestFeatureBreakdown } = require('../utils/spec-resolver');
|
|
17
|
-
const { findProjectRoot } = require('../utils/detection');
|
|
18
|
-
const { loadRegistry: loadWorktreeRegistry, getRepoRoot } = require('../worktree/worktree-manager');
|
|
19
|
-
const { getAgentSessionId } = require('../utils/agent-session');
|
|
20
|
-
const { initializeState, saveState, deleteState } = require('../utils/working-state');
|
|
21
|
-
const { appendEvent } = require('../utils/event-log');
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Check if a spec is referenced by any active worktree.
|
|
25
|
-
* Returns the list of worktree names that reference it, or empty array.
|
|
26
|
-
* @param {string} specId - Spec identifier to check
|
|
27
|
-
* @returns {string[]} Names of worktrees referencing this spec
|
|
28
|
-
*/
|
|
29
|
-
function getWorktreesReferencingSpec(specId) {
|
|
30
|
-
try {
|
|
31
|
-
const root = getRepoRoot();
|
|
32
|
-
const registry = loadWorktreeRegistry(root);
|
|
33
|
-
const matches = [];
|
|
34
|
-
for (const [name, entry] of Object.entries(registry.worktrees || {})) {
|
|
35
|
-
if (
|
|
36
|
-
entry.specId === specId &&
|
|
37
|
-
entry.status !== 'destroyed' &&
|
|
38
|
-
entry.status !== 'merged'
|
|
39
|
-
) {
|
|
40
|
-
matches.push(name);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return matches;
|
|
44
|
-
} catch {
|
|
45
|
-
// If worktree registry can't be loaded (e.g., no .caws dir), no conflict
|
|
46
|
-
return [];
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Specs directory structure — anchored to the CAWS project root,
|
|
52
|
-
* not process.cwd(), so the CLI works from subdirectories and monorepos.
|
|
53
|
-
*/
|
|
54
|
-
function getSpecsDir() {
|
|
55
|
-
return path.join(findProjectRoot(), '.caws', 'specs');
|
|
56
|
-
}
|
|
57
|
-
function getSpecsRegistry() {
|
|
58
|
-
return path.join(findProjectRoot(), '.caws', 'specs', 'registry.json');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function detectCurrentWorktreeName() {
|
|
62
|
-
const cwd = process.cwd().replace(/\\/g, '/');
|
|
63
|
-
const worktreeMatch = cwd.match(/\/\.caws\/worktrees\/([^/]+)(?:\/|$)/);
|
|
64
|
-
if (worktreeMatch) {
|
|
65
|
-
return worktreeMatch[1];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
const root = getRepoRoot();
|
|
70
|
-
const branch = require('child_process')
|
|
71
|
-
.execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
72
|
-
cwd: root,
|
|
73
|
-
encoding: 'utf8',
|
|
74
|
-
stdio: 'pipe',
|
|
75
|
-
})
|
|
76
|
-
.trim();
|
|
77
|
-
const registry = loadWorktreeRegistry(root);
|
|
78
|
-
for (const [name, entry] of Object.entries(registry.worktrees || {})) {
|
|
79
|
-
if (entry.branch === branch && entry.status !== 'destroyed' && entry.status !== 'merged') {
|
|
80
|
-
return name;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
} catch {
|
|
84
|
-
// Best-effort only; specs can still be created outside a worktree.
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
// Legacy constants kept for backward compatibility in tests
|
|
90
|
-
const SPECS_DIR = '.caws/specs';
|
|
91
|
-
const SPECS_REGISTRY = '.caws/specs/registry.json';
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Load specs registry
|
|
95
|
-
* @returns {Promise<Object>} Registry data
|
|
96
|
-
*/
|
|
97
|
-
async function loadSpecsRegistry() {
|
|
98
|
-
try {
|
|
99
|
-
const registryPath = getSpecsRegistry();
|
|
100
|
-
if (!(await fs.pathExists(registryPath))) {
|
|
101
|
-
return {
|
|
102
|
-
version: '1.0.0',
|
|
103
|
-
specs: {},
|
|
104
|
-
lastUpdated: new Date().toISOString(),
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const registry = JSON.parse(await fs.readFile(registryPath, 'utf8'));
|
|
109
|
-
return registry;
|
|
110
|
-
} catch (error) {
|
|
111
|
-
return {
|
|
112
|
-
version: '1.0.0',
|
|
113
|
-
specs: {},
|
|
114
|
-
lastUpdated: new Date().toISOString(),
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Save specs registry
|
|
121
|
-
* @param {Object} registry - Registry data
|
|
122
|
-
* @returns {Promise<void>}
|
|
123
|
-
*/
|
|
124
|
-
async function saveSpecsRegistry(registry) {
|
|
125
|
-
const registryPath = getSpecsRegistry();
|
|
126
|
-
await fs.ensureDir(path.dirname(registryPath));
|
|
127
|
-
registry.lastUpdated = new Date().toISOString();
|
|
128
|
-
await fs.writeFile(registryPath, JSON.stringify(registry, null, 2));
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Read and validate a spec YAML file that was just written.
|
|
133
|
-
* This catches malformed YAML and duplicate keys before registry sync.
|
|
134
|
-
* @param {string} filePath - Absolute path to the spec file
|
|
135
|
-
* @returns {Promise<Object>} Parsed spec object
|
|
136
|
-
*/
|
|
137
|
-
async function validateAndReadSpecFile(filePath) {
|
|
138
|
-
const writtenContent = await fs.readFile(filePath, 'utf8');
|
|
139
|
-
const parsed = yaml.load(writtenContent);
|
|
140
|
-
|
|
141
|
-
if (!parsed || typeof parsed !== 'object') {
|
|
142
|
-
throw new Error('Failed to parse written spec file - invalid YAML structure');
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const { validateWorkingSpec } = require('../validation/spec-validation');
|
|
146
|
-
const validation = validateWorkingSpec(parsed);
|
|
147
|
-
|
|
148
|
-
if (!validation.valid) {
|
|
149
|
-
const errorMessages = validation.errors
|
|
150
|
-
.map((e) => `${e.instancePath}: ${e.message}`)
|
|
151
|
-
.join('; ');
|
|
152
|
-
throw new Error(`Spec validation failed: ${errorMessages}`);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return parsed;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Build the registry entry from the parsed spec content instead of caller assumptions.
|
|
160
|
-
* @param {Object} spec - Parsed spec object
|
|
161
|
-
* @param {string} fileName - Registry path for the spec
|
|
162
|
-
* @param {string|null} owner - Session owner for the registry entry
|
|
163
|
-
* @returns {Object} Registry entry
|
|
164
|
-
*/
|
|
165
|
-
function buildRegistryEntryFromSpec(spec, fileName, owner = null) {
|
|
166
|
-
return {
|
|
167
|
-
path: fileName,
|
|
168
|
-
type: spec.type || 'feature',
|
|
169
|
-
status: spec.status || 'draft',
|
|
170
|
-
created_at: spec.created_at || new Date().toISOString(),
|
|
171
|
-
updated_at: spec.updated_at || new Date().toISOString(),
|
|
172
|
-
owner,
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Backfill legacy sparse specs so write-time validation can succeed when
|
|
178
|
-
* update/merge flows touch older files created before the stricter schema.
|
|
179
|
-
* @param {Object} spec - Spec content to normalize
|
|
180
|
-
* @returns {Object} Normalized spec content
|
|
181
|
-
*/
|
|
182
|
-
function normalizeSpecForValidation(spec = {}) {
|
|
183
|
-
const normalizedRiskTier =
|
|
184
|
-
typeof spec.risk_tier === 'string'
|
|
185
|
-
? parseInt(spec.risk_tier.replace(/^T/i, ''), 10) || 3
|
|
186
|
-
: spec.risk_tier || 3;
|
|
187
|
-
|
|
188
|
-
const acceptanceVal = Array.isArray(spec.acceptance)
|
|
189
|
-
? spec.acceptance
|
|
190
|
-
: Array.isArray(spec.acceptance_criteria)
|
|
191
|
-
? spec.acceptance_criteria
|
|
192
|
-
: [];
|
|
193
|
-
|
|
194
|
-
const defaults = {
|
|
195
|
-
type: 'feature',
|
|
196
|
-
status: 'draft',
|
|
197
|
-
risk_tier: normalizedRiskTier,
|
|
198
|
-
mode: 'standard',
|
|
199
|
-
blast_radius: { modules: [], data_migration: false },
|
|
200
|
-
operational_rollback_slo: '5m',
|
|
201
|
-
scope: { in: ['src/', 'tests/'], out: ['node_modules/', 'dist/', 'build/'] },
|
|
202
|
-
invariants: ['System maintains data consistency'],
|
|
203
|
-
acceptance: [],
|
|
204
|
-
acceptance_criteria: [],
|
|
205
|
-
non_functional: { a11y: [], perf: {}, security: [] },
|
|
206
|
-
contracts: [],
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
return {
|
|
210
|
-
...defaults,
|
|
211
|
-
...spec,
|
|
212
|
-
risk_tier: normalizedRiskTier,
|
|
213
|
-
blast_radius: { ...defaults.blast_radius, ...(spec.blast_radius || {}) },
|
|
214
|
-
scope: { ...defaults.scope, ...(spec.scope || {}) },
|
|
215
|
-
non_functional: { ...defaults.non_functional, ...(spec.non_functional || {}) },
|
|
216
|
-
acceptance: acceptanceVal,
|
|
217
|
-
acceptance_criteria: Array.isArray(spec.acceptance_criteria)
|
|
218
|
-
? spec.acceptance_criteria
|
|
219
|
-
: acceptanceVal,
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* List all spec files in the specs directory
|
|
225
|
-
* @returns {Promise<Array>} Array of spec file info
|
|
226
|
-
*/
|
|
227
|
-
async function listSpecFiles() {
|
|
228
|
-
const specsDir = getSpecsDir();
|
|
229
|
-
if (!(await fs.pathExists(specsDir))) {
|
|
230
|
-
return [];
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const files = await fs.readdir(specsDir, { recursive: true });
|
|
234
|
-
const yamlFiles = files.filter((file) => file.endsWith('.yaml') || file.endsWith('.yml'));
|
|
235
|
-
|
|
236
|
-
const specs = [];
|
|
237
|
-
for (const file of yamlFiles) {
|
|
238
|
-
const filePath = path.join(specsDir, file);
|
|
239
|
-
try {
|
|
240
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
241
|
-
const spec = yaml.load(content);
|
|
242
|
-
|
|
243
|
-
specs.push({
|
|
244
|
-
id: spec.id || path.basename(file, path.extname(file)),
|
|
245
|
-
path: file,
|
|
246
|
-
type: spec.type || 'feature',
|
|
247
|
-
title: spec.title || 'Untitled',
|
|
248
|
-
status: spec.status || 'draft',
|
|
249
|
-
risk_tier: spec.risk_tier || 'T3',
|
|
250
|
-
mode: spec.mode || 'development',
|
|
251
|
-
created_at: spec.created_at || new Date().toISOString(),
|
|
252
|
-
updated_at: spec.updated_at || new Date().toISOString(),
|
|
253
|
-
});
|
|
254
|
-
} catch (error) {
|
|
255
|
-
// Skip invalid spec files
|
|
256
|
-
console.warn(`Warning: Could not parse spec file ${file}: ${error.message}`);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return specs;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Create a new spec file
|
|
265
|
-
* @param {string} id - Spec identifier
|
|
266
|
-
* @param {Object} options - Creation options
|
|
267
|
-
* @returns {Promise<Object>} Created spec info
|
|
268
|
-
*/
|
|
269
|
-
async function createSpec(id, options = {}) {
|
|
270
|
-
const {
|
|
271
|
-
type = 'feature',
|
|
272
|
-
title = `New ${type}`,
|
|
273
|
-
risk_tier = 3, // Default to numeric 3 (low-risk)
|
|
274
|
-
mode = 'development',
|
|
275
|
-
template = null,
|
|
276
|
-
force = false, // Override existing specs
|
|
277
|
-
interactive = false, // Ask for confirmation on conflicts
|
|
278
|
-
} = options;
|
|
279
|
-
|
|
280
|
-
// Convert string tiers to numeric (handle both 'T3' and 3)
|
|
281
|
-
let numericRiskTier = risk_tier;
|
|
282
|
-
if (typeof risk_tier === 'string') {
|
|
283
|
-
const tierMap = { T1: 1, T2: 2, T3: 3 };
|
|
284
|
-
numericRiskTier = tierMap[risk_tier] || 3; // Default to 3 if invalid
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Check for existing spec
|
|
288
|
-
const specsDir = getSpecsDir();
|
|
289
|
-
const existingSpecPath = path.join(specsDir, `${id}.yaml`);
|
|
290
|
-
const specExists = await fs.pathExists(existingSpecPath);
|
|
291
|
-
|
|
292
|
-
// Handle conflict resolution
|
|
293
|
-
let answer = null;
|
|
294
|
-
|
|
295
|
-
if (specExists && !force) {
|
|
296
|
-
if (interactive) {
|
|
297
|
-
console.log(chalk.yellow(`Spec '${id}' already exists.`));
|
|
298
|
-
console.log(chalk.gray(` Path: ${existingSpecPath}`));
|
|
299
|
-
|
|
300
|
-
// Load existing spec to show details
|
|
301
|
-
try {
|
|
302
|
-
const existingContent = await fs.readFile(existingSpecPath, 'utf8');
|
|
303
|
-
const existingSpec = yaml.load(existingContent);
|
|
304
|
-
console.log(chalk.gray(` Title: ${existingSpec.title || 'Untitled'}`));
|
|
305
|
-
console.log(chalk.gray(` Status: ${existingSpec.status || 'draft'}`));
|
|
306
|
-
console.log(
|
|
307
|
-
chalk.gray(
|
|
308
|
-
` Created: ${new Date(existingSpec.created_at || Date.now()).toLocaleDateString()}`
|
|
309
|
-
)
|
|
310
|
-
);
|
|
311
|
-
} catch (error) {
|
|
312
|
-
console.log(chalk.gray(` (Could not load existing spec details)`));
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Ask for conflict resolution
|
|
316
|
-
answer = await askConflictResolution();
|
|
317
|
-
|
|
318
|
-
if (answer === 'cancel') {
|
|
319
|
-
console.log(chalk.blue('Spec creation canceled.'));
|
|
320
|
-
return null;
|
|
321
|
-
} else if (answer === 'rename') {
|
|
322
|
-
// Generate new name with valid PREFIX-NUMBER format
|
|
323
|
-
// Extract prefix from existing ID or use default
|
|
324
|
-
const prefixMatch = id.match(/^([A-Z]+)-\d+$/);
|
|
325
|
-
const prefix = prefixMatch ? prefixMatch[1] : 'FEAT';
|
|
326
|
-
// Generate sequential number based on timestamp
|
|
327
|
-
const number = Date.now().toString().slice(-6); // Last 6 digits of timestamp
|
|
328
|
-
const newId = `${prefix}-${number}`;
|
|
329
|
-
console.log(chalk.blue(`Creating spec with new name: ${newId}`));
|
|
330
|
-
return await createSpec(newId, { ...options, interactive: false });
|
|
331
|
-
} else if (answer === 'merge') {
|
|
332
|
-
// Merge new spec data with existing spec
|
|
333
|
-
console.log(chalk.blue('Merging with existing spec...'));
|
|
334
|
-
return await mergeSpec(id, options);
|
|
335
|
-
} else if (answer === 'override') {
|
|
336
|
-
console.log(chalk.yellow('Overriding existing spec...'));
|
|
337
|
-
}
|
|
338
|
-
} else {
|
|
339
|
-
console.error(chalk.red(`Spec '${id}' already exists.`));
|
|
340
|
-
console.error(
|
|
341
|
-
chalk.yellow('Use --force to override, or --interactive for conflict resolution.')
|
|
342
|
-
);
|
|
343
|
-
throw new Error(`Spec '${id}' already exists. Use --force to override.`);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// If we got here via override choice, check ownership and worktree associations
|
|
348
|
-
if (specExists && (force || answer === 'override')) {
|
|
349
|
-
// Check session ownership — only the creator session can override
|
|
350
|
-
const registry = await loadSpecsRegistry();
|
|
351
|
-
const existingEntry = registry.specs[id];
|
|
352
|
-
const currentSession = getAgentSessionId(findProjectRoot());
|
|
353
|
-
if (existingEntry?.owner && currentSession && existingEntry.owner !== currentSession) {
|
|
354
|
-
throw new Error(
|
|
355
|
-
`Cannot override spec '${id}': owned by another session (${existingEntry.owner}). ` +
|
|
356
|
-
`Only the creator session can override a spec. Create a new spec with a different ID instead.`
|
|
357
|
-
);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Check for active worktree associations
|
|
361
|
-
const referencingWorktrees = getWorktreesReferencingSpec(id);
|
|
362
|
-
if (referencingWorktrees.length > 0) {
|
|
363
|
-
const names = referencingWorktrees.join(', ');
|
|
364
|
-
throw new Error(
|
|
365
|
-
`Cannot override spec '${id}': active worktree(s) [${names}] reference it. ` +
|
|
366
|
-
`Destroy the worktree(s) first with 'caws worktree destroy <name>', or create a new spec with a different ID.`
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
console.log(chalk.yellow('Overriding existing spec...'));
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// Ensure specs directory exists
|
|
373
|
-
await fs.ensureDir(specsDir);
|
|
374
|
-
|
|
375
|
-
// Generate spec content with all required fields
|
|
376
|
-
// Merge template carefully to preserve required fields and structure
|
|
377
|
-
const defaultSpec = {
|
|
378
|
-
id, // Always use the provided id parameter
|
|
379
|
-
type,
|
|
380
|
-
title,
|
|
381
|
-
status: 'draft',
|
|
382
|
-
risk_tier: numericRiskTier,
|
|
383
|
-
mode,
|
|
384
|
-
created_at: new Date().toISOString(),
|
|
385
|
-
updated_at: new Date().toISOString(),
|
|
386
|
-
// Required fields for validation
|
|
387
|
-
blast_radius: {
|
|
388
|
-
modules: [],
|
|
389
|
-
data_migration: false,
|
|
390
|
-
},
|
|
391
|
-
operational_rollback_slo: '5m',
|
|
392
|
-
scope: {
|
|
393
|
-
in: ['src/', 'tests/'],
|
|
394
|
-
out: ['node_modules/', 'dist/', 'build/'],
|
|
395
|
-
},
|
|
396
|
-
invariants: ['System maintains data consistency'],
|
|
397
|
-
acceptance: [], // Note: validation expects 'acceptance', not 'acceptance_criteria'
|
|
398
|
-
acceptance_criteria: [], // Keep for backward compatibility
|
|
399
|
-
non_functional: {
|
|
400
|
-
a11y: [],
|
|
401
|
-
perf: {},
|
|
402
|
-
security: [],
|
|
403
|
-
},
|
|
404
|
-
contracts: [],
|
|
405
|
-
};
|
|
406
|
-
|
|
407
|
-
const detectedWorktree = detectCurrentWorktreeName();
|
|
408
|
-
if (detectedWorktree) {
|
|
409
|
-
defaultSpec.worktree = detectedWorktree;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// Merge template, but preserve required structure
|
|
413
|
-
// Map template.criteria to acceptance if present
|
|
414
|
-
const templateAcceptance = template?.criteria || template?.acceptance;
|
|
415
|
-
|
|
416
|
-
const specContent = {
|
|
417
|
-
...defaultSpec,
|
|
418
|
-
...(template || {}),
|
|
419
|
-
// Always preserve these critical fields
|
|
420
|
-
id, // Never allow template to override id
|
|
421
|
-
// Map criteria to acceptance if template uses criteria
|
|
422
|
-
acceptance: templateAcceptance || defaultSpec.acceptance,
|
|
423
|
-
acceptance_criteria: templateAcceptance || defaultSpec.acceptance_criteria,
|
|
424
|
-
// Deep merge scope if template provides it
|
|
425
|
-
scope: template?.scope
|
|
426
|
-
? {
|
|
427
|
-
in: template.scope.in || defaultSpec.scope.in,
|
|
428
|
-
out: template.scope.out || defaultSpec.scope.out,
|
|
429
|
-
}
|
|
430
|
-
: defaultSpec.scope,
|
|
431
|
-
// Deep merge blast_radius if template provides it
|
|
432
|
-
blast_radius: template?.blast_radius
|
|
433
|
-
? {
|
|
434
|
-
modules: template.blast_radius.modules || defaultSpec.blast_radius.modules,
|
|
435
|
-
data_migration:
|
|
436
|
-
template.blast_radius.data_migration !== undefined
|
|
437
|
-
? template.blast_radius.data_migration
|
|
438
|
-
: defaultSpec.blast_radius.data_migration,
|
|
439
|
-
}
|
|
440
|
-
: defaultSpec.blast_radius,
|
|
441
|
-
// Deep merge non_functional if template provides it
|
|
442
|
-
non_functional: template?.non_functional
|
|
443
|
-
? {
|
|
444
|
-
a11y: template.non_functional.a11y || defaultSpec.non_functional.a11y,
|
|
445
|
-
perf: template.non_functional.perf || defaultSpec.non_functional.perf,
|
|
446
|
-
security: template.non_functional.security || defaultSpec.non_functional.security,
|
|
447
|
-
}
|
|
448
|
-
: defaultSpec.non_functional,
|
|
449
|
-
};
|
|
450
|
-
|
|
451
|
-
// Create file path
|
|
452
|
-
const fileName = `${id}.yaml`;
|
|
453
|
-
const filePath = path.join(specsDir, fileName);
|
|
454
|
-
|
|
455
|
-
// Write spec file
|
|
456
|
-
const yamlContent = yaml.dump(specContent, { indent: 2 });
|
|
457
|
-
await fs.writeFile(filePath, yamlContent);
|
|
458
|
-
|
|
459
|
-
// Validate written file (YAML syntax and structure)
|
|
460
|
-
let parsedSpec;
|
|
461
|
-
try {
|
|
462
|
-
parsedSpec = await validateAndReadSpecFile(filePath);
|
|
463
|
-
} catch (error) {
|
|
464
|
-
// Clean up invalid file if it exists
|
|
465
|
-
if (await fs.pathExists(filePath)) {
|
|
466
|
-
await fs.remove(filePath);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// Re-throw with helpful message
|
|
470
|
-
if (error.message.includes('YAMLException') || error.message.includes('yaml')) {
|
|
471
|
-
throw new Error(
|
|
472
|
-
`Failed to create valid spec: YAML syntax error. ${error.message}\n` +
|
|
473
|
-
'Consider using the interactive mode: caws specs create <id> --interactive'
|
|
474
|
-
);
|
|
475
|
-
}
|
|
476
|
-
throw error;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// Update registry
|
|
480
|
-
const registry = await loadSpecsRegistry();
|
|
481
|
-
registry.specs[id] = buildRegistryEntryFromSpec(
|
|
482
|
-
parsedSpec,
|
|
483
|
-
fileName,
|
|
484
|
-
getAgentSessionId(findProjectRoot())
|
|
485
|
-
);
|
|
486
|
-
await saveSpecsRegistry(registry);
|
|
487
|
-
|
|
488
|
-
// Initialize working state for new spec
|
|
489
|
-
try {
|
|
490
|
-
const initialState = initializeState(id);
|
|
491
|
-
saveState(id, initialState, findProjectRoot());
|
|
492
|
-
} catch { /* non-fatal */ }
|
|
493
|
-
|
|
494
|
-
// CAWSFIX-06: warn when a feature spec is created without contracts.
|
|
495
|
-
// Contract-first development is a CAWS value proposition; empty `contracts`
|
|
496
|
-
// on a feature-type spec is discouraged but not fatal. Emit a non-fatal
|
|
497
|
-
// warning to stderr so agents and humans notice and can update the spec.
|
|
498
|
-
//
|
|
499
|
-
// Note: the spec's acceptance text uses "mode=feature" colloquially, but in
|
|
500
|
-
// CAWS the discriminator is the `type` field (feature/fix/refactor/chore),
|
|
501
|
-
// not the `mode` field (development/pilot/etc.). We key off `type` to match
|
|
502
|
-
// the --type CLI flag and the schema.
|
|
503
|
-
const specType = parsedSpec.type || type;
|
|
504
|
-
const specContracts = Array.isArray(parsedSpec.contracts) ? parsedSpec.contracts : [];
|
|
505
|
-
if (specType === 'feature' && specContracts.length === 0) {
|
|
506
|
-
console.warn(
|
|
507
|
-
chalk.yellow(
|
|
508
|
-
`⚠ Spec ${id} has mode=feature but no contracts. ` +
|
|
509
|
-
`mode=feature without contracts is discouraged — ` +
|
|
510
|
-
`run 'caws specs update ${id}' to add a contract reference.`
|
|
511
|
-
)
|
|
512
|
-
);
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// EVLOG-001: emit spec_created event alongside state write.
|
|
516
|
-
//
|
|
517
|
-
// Spec-lifecycle events (spec_created / spec_closed / spec_deleted) are
|
|
518
|
-
// **informational redundancy** with the spec file + registry, which are
|
|
519
|
-
// the true sources of truth for spec identity. In contrast, the
|
|
520
|
-
// validation/evaluation/gates/verify_acs events are the ONLY record of
|
|
521
|
-
// those verification runs and losing them is real data loss.
|
|
522
|
-
//
|
|
523
|
-
// So we deliberately wrap spec-lifecycle emits in try/catch: a
|
|
524
|
-
// filesystem error here (test mocks, readonly fs, etc.) must not crash
|
|
525
|
-
// the spec create/close/delete flow, because the spec file itself is
|
|
526
|
-
// already persisted by the time we get here. This is a principled
|
|
527
|
-
// divergence from the strict contract for the observation events —
|
|
528
|
-
// see docs/internal/EVENTS_LOG_MIGRATION.md §4.5 and EVLOG-001 spec.
|
|
529
|
-
try {
|
|
530
|
-
await appendEvent(
|
|
531
|
-
{
|
|
532
|
-
actor: 'cli',
|
|
533
|
-
event: 'spec_created',
|
|
534
|
-
spec_id: id,
|
|
535
|
-
data: {
|
|
536
|
-
id,
|
|
537
|
-
type: parsedSpec.type || type,
|
|
538
|
-
title: parsedSpec.title || title,
|
|
539
|
-
risk_tier: parsedSpec.risk_tier || numericRiskTier,
|
|
540
|
-
mode: parsedSpec.mode || mode,
|
|
541
|
-
},
|
|
542
|
-
},
|
|
543
|
-
{ projectRoot: findProjectRoot() }
|
|
544
|
-
);
|
|
545
|
-
} catch (err) {
|
|
546
|
-
// Surface on stderr but don't propagate — the spec is already created.
|
|
547
|
-
|
|
548
|
-
console.error(`event-log: failed to record spec_created for ${id}: ${err.message}`);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
return {
|
|
552
|
-
id,
|
|
553
|
-
path: fileName,
|
|
554
|
-
type: parsedSpec.type || type,
|
|
555
|
-
title: parsedSpec.title || title,
|
|
556
|
-
status: parsedSpec.status || 'draft',
|
|
557
|
-
risk_tier: parsedSpec.risk_tier || numericRiskTier,
|
|
558
|
-
mode: parsedSpec.mode || mode,
|
|
559
|
-
created_at: parsedSpec.created_at || specContent.created_at,
|
|
560
|
-
updated_at: parsedSpec.updated_at || specContent.updated_at,
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
/**
|
|
565
|
-
* Load a specific spec file
|
|
566
|
-
* @param {string} id - Spec identifier
|
|
567
|
-
* @returns {Promise<Object|null>} Spec data or null
|
|
568
|
-
*/
|
|
569
|
-
async function loadSpec(id) {
|
|
570
|
-
const registry = await loadSpecsRegistry();
|
|
571
|
-
|
|
572
|
-
if (!registry.specs[id]) {
|
|
573
|
-
return null;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
const specPath = path.join(getSpecsDir(), registry.specs[id].path);
|
|
577
|
-
|
|
578
|
-
try {
|
|
579
|
-
const content = await fs.readFile(specPath, 'utf8');
|
|
580
|
-
return yaml.load(content);
|
|
581
|
-
} catch (error) {
|
|
582
|
-
throw new Error(`Failed to load spec '${id}' from ${specPath}: ${error.message}`);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
/**
|
|
587
|
-
* Update a spec file
|
|
588
|
-
* @param {string} id - Spec identifier
|
|
589
|
-
* @param {Object} updates - Updates to apply
|
|
590
|
-
* @returns {Promise<boolean>} Success status
|
|
591
|
-
*/
|
|
592
|
-
async function updateSpec(id, updates = {}) {
|
|
593
|
-
const spec = await loadSpec(id);
|
|
594
|
-
|
|
595
|
-
if (!spec) {
|
|
596
|
-
return false;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// Validate status if being updated
|
|
600
|
-
if (updates.status) {
|
|
601
|
-
const { SPEC_STATUSES } = require('../constants/spec-types');
|
|
602
|
-
if (!SPEC_STATUSES[updates.status]) {
|
|
603
|
-
throw new Error(
|
|
604
|
-
`Invalid status '${updates.status}'. Valid values: ${Object.keys(SPEC_STATUSES).join(', ')}`
|
|
605
|
-
);
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
// Apply updates
|
|
610
|
-
const updatedSpec = {
|
|
611
|
-
...spec,
|
|
612
|
-
...updates,
|
|
613
|
-
updated_at: new Date().toISOString(),
|
|
614
|
-
};
|
|
615
|
-
const normalizedSpec = normalizeSpecForValidation(updatedSpec);
|
|
616
|
-
|
|
617
|
-
// Write back to file
|
|
618
|
-
const registry = await loadSpecsRegistry();
|
|
619
|
-
const specPath = path.join(getSpecsDir(), registry.specs[id].path);
|
|
620
|
-
const previousContent = await fs.readFile(specPath, 'utf8');
|
|
621
|
-
await fs.writeFile(specPath, yaml.dump(normalizedSpec, { indent: 2 }));
|
|
622
|
-
|
|
623
|
-
let parsedSpec;
|
|
624
|
-
try {
|
|
625
|
-
parsedSpec = await validateAndReadSpecFile(specPath);
|
|
626
|
-
} catch (error) {
|
|
627
|
-
await fs.writeFile(specPath, previousContent);
|
|
628
|
-
throw new Error(`Failed to update spec '${id}': ${error.message}`);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
registry.specs[id] = buildRegistryEntryFromSpec(
|
|
632
|
-
parsedSpec,
|
|
633
|
-
registry.specs[id].path,
|
|
634
|
-
registry.specs[id].owner || null
|
|
635
|
-
);
|
|
636
|
-
await saveSpecsRegistry(registry);
|
|
637
|
-
|
|
638
|
-
return true;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
/**
|
|
642
|
-
* Merge new spec data with an existing spec
|
|
643
|
-
* Combines acceptance criteria, updates metadata, preserves history
|
|
644
|
-
* @param {string} id - Spec identifier
|
|
645
|
-
* @param {Object} options - Options including new spec data to merge
|
|
646
|
-
* @returns {Promise<Object>} Merged spec
|
|
647
|
-
*/
|
|
648
|
-
async function mergeSpec(id, options = {}) {
|
|
649
|
-
const existingSpec = await loadSpec(id);
|
|
650
|
-
if (!existingSpec) {
|
|
651
|
-
throw new Error(`Spec '${id}' not found`);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
console.log(chalk.blue(`\nMerging into existing spec: ${id}`));
|
|
655
|
-
console.log(chalk.gray('==============================================\n'));
|
|
656
|
-
|
|
657
|
-
// Show existing spec summary
|
|
658
|
-
console.log(chalk.gray(`Existing spec:`));
|
|
659
|
-
console.log(chalk.gray(` Title: ${existingSpec.title}`));
|
|
660
|
-
console.log(chalk.gray(` Status: ${existingSpec.status}`));
|
|
661
|
-
console.log(
|
|
662
|
-
chalk.gray(` Acceptance Criteria: ${existingSpec.acceptance_criteria?.length || 0}`)
|
|
663
|
-
);
|
|
664
|
-
console.log('');
|
|
665
|
-
|
|
666
|
-
// Prepare merge data from options
|
|
667
|
-
const {
|
|
668
|
-
title: newTitle,
|
|
669
|
-
description: newDescription,
|
|
670
|
-
acceptance_criteria: newCriteria,
|
|
671
|
-
mode: newMode,
|
|
672
|
-
risk_tier: newRiskTier,
|
|
673
|
-
} = options;
|
|
674
|
-
|
|
675
|
-
const mergedSpec = { ...existingSpec };
|
|
676
|
-
|
|
677
|
-
// Track what was merged
|
|
678
|
-
const mergeLog = [];
|
|
679
|
-
|
|
680
|
-
// Merge title (prefer new if provided)
|
|
681
|
-
if (newTitle && newTitle !== existingSpec.title) {
|
|
682
|
-
mergedSpec.title = newTitle;
|
|
683
|
-
mergeLog.push(`Title updated: "${existingSpec.title}" → "${newTitle}"`);
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// Merge description
|
|
687
|
-
if (newDescription) {
|
|
688
|
-
if (existingSpec.description) {
|
|
689
|
-
mergedSpec.description = `${existingSpec.description}\n\n---\n\n${newDescription}`;
|
|
690
|
-
mergeLog.push('Description appended');
|
|
691
|
-
} else {
|
|
692
|
-
mergedSpec.description = newDescription;
|
|
693
|
-
mergeLog.push('Description added');
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
// Merge acceptance criteria (append new ones, avoid duplicates)
|
|
698
|
-
if (newCriteria && Array.isArray(newCriteria) && newCriteria.length > 0) {
|
|
699
|
-
const existingCriteria = existingSpec.acceptance_criteria || [];
|
|
700
|
-
const existingIds = new Set(existingCriteria.map((c) => c.id));
|
|
701
|
-
|
|
702
|
-
const criteriaToAdd = newCriteria.filter((c) => !existingIds.has(c.id));
|
|
703
|
-
if (criteriaToAdd.length > 0) {
|
|
704
|
-
mergedSpec.acceptance_criteria = [...existingCriteria, ...criteriaToAdd];
|
|
705
|
-
mergeLog.push(`Added ${criteriaToAdd.length} new acceptance criteria`);
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
// Also update the 'acceptance' array if it exists
|
|
709
|
-
if (existingSpec.acceptance) {
|
|
710
|
-
const existingAcceptIds = new Set(existingSpec.acceptance.map((a) => a.id));
|
|
711
|
-
const acceptToAdd = newCriteria.filter((c) => !existingAcceptIds.has(c.id));
|
|
712
|
-
if (acceptToAdd.length > 0) {
|
|
713
|
-
mergedSpec.acceptance = [...existingSpec.acceptance, ...acceptToAdd];
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
// Merge mode (prefer higher tier if both provided)
|
|
719
|
-
if (newMode && newMode !== existingSpec.mode) {
|
|
720
|
-
// Mode priority: crisis > standard > minimal
|
|
721
|
-
const modePriority = { minimal: 1, standard: 2, crisis: 3 };
|
|
722
|
-
if ((modePriority[newMode] || 0) > (modePriority[existingSpec.mode] || 0)) {
|
|
723
|
-
mergedSpec.mode = newMode;
|
|
724
|
-
mergeLog.push(`Mode upgraded: ${existingSpec.mode} → ${newMode}`);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
// Merge risk tier (prefer higher risk if both provided)
|
|
729
|
-
if (newRiskTier && newRiskTier !== existingSpec.risk_tier) {
|
|
730
|
-
// Risk priority: T1 > T2 > T3
|
|
731
|
-
const riskPriority = { T3: 1, T2: 2, T1: 3, 3: 1, 2: 2, 1: 3 };
|
|
732
|
-
if ((riskPriority[newRiskTier] || 0) > (riskPriority[existingSpec.risk_tier] || 0)) {
|
|
733
|
-
mergedSpec.risk_tier = newRiskTier;
|
|
734
|
-
mergeLog.push(`Risk tier updated: ${existingSpec.risk_tier} → ${newRiskTier}`);
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
// Update metadata
|
|
739
|
-
mergedSpec.updated_at = new Date().toISOString();
|
|
740
|
-
|
|
741
|
-
// Add merge history entry
|
|
742
|
-
if (!mergedSpec.history) {
|
|
743
|
-
mergedSpec.history = [];
|
|
744
|
-
}
|
|
745
|
-
mergedSpec.history.push({
|
|
746
|
-
action: 'merge',
|
|
747
|
-
timestamp: new Date().toISOString(),
|
|
748
|
-
changes: mergeLog,
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
// Save merged spec
|
|
752
|
-
await updateSpec(id, mergedSpec);
|
|
753
|
-
|
|
754
|
-
// Display merge results
|
|
755
|
-
console.log(chalk.green('Merge completed:'));
|
|
756
|
-
if (mergeLog.length > 0) {
|
|
757
|
-
mergeLog.forEach((change) => {
|
|
758
|
-
console.log(chalk.gray(` - ${change}`));
|
|
759
|
-
});
|
|
760
|
-
} else {
|
|
761
|
-
console.log(chalk.gray(' - No changes needed (specs were identical)'));
|
|
762
|
-
}
|
|
763
|
-
console.log('');
|
|
764
|
-
|
|
765
|
-
return mergedSpec;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
/**
|
|
769
|
-
* Delete a spec file
|
|
770
|
-
* @param {string} id - Spec identifier
|
|
771
|
-
* @returns {Promise<boolean>} Success status
|
|
772
|
-
*/
|
|
773
|
-
async function deleteSpec(id) {
|
|
774
|
-
const registry = await loadSpecsRegistry();
|
|
775
|
-
|
|
776
|
-
if (!registry.specs[id]) {
|
|
777
|
-
return false;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
// Block deletion if owned by another session
|
|
781
|
-
const currentSession = getAgentSessionId(findProjectRoot());
|
|
782
|
-
const existingEntry = registry.specs[id];
|
|
783
|
-
if (existingEntry?.owner && currentSession && existingEntry.owner !== currentSession) {
|
|
784
|
-
throw new Error(
|
|
785
|
-
`Cannot delete spec '${id}': owned by another session (${existingEntry.owner}). ` +
|
|
786
|
-
`Only the creator session can delete a spec.`
|
|
787
|
-
);
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
// Block deletion if active worktrees reference this spec
|
|
791
|
-
const referencingWorktrees = getWorktreesReferencingSpec(id);
|
|
792
|
-
if (referencingWorktrees.length > 0) {
|
|
793
|
-
const names = referencingWorktrees.join(', ');
|
|
794
|
-
throw new Error(
|
|
795
|
-
`Cannot delete spec '${id}': active worktree(s) [${names}] reference it. ` +
|
|
796
|
-
`Destroy the worktree(s) first with 'caws worktree destroy <name>'.`
|
|
797
|
-
);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
const specPath = path.join(getSpecsDir(), registry.specs[id].path);
|
|
801
|
-
|
|
802
|
-
// Remove file
|
|
803
|
-
await fs.remove(specPath);
|
|
804
|
-
|
|
805
|
-
// Clean up working state
|
|
806
|
-
try { deleteState(id, findProjectRoot()); } catch { /* non-fatal */ }
|
|
807
|
-
|
|
808
|
-
// Update registry
|
|
809
|
-
delete registry.specs[id];
|
|
810
|
-
await saveSpecsRegistry(registry);
|
|
811
|
-
|
|
812
|
-
// EVLOG-001: emit spec_deleted event in best-effort mode. See the
|
|
813
|
-
// createSpec commentary for why spec-lifecycle events diverge from
|
|
814
|
-
// the strict fail-loud contract used by the observation events.
|
|
815
|
-
try {
|
|
816
|
-
await appendEvent(
|
|
817
|
-
{ actor: 'cli', event: 'spec_deleted', spec_id: id, data: { id } },
|
|
818
|
-
{ projectRoot: findProjectRoot() }
|
|
819
|
-
);
|
|
820
|
-
} catch (err) {
|
|
821
|
-
|
|
822
|
-
console.error(`event-log: failed to record spec_deleted for ${id}: ${err.message}`);
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
return true;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
/**
|
|
829
|
-
* Close a spec (sets status to 'closed', removing scope enforcement).
|
|
830
|
-
* @param {string} id - Spec identifier
|
|
831
|
-
* @returns {Promise<boolean>} Success status
|
|
832
|
-
*/
|
|
833
|
-
async function closeSpec(id) {
|
|
834
|
-
const spec = await loadSpec(id);
|
|
835
|
-
if (!spec) {
|
|
836
|
-
return false;
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
const currentStatus = spec.status || 'draft';
|
|
840
|
-
if (currentStatus === 'closed') {
|
|
841
|
-
console.log(chalk.yellow(`Spec '${id}' is already closed.`));
|
|
842
|
-
return true;
|
|
843
|
-
}
|
|
844
|
-
if (currentStatus === 'archived') {
|
|
845
|
-
console.log(chalk.yellow(`Spec '${id}' is archived and cannot be closed.`));
|
|
846
|
-
return false;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
// Block closure if owned by another session
|
|
850
|
-
const registry = await loadSpecsRegistry();
|
|
851
|
-
const existingEntry = registry.specs[id];
|
|
852
|
-
const currentSession = getAgentSessionId(findProjectRoot());
|
|
853
|
-
if (existingEntry?.owner && currentSession && existingEntry.owner !== currentSession) {
|
|
854
|
-
console.error(
|
|
855
|
-
chalk.red(
|
|
856
|
-
`Cannot close spec '${id}': owned by another session (${existingEntry.owner}). ` +
|
|
857
|
-
`Only the creator session can close a spec.`
|
|
858
|
-
)
|
|
859
|
-
);
|
|
860
|
-
return false;
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
// Block closure if active worktrees reference this spec (closing removes scope enforcement)
|
|
864
|
-
const referencingWorktrees = getWorktreesReferencingSpec(id);
|
|
865
|
-
if (referencingWorktrees.length > 0) {
|
|
866
|
-
const names = referencingWorktrees.join(', ');
|
|
867
|
-
console.error(
|
|
868
|
-
chalk.red(
|
|
869
|
-
`Cannot close spec '${id}': active worktree(s) [${names}] reference it. ` +
|
|
870
|
-
`Closing would remove scope enforcement while work is in progress. ` +
|
|
871
|
-
`Destroy the worktree(s) first with 'caws worktree destroy <name>'.`
|
|
872
|
-
)
|
|
873
|
-
);
|
|
874
|
-
return false;
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
// CAWSFIX-15: status-only flip uses targeted line-replace so the diff
|
|
878
|
-
// stays a single line. Full `updateSpec` reserializes the whole YAML,
|
|
879
|
-
// reordering fields and injecting `*ref_0` anchors for the
|
|
880
|
-
// acceptance/acceptance_criteria alias — ~20 lines of noise for what
|
|
881
|
-
// should be a one-word change.
|
|
882
|
-
const specPath = path.join(getSpecsDir(), registry.specs[id].path);
|
|
883
|
-
const original = await fs.readFile(specPath, 'utf8');
|
|
884
|
-
const nowIso = new Date().toISOString();
|
|
885
|
-
let patched = original.replace(/^status:\s*\S+\s*$/m, 'status: closed');
|
|
886
|
-
patched = patched.replace(/^updated_at:.*$/m, `updated_at: '${nowIso}'`);
|
|
887
|
-
let ok = false;
|
|
888
|
-
if (patched !== original) {
|
|
889
|
-
await fs.writeFile(specPath, patched);
|
|
890
|
-
registry.specs[id] = {
|
|
891
|
-
...registry.specs[id],
|
|
892
|
-
status: 'closed',
|
|
893
|
-
updated_at: nowIso,
|
|
894
|
-
};
|
|
895
|
-
await saveSpecsRegistry(registry);
|
|
896
|
-
ok = true;
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
// EVLOG-001: emit spec_closed event after the status update succeeds.
|
|
900
|
-
// Records the prior status so the renderer can reconstruct the lifecycle.
|
|
901
|
-
// Best-effort mode — see createSpec commentary.
|
|
902
|
-
if (ok) {
|
|
903
|
-
try {
|
|
904
|
-
await appendEvent(
|
|
905
|
-
{
|
|
906
|
-
actor: 'cli',
|
|
907
|
-
event: 'spec_closed',
|
|
908
|
-
spec_id: id,
|
|
909
|
-
data: { id, prior_status: currentStatus },
|
|
910
|
-
},
|
|
911
|
-
{ projectRoot: findProjectRoot() }
|
|
912
|
-
);
|
|
913
|
-
} catch (err) {
|
|
914
|
-
|
|
915
|
-
console.error(`event-log: failed to record spec_closed for ${id}: ${err.message}`);
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
return ok;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
/**
|
|
923
|
-
* Display specs in a formatted table
|
|
924
|
-
* @param {Array} specs - Array of spec objects
|
|
925
|
-
*/
|
|
926
|
-
function displaySpecsTable(specs) {
|
|
927
|
-
console.log(chalk.bold.cyan('\nCAWS Specs'));
|
|
928
|
-
console.log(chalk.cyan('==============================================\n'));
|
|
929
|
-
|
|
930
|
-
if (specs.length === 0) {
|
|
931
|
-
console.log(chalk.gray(' No specs found. Create one with: caws specs create <id>'));
|
|
932
|
-
return;
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
// Header
|
|
936
|
-
console.log(chalk.bold('ID'.padEnd(15) + 'Type'.padEnd(10) + 'Status'.padEnd(12) + 'Title'));
|
|
937
|
-
console.log(chalk.gray('-'.repeat(80)));
|
|
938
|
-
|
|
939
|
-
// Sort specs by type and status priority
|
|
940
|
-
const statusPriority = { active: 0, draft: 1, completed: 2, closed: 3, archived: 4 };
|
|
941
|
-
const sortedSpecs = specs.sort((a, b) => {
|
|
942
|
-
const typeDiff = a.type.localeCompare(b.type);
|
|
943
|
-
if (typeDiff !== 0) return typeDiff;
|
|
944
|
-
return (statusPriority[a.status] || 999) - (statusPriority[b.status] || 999);
|
|
945
|
-
});
|
|
946
|
-
|
|
947
|
-
sortedSpecs.forEach((spec) => {
|
|
948
|
-
const specType = SPEC_TYPES[spec.type] || SPEC_TYPES.feature;
|
|
949
|
-
const typeColor = specType.color;
|
|
950
|
-
|
|
951
|
-
const statusColor =
|
|
952
|
-
spec.status === 'active'
|
|
953
|
-
? chalk.green
|
|
954
|
-
: spec.status === 'draft'
|
|
955
|
-
? chalk.yellow
|
|
956
|
-
: spec.status === 'completed'
|
|
957
|
-
? chalk.blue
|
|
958
|
-
: chalk.gray;
|
|
959
|
-
|
|
960
|
-
console.log(
|
|
961
|
-
spec.id.padEnd(15) +
|
|
962
|
-
typeColor(spec.type.padEnd(9)) +
|
|
963
|
-
statusColor(spec.status.padEnd(11)) +
|
|
964
|
-
chalk.white(spec.title)
|
|
965
|
-
);
|
|
966
|
-
});
|
|
967
|
-
|
|
968
|
-
console.log('');
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
/**
|
|
972
|
-
* Display detailed spec information
|
|
973
|
-
* @param {Object} spec - Spec object
|
|
974
|
-
*/
|
|
975
|
-
function displaySpecDetails(spec) {
|
|
976
|
-
const specType = SPEC_TYPES[spec.type] || SPEC_TYPES.feature;
|
|
977
|
-
const typeColor = specType.color;
|
|
978
|
-
|
|
979
|
-
console.log(chalk.bold.cyan(`\nSpec Details: ${spec.id}`));
|
|
980
|
-
console.log(chalk.cyan('==============================================\n'));
|
|
981
|
-
|
|
982
|
-
console.log(`${specType.icon} ${typeColor(spec.type.toUpperCase())} - ${spec.title}`);
|
|
983
|
-
console.log(
|
|
984
|
-
chalk.gray(` Status: ${spec.status} | Risk Tier: ${spec.risk_tier} | Mode: ${spec.mode}`)
|
|
985
|
-
);
|
|
986
|
-
console.log(chalk.gray(` Created: ${new Date(spec.created_at).toLocaleDateString()}`));
|
|
987
|
-
console.log(chalk.gray(` Updated: ${new Date(spec.updated_at).toLocaleDateString()}`));
|
|
988
|
-
|
|
989
|
-
if (spec.description) {
|
|
990
|
-
console.log(chalk.gray(`\n Description: ${spec.description}`));
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
if (spec.acceptance_criteria && spec.acceptance_criteria.length > 0) {
|
|
994
|
-
console.log(chalk.gray(`\n Acceptance Criteria (${spec.acceptance_criteria.length}):`));
|
|
995
|
-
spec.acceptance_criteria.forEach((criterion, index) => {
|
|
996
|
-
const status = criterion.completed ? chalk.green('[done]') : chalk.red('[ ]');
|
|
997
|
-
console.log(
|
|
998
|
-
chalk.gray(` ${status} ${criterion.description || criterion.title || `A${index + 1}`}`)
|
|
999
|
-
);
|
|
1000
|
-
});
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
if (spec.contracts && spec.contracts.length > 0) {
|
|
1004
|
-
console.log(chalk.gray(`\n Contracts (${spec.contracts.length}):`));
|
|
1005
|
-
spec.contracts.forEach((contract) => {
|
|
1006
|
-
console.log(chalk.gray(` ${contract.type}: ${contract.path}`));
|
|
1007
|
-
});
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
console.log('');
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
/**
|
|
1014
|
-
* Migrate from legacy working-spec.yaml to feature-specific specs
|
|
1015
|
-
* @param {Object} options - Migration options
|
|
1016
|
-
* @param {Function} [createSpecFn] - Function to create specs (for testing)
|
|
1017
|
-
* @returns {Promise<Object>} Migration result
|
|
1018
|
-
*/
|
|
1019
|
-
async function migrateFromLegacy(options = {}, createSpecFn = createSpec) {
|
|
1020
|
-
const fs = require('fs-extra');
|
|
1021
|
-
const path = require('path');
|
|
1022
|
-
const yaml = require('js-yaml');
|
|
1023
|
-
const chalk = require('chalk');
|
|
1024
|
-
|
|
1025
|
-
const legacyPath = path.join(findProjectRoot(), '.caws', 'working-spec.yaml');
|
|
1026
|
-
|
|
1027
|
-
if (!(await fs.pathExists(legacyPath))) {
|
|
1028
|
-
throw new Error('No legacy working-spec.yaml found to migrate');
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
console.log(chalk.blue('Migrating from legacy single-spec to multi-spec...'));
|
|
1032
|
-
|
|
1033
|
-
const legacyContent = await fs.readFile(legacyPath, 'utf8');
|
|
1034
|
-
const legacySpec = yaml.load(legacyContent);
|
|
1035
|
-
|
|
1036
|
-
if (!legacySpec) {
|
|
1037
|
-
throw new Error('Legacy working-spec.yaml is empty or invalid');
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
if (!legacySpec.acceptance || !Array.isArray(legacySpec.acceptance)) {
|
|
1041
|
-
throw new Error('Legacy working-spec.yaml must have an acceptance array');
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
// Suggest feature breakdown based on acceptance criteria
|
|
1045
|
-
const features = suggestFeatureBreakdown(legacySpec);
|
|
1046
|
-
|
|
1047
|
-
console.log(chalk.green(`\nFound ${features.length} potential features to extract:`));
|
|
1048
|
-
features.forEach((feature, index) => {
|
|
1049
|
-
console.log(chalk.yellow(` ${index + 1}. ${feature.id} - ${feature.title}`));
|
|
1050
|
-
console.log(chalk.gray(` Scope: ${feature.scope.in.join(', ')}`));
|
|
1051
|
-
});
|
|
1052
|
-
|
|
1053
|
-
// Interactive selection or use provided feature IDs
|
|
1054
|
-
let selectedFeatures = features;
|
|
1055
|
-
|
|
1056
|
-
if (options.interactive) {
|
|
1057
|
-
selectedFeatures = await selectFeaturesInteractively(features);
|
|
1058
|
-
if (selectedFeatures.length === 0) {
|
|
1059
|
-
console.log(chalk.yellow('No features selected. Migration cancelled.'));
|
|
1060
|
-
return { migrated: 0, total: features.length, createdSpecs: [], legacySpec: legacySpec.id };
|
|
1061
|
-
}
|
|
1062
|
-
console.log(chalk.blue(`\nMigrating ${selectedFeatures.length} selected features`));
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
if (options.features && options.features.length > 0) {
|
|
1066
|
-
// Filter by original feature IDs (before transformation)
|
|
1067
|
-
selectedFeatures = features.filter((f) => options.features.includes(f.id));
|
|
1068
|
-
if (selectedFeatures.length === 0) {
|
|
1069
|
-
const errorMsg = `No features found matching: ${options.features.join(', ')}. Available features: ${features.map((f) => f.id).join(', ')}`;
|
|
1070
|
-
console.log(chalk.yellow(`${errorMsg}`));
|
|
1071
|
-
throw new Error(errorMsg);
|
|
1072
|
-
} else {
|
|
1073
|
-
console.log(chalk.blue(`\nMigrating selected features: ${options.features.join(', ')}`));
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
// Create each feature spec
|
|
1078
|
-
const createdSpecs = [];
|
|
1079
|
-
let featureCounter = 1;
|
|
1080
|
-
for (const feature of selectedFeatures) {
|
|
1081
|
-
try {
|
|
1082
|
-
// Transform feature ID to proper format (PREFIX-NUMBER) if needed
|
|
1083
|
-
let specId = feature.id;
|
|
1084
|
-
if (!/^[A-Z]+-\d+$/.test(specId)) {
|
|
1085
|
-
// Convert 'auth' -> 'FEAT-001', 'payment' -> 'FEAT-002', etc.
|
|
1086
|
-
const prefix = specId.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
|
1087
|
-
specId = `${prefix || 'FEAT'}-${String(featureCounter).padStart(3, '0')}`;
|
|
1088
|
-
featureCounter++;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
await createSpecFn(specId, {
|
|
1092
|
-
type: 'feature',
|
|
1093
|
-
title: feature.title,
|
|
1094
|
-
risk_tier: 'T3', // Default tier
|
|
1095
|
-
mode: 'development',
|
|
1096
|
-
template: feature,
|
|
1097
|
-
});
|
|
1098
|
-
|
|
1099
|
-
createdSpecs.push(specId);
|
|
1100
|
-
console.log(chalk.green(` Created spec: ${specId}`));
|
|
1101
|
-
} catch (error) {
|
|
1102
|
-
// Log full error details for debugging
|
|
1103
|
-
console.log(chalk.red(` Failed to create spec ${feature.id}: ${error.message}`));
|
|
1104
|
-
if (process.env.DEBUG_MIGRATION) {
|
|
1105
|
-
console.log(chalk.gray(` Error details: ${error.stack}`));
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
console.log(
|
|
1111
|
-
chalk.green(`\nMigration completed! Created ${createdSpecs.length} feature specs.`)
|
|
1112
|
-
);
|
|
1113
|
-
|
|
1114
|
-
if (createdSpecs.length > 0) {
|
|
1115
|
-
console.log(chalk.blue('\nNext steps:'));
|
|
1116
|
-
console.log(chalk.gray(' 1. Review and customize each feature spec'));
|
|
1117
|
-
console.log(chalk.gray(' 2. Update agents to use --spec-id <feature-id>'));
|
|
1118
|
-
console.log(chalk.gray(' 3. Consider archiving legacy working-spec.yaml when ready'));
|
|
1119
|
-
console.log(chalk.blue('\n Example: caws validate --spec-id user-auth'));
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
return {
|
|
1123
|
-
migrated: createdSpecs.length,
|
|
1124
|
-
total: selectedFeatures.length,
|
|
1125
|
-
createdSpecs,
|
|
1126
|
-
legacySpec: legacySpec.id,
|
|
1127
|
-
};
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
/**
|
|
1131
|
-
* Interactive feature selection for migration
|
|
1132
|
-
* @param {Array} features - Array of suggested features
|
|
1133
|
-
* @returns {Promise<Array>} Selected features
|
|
1134
|
-
*/
|
|
1135
|
-
async function selectFeaturesInteractively(features) {
|
|
1136
|
-
const readline = require('readline');
|
|
1137
|
-
const rl = readline.createInterface({
|
|
1138
|
-
input: process.stdin,
|
|
1139
|
-
output: process.stdout,
|
|
1140
|
-
});
|
|
1141
|
-
|
|
1142
|
-
console.log(chalk.cyan('\nSelect features to migrate:\n'));
|
|
1143
|
-
features.forEach((f, i) => {
|
|
1144
|
-
const scope = f.scope?.in?.join(', ') || 'N/A';
|
|
1145
|
-
console.log(` ${chalk.yellow(i + 1)}. ${chalk.bold(f.id || f.name)} - ${f.title || f.description}`);
|
|
1146
|
-
console.log(chalk.gray(` Scope: ${scope}`));
|
|
1147
|
-
});
|
|
1148
|
-
console.log(chalk.cyan(`\nEnter numbers separated by commas, or 'all' for all features:`));
|
|
1149
|
-
console.log(chalk.gray(`Example: 1,3,5 or all`));
|
|
1150
|
-
|
|
1151
|
-
try {
|
|
1152
|
-
const answer = await question(rl, '> ');
|
|
1153
|
-
const trimmed = answer.trim().toLowerCase();
|
|
1154
|
-
|
|
1155
|
-
if (trimmed === 'all' || trimmed === '*') {
|
|
1156
|
-
return features;
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
if (trimmed === '' || trimmed === 'none' || trimmed === 'q' || trimmed === 'quit') {
|
|
1160
|
-
return [];
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
// Parse comma-separated numbers
|
|
1164
|
-
const indices = trimmed
|
|
1165
|
-
.split(',')
|
|
1166
|
-
.map(n => parseInt(n.trim(), 10) - 1)
|
|
1167
|
-
.filter(i => !isNaN(i) && i >= 0 && i < features.length);
|
|
1168
|
-
|
|
1169
|
-
// Remove duplicates and sort
|
|
1170
|
-
const uniqueIndices = [...new Set(indices)].sort((a, b) => a - b);
|
|
1171
|
-
|
|
1172
|
-
return features.filter((_, i) => uniqueIndices.includes(i));
|
|
1173
|
-
} finally {
|
|
1174
|
-
await closeReadline(rl);
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
/**
|
|
1179
|
-
* Ask user how to resolve spec creation conflicts
|
|
1180
|
-
* @returns {Promise<string>} User's choice: 'cancel', 'rename', 'merge', 'override'
|
|
1181
|
-
*/
|
|
1182
|
-
async function askConflictResolution() {
|
|
1183
|
-
const readline = require('readline');
|
|
1184
|
-
|
|
1185
|
-
console.log(chalk.blue('\nConflict Resolution Options:'));
|
|
1186
|
-
console.log(chalk.gray(" 1. Cancel - Don't create the spec"));
|
|
1187
|
-
console.log(chalk.gray(' 2. Rename - Create with auto-generated name'));
|
|
1188
|
-
console.log(chalk.gray(' 3. Merge - Merge with existing spec (not implemented)'));
|
|
1189
|
-
console.log(chalk.gray(' 4. Override - Replace existing spec (use --force)'));
|
|
1190
|
-
console.log(chalk.yellow('\nEnter your choice (1-4) or the option name:'));
|
|
1191
|
-
|
|
1192
|
-
const rl = readline.createInterface({
|
|
1193
|
-
input: process.stdin,
|
|
1194
|
-
output: process.stdout,
|
|
1195
|
-
});
|
|
1196
|
-
|
|
1197
|
-
try {
|
|
1198
|
-
const answer = await question(rl, '> ');
|
|
1199
|
-
const trimmed = answer.trim().toLowerCase();
|
|
1200
|
-
|
|
1201
|
-
// Handle numeric choices
|
|
1202
|
-
if (trimmed === '1' || trimmed === 'cancel') {
|
|
1203
|
-
return 'cancel';
|
|
1204
|
-
} else if (trimmed === '2' || trimmed === 'rename') {
|
|
1205
|
-
return 'rename';
|
|
1206
|
-
} else if (trimmed === '3' || trimmed === 'merge') {
|
|
1207
|
-
return 'merge';
|
|
1208
|
-
} else if (trimmed === '4' || trimmed === 'override') {
|
|
1209
|
-
return 'override';
|
|
1210
|
-
} else {
|
|
1211
|
-
console.log(chalk.red('Invalid choice. Defaulting to cancel.'));
|
|
1212
|
-
return 'cancel';
|
|
1213
|
-
}
|
|
1214
|
-
} finally {
|
|
1215
|
-
await closeReadline(rl);
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
/**
|
|
1220
|
-
* Specs command handler
|
|
1221
|
-
* @param {string} action - Action to perform (list, create, show, update, delete, conflicts, migrate)
|
|
1222
|
-
* @param {Object} options - Command options
|
|
1223
|
-
*/
|
|
1224
|
-
async function specsCommand(action, options = {}) {
|
|
1225
|
-
return safeAsync(
|
|
1226
|
-
async () => {
|
|
1227
|
-
switch (action) {
|
|
1228
|
-
case 'list': {
|
|
1229
|
-
const specs = await listSpecFiles();
|
|
1230
|
-
displaySpecsTable(specs);
|
|
1231
|
-
|
|
1232
|
-
return outputResult({
|
|
1233
|
-
command: 'specs list',
|
|
1234
|
-
count: specs.length,
|
|
1235
|
-
specs: specs.map((s) => ({ id: s.id, type: s.type, status: s.status })),
|
|
1236
|
-
});
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
case 'conflicts': {
|
|
1240
|
-
const { checkScopeConflicts } = require('../utils/spec-resolver');
|
|
1241
|
-
const registry = await loadSpecsRegistry();
|
|
1242
|
-
const specIds = Object.keys(registry.specs ?? {});
|
|
1243
|
-
|
|
1244
|
-
if (specIds.length < 2) {
|
|
1245
|
-
console.log(chalk.blue('No scope conflicts possible with fewer than 2 specs'));
|
|
1246
|
-
return outputResult({
|
|
1247
|
-
command: 'specs conflicts',
|
|
1248
|
-
conflictCount: 0,
|
|
1249
|
-
conflicts: [],
|
|
1250
|
-
});
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
console.log(chalk.blue(`Checking scope conflicts between ${specIds.length} specs...`));
|
|
1254
|
-
const conflicts = await checkScopeConflicts(specIds);
|
|
1255
|
-
|
|
1256
|
-
if (conflicts.length === 0) {
|
|
1257
|
-
console.log(chalk.green('No scope conflicts detected'));
|
|
1258
|
-
} else {
|
|
1259
|
-
console.log(
|
|
1260
|
-
chalk.yellow(
|
|
1261
|
-
`Found ${conflicts.length} scope conflict${conflicts.length > 1 ? 's' : ''}:`
|
|
1262
|
-
)
|
|
1263
|
-
);
|
|
1264
|
-
conflicts.forEach((conflict) => {
|
|
1265
|
-
console.log(chalk.red(` ${conflict.spec1} ↔ ${conflict.spec2}:`));
|
|
1266
|
-
conflict.conflicts.forEach((pathConflict) => {
|
|
1267
|
-
console.log(chalk.gray(` ${pathConflict}`));
|
|
1268
|
-
});
|
|
1269
|
-
});
|
|
1270
|
-
console.log(
|
|
1271
|
-
chalk.blue('\nTip: Use non-overlapping scope.in paths to avoid conflicts')
|
|
1272
|
-
);
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
return outputResult({
|
|
1276
|
-
command: 'specs conflicts',
|
|
1277
|
-
conflictCount: conflicts.length,
|
|
1278
|
-
conflicts,
|
|
1279
|
-
});
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
case 'migrate': {
|
|
1283
|
-
// Allow tests to inject createSpec function
|
|
1284
|
-
const createSpecFn = options._createSpecFn || createSpec;
|
|
1285
|
-
const migrationOptions = { ...options };
|
|
1286
|
-
delete migrationOptions._createSpecFn; // Remove test-only option
|
|
1287
|
-
const result = await migrateFromLegacy(migrationOptions, createSpecFn);
|
|
1288
|
-
|
|
1289
|
-
return outputResult({
|
|
1290
|
-
command: 'specs migrate',
|
|
1291
|
-
...result,
|
|
1292
|
-
});
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
case 'create': {
|
|
1296
|
-
if (!options.id) {
|
|
1297
|
-
throw new Error('Spec ID is required. Usage: caws specs create <id>');
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
const newSpec = await createSpec(options.id, {
|
|
1301
|
-
type: options.type,
|
|
1302
|
-
title: options.title,
|
|
1303
|
-
risk_tier: options.tier,
|
|
1304
|
-
mode: options.mode,
|
|
1305
|
-
force: options.force,
|
|
1306
|
-
interactive: options.interactive,
|
|
1307
|
-
});
|
|
1308
|
-
|
|
1309
|
-
if (!newSpec) {
|
|
1310
|
-
// User canceled or creation failed
|
|
1311
|
-
return outputResult({
|
|
1312
|
-
command: 'specs create',
|
|
1313
|
-
canceled: true,
|
|
1314
|
-
message: 'Spec creation was canceled or failed',
|
|
1315
|
-
});
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
console.log(chalk.green(`Created spec: ${newSpec.id}`));
|
|
1319
|
-
displaySpecDetails(newSpec);
|
|
1320
|
-
|
|
1321
|
-
return outputResult({
|
|
1322
|
-
command: 'specs create',
|
|
1323
|
-
spec: newSpec,
|
|
1324
|
-
});
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
case 'show': {
|
|
1328
|
-
if (!options.id) {
|
|
1329
|
-
throw new Error('Spec ID is required. Usage: caws specs show <id>');
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
const spec = await loadSpec(options.id);
|
|
1333
|
-
if (!spec) {
|
|
1334
|
-
throw new Error(`Spec '${options.id}' not found`);
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
displaySpecDetails(spec);
|
|
1338
|
-
|
|
1339
|
-
return outputResult({
|
|
1340
|
-
command: 'specs show',
|
|
1341
|
-
spec: { id: spec.id, type: spec.type, status: spec.status },
|
|
1342
|
-
});
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
case 'update': {
|
|
1346
|
-
if (!options.id) {
|
|
1347
|
-
throw new Error('Spec ID is required. Usage: caws specs update <id>');
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
const updates = {};
|
|
1351
|
-
if (options.status) updates.status = options.status;
|
|
1352
|
-
if (options.title) updates.title = options.title;
|
|
1353
|
-
if (options.description) updates.description = options.description;
|
|
1354
|
-
|
|
1355
|
-
const updated = await updateSpec(options.id, updates);
|
|
1356
|
-
if (!updated) {
|
|
1357
|
-
throw new Error(`Spec '${options.id}' not found`);
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
console.log(chalk.green(`Updated spec: ${options.id}`));
|
|
1361
|
-
|
|
1362
|
-
return outputResult({
|
|
1363
|
-
command: 'specs update',
|
|
1364
|
-
spec: options.id,
|
|
1365
|
-
updates,
|
|
1366
|
-
});
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
case 'delete': {
|
|
1370
|
-
if (!options.id) {
|
|
1371
|
-
throw new Error('Spec ID is required. Usage: caws specs delete <id>');
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
const deleted = await deleteSpec(options.id);
|
|
1375
|
-
if (!deleted) {
|
|
1376
|
-
throw new Error(`Spec '${options.id}' not found`);
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
console.log(chalk.green(`Deleted spec: ${options.id}`));
|
|
1380
|
-
|
|
1381
|
-
return outputResult({
|
|
1382
|
-
command: 'specs delete',
|
|
1383
|
-
spec: options.id,
|
|
1384
|
-
});
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
case 'close': {
|
|
1388
|
-
if (!options.id) {
|
|
1389
|
-
throw new Error('Spec ID is required. Usage: caws specs close <id>');
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
const closed = await closeSpec(options.id);
|
|
1393
|
-
if (!closed) {
|
|
1394
|
-
throw new Error(`Could not close spec '${options.id}'`);
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
console.log(chalk.green(`Closed spec: ${options.id} -- scope restrictions removed`));
|
|
1398
|
-
|
|
1399
|
-
return outputResult({
|
|
1400
|
-
command: 'specs close',
|
|
1401
|
-
spec: options.id,
|
|
1402
|
-
});
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
case 'types': {
|
|
1406
|
-
console.log(chalk.bold.cyan('\nAvailable Spec Types'));
|
|
1407
|
-
console.log(chalk.cyan('==============================================\n'));
|
|
1408
|
-
|
|
1409
|
-
Object.entries(SPEC_TYPES).forEach(([type, info]) => {
|
|
1410
|
-
console.log(`${info.icon} ${info.color(type.padEnd(10))} - ${info.description}`);
|
|
1411
|
-
});
|
|
1412
|
-
|
|
1413
|
-
console.log('');
|
|
1414
|
-
|
|
1415
|
-
return outputResult({
|
|
1416
|
-
command: 'specs types',
|
|
1417
|
-
types: Object.keys(SPEC_TYPES),
|
|
1418
|
-
});
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
default:
|
|
1422
|
-
throw new Error(
|
|
1423
|
-
`Unknown specs action: ${action}. Use: list, create, show, update, delete, close, conflicts, migrate, types`
|
|
1424
|
-
);
|
|
1425
|
-
}
|
|
1426
|
-
},
|
|
1427
|
-
`specs ${action}`,
|
|
1428
|
-
true
|
|
1429
|
-
);
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
module.exports = {
|
|
1433
|
-
specsCommand,
|
|
1434
|
-
loadSpecsRegistry,
|
|
1435
|
-
saveSpecsRegistry,
|
|
1436
|
-
listSpecFiles,
|
|
1437
|
-
createSpec,
|
|
1438
|
-
loadSpec,
|
|
1439
|
-
updateSpec,
|
|
1440
|
-
deleteSpec,
|
|
1441
|
-
closeSpec,
|
|
1442
|
-
displaySpecsTable,
|
|
1443
|
-
displaySpecDetails,
|
|
1444
|
-
askConflictResolution,
|
|
1445
|
-
SPECS_DIR,
|
|
1446
|
-
SPECS_REGISTRY,
|
|
1447
|
-
SPEC_TYPES,
|
|
1448
|
-
};
|