@paths.design/caws-cli 9.3.2 → 10.0.1
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 +58 -27
- package/dist/commands/archive.js +67 -28
- package/dist/commands/burnup.js +20 -11
- package/dist/commands/diagnose.js +34 -22
- package/dist/commands/evaluate.js +27 -15
- package/dist/commands/gates.js +122 -0
- package/dist/commands/init.js +143 -15
- package/dist/commands/iterate.js +77 -4
- package/dist/commands/parallel.js +4 -0
- package/dist/commands/plan.js +9 -19
- package/dist/commands/provenance.js +53 -17
- package/dist/commands/quality-monitor.js +64 -45
- package/dist/commands/sidecar.js +71 -0
- package/dist/commands/specs.js +233 -44
- package/dist/commands/status.js +113 -9
- package/dist/commands/tutorial.js +10 -9
- package/dist/commands/validate.js +49 -6
- package/dist/commands/verify-acs.js +35 -78
- package/dist/commands/waivers.js +69 -12
- package/dist/commands/worktree.js +50 -25
- package/dist/error-handler.js +2 -13
- package/dist/gates/budget-limit.js +116 -0
- package/dist/gates/feedback.js +260 -0
- package/dist/gates/format.js +179 -0
- package/dist/gates/god-object.js +117 -0
- package/dist/gates/pipeline.js +167 -0
- package/dist/gates/scope-boundary.js +93 -0
- package/dist/gates/spec-completeness.js +102 -0
- package/dist/gates/todo-detection.js +205 -0
- package/dist/index.js +130 -151
- package/dist/parallel/parallel-manager.js +3 -3
- package/dist/policy/PolicyManager.js +42 -10
- package/dist/scaffold/claude-hooks.js +24 -1
- package/dist/scaffold/git-hooks.js +45 -102
- package/dist/scaffold/index.js +4 -3
- package/dist/session/session-manager.js +71 -14
- package/dist/sidecars/index.js +33 -0
- package/dist/sidecars/listeners.js +40 -0
- package/dist/sidecars/provenance-summary.js +238 -0
- package/dist/sidecars/quality-gaps.js +258 -0
- package/dist/sidecars/schema.js +149 -0
- package/dist/sidecars/spec-drift.js +151 -0
- package/dist/sidecars/waiver-draft.js +176 -0
- package/dist/templates/.caws/schemas/policy.schema.json +50 -0
- package/dist/templates/.caws/schemas/waivers.schema.json +30 -24
- package/dist/templates/.caws/schemas/working-spec.schema.json +51 -8
- package/dist/templates/.caws/schemas/worktrees.schema.json +3 -1
- package/dist/templates/.caws/templates/working-spec.template.yml +7 -3
- package/dist/templates/.claude/hooks/audit.sh +0 -0
- package/dist/templates/.claude/hooks/block-dangerous.sh +52 -11
- package/dist/templates/.claude/hooks/classify_command.py +592 -0
- package/dist/templates/.claude/hooks/doc-frontmatter-check.sh +173 -0
- package/dist/templates/.claude/hooks/quality-check.sh +23 -10
- package/dist/templates/.claude/hooks/scope-guard.sh +34 -32
- package/dist/templates/.claude/hooks/session-caws-status.sh +2 -2
- package/dist/templates/.claude/hooks/session-log.sh +76 -3
- package/dist/templates/.claude/hooks/stop-worktree-check.sh +1 -1
- package/dist/templates/.claude/hooks/test_classify_command.py +370 -0
- package/dist/templates/.claude/hooks/test_wrapper_smoke.sh +96 -0
- package/dist/templates/.claude/hooks/worktree-guard.sh +2 -2
- package/dist/templates/.claude/hooks/worktree-write-guard.sh +1 -1
- package/dist/templates/.claude/settings.json +26 -0
- package/dist/templates/.cursor/hooks/caws-quality-check.sh +4 -4
- package/dist/templates/.cursor/hooks/caws-scope-guard.sh +1 -1
- package/dist/templates/.cursor/hooks/session-log.sh +924 -0
- package/dist/templates/.cursor/hooks.json +25 -0
- package/dist/templates/.cursor/rules/02-quality-gates.mdc +3 -5
- package/dist/templates/.cursor/rules/10-documentation-quality-standards.mdc +6 -11
- package/dist/templates/.cursor/rules/11-scope-management-waivers.mdc +14 -18
- package/dist/templates/.cursor/rules/12-implementation-completeness.mdc +4 -4
- package/dist/templates/.cursor/rules/13-language-agnostic-standards.mdc +3 -13
- package/dist/templates/.github/copilot-instructions.md +5 -5
- package/dist/templates/.idea/runConfigurations/CAWS_Evaluate.xml +1 -1
- package/dist/templates/.junie/guidelines.md +2 -2
- package/dist/templates/.vscode/settings.json +3 -1
- package/dist/templates/.windsurf/rules/caws-quality-standards.md +2 -2
- package/dist/templates/.windsurf/workflows/caws-guided-development.md +3 -3
- package/dist/templates/CLAUDE.md +43 -8
- package/dist/templates/agents.md +29 -9
- package/dist/templates/docs/README.md +8 -7
- package/dist/templates/scripts/new_feature.sh +80 -0
- package/dist/test-analysis.js +43 -30
- package/dist/tool-loader.js +1 -1
- package/dist/utils/agent-session.js +202 -0
- package/dist/utils/detection.js +8 -2
- package/dist/utils/finalization.js +7 -6
- package/dist/utils/gitignore-updater.js +3 -0
- package/dist/utils/lifecycle-events.js +94 -0
- package/dist/utils/quality-gates-utils.js +29 -44
- package/dist/utils/schema-validator.js +42 -0
- package/dist/utils/spec-resolver.js +93 -21
- package/dist/utils/working-state.js +505 -0
- package/dist/validation/spec-validation.js +92 -22
- package/dist/waivers-manager.js +60 -6
- package/dist/worktree/worktree-manager.js +390 -93
- package/package.json +6 -6
- package/templates/.caws/schemas/policy.schema.json +50 -0
- package/templates/.caws/schemas/waivers.schema.json +30 -24
- package/templates/.caws/schemas/working-spec.schema.json +51 -8
- package/templates/.caws/schemas/worktrees.schema.json +3 -1
- package/templates/.caws/templates/working-spec.template.yml +7 -3
- package/templates/.claude/hooks/block-dangerous.sh +52 -11
- package/templates/.claude/hooks/classify_command.py +592 -0
- package/templates/.claude/hooks/doc-frontmatter-check.sh +173 -0
- package/templates/.claude/hooks/quality-check.sh +23 -10
- package/templates/.claude/hooks/scope-guard.sh +34 -32
- package/templates/.claude/hooks/session-caws-status.sh +2 -2
- package/templates/.claude/hooks/session-log.sh +76 -3
- package/templates/.claude/hooks/stop-worktree-check.sh +1 -1
- package/templates/.claude/hooks/test_classify_command.py +370 -0
- package/templates/.claude/hooks/test_wrapper_smoke.sh +96 -0
- package/templates/.claude/hooks/worktree-guard.sh +2 -2
- package/templates/.claude/hooks/worktree-write-guard.sh +1 -1
- package/templates/.claude/settings.json +26 -0
- package/templates/.cursor/hooks/caws-quality-check.sh +4 -4
- package/templates/.cursor/hooks/caws-scope-guard.sh +1 -1
- package/templates/.cursor/hooks/session-log.sh +924 -0
- package/templates/.cursor/hooks.json +25 -0
- package/templates/.cursor/rules/02-quality-gates.mdc +3 -5
- package/templates/.cursor/rules/10-documentation-quality-standards.mdc +6 -11
- package/templates/.cursor/rules/11-scope-management-waivers.mdc +14 -18
- package/templates/.cursor/rules/12-implementation-completeness.mdc +4 -4
- package/templates/.cursor/rules/13-language-agnostic-standards.mdc +3 -13
- package/templates/.github/copilot-instructions.md +5 -5
- package/templates/.idea/runConfigurations/CAWS_Evaluate.xml +1 -1
- package/templates/.junie/guidelines.md +2 -2
- package/templates/.vscode/settings.json +3 -1
- package/templates/.windsurf/rules/caws-quality-standards.md +2 -2
- package/templates/.windsurf/workflows/caws-guided-development.md +3 -3
- package/templates/CLAUDE.md +43 -8
- package/templates/{AGENTS.md → agents.md} +29 -9
- package/templates/docs/README.md +8 -7
- package/templates/scripts/new_feature.sh +80 -0
- package/dist/budget-derivation.d.ts +0 -74
- package/dist/budget-derivation.d.ts.map +0 -1
- package/dist/cicd-optimizer.d.ts +0 -142
- package/dist/cicd-optimizer.d.ts.map +0 -1
- package/dist/commands/archive.d.ts +0 -51
- package/dist/commands/archive.d.ts.map +0 -1
- package/dist/commands/burnup.d.ts +0 -6
- package/dist/commands/burnup.d.ts.map +0 -1
- package/dist/commands/diagnose.d.ts +0 -52
- package/dist/commands/diagnose.d.ts.map +0 -1
- package/dist/commands/evaluate.d.ts +0 -8
- package/dist/commands/evaluate.d.ts.map +0 -1
- package/dist/commands/init.d.ts +0 -5
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/iterate.d.ts +0 -8
- package/dist/commands/iterate.d.ts.map +0 -1
- package/dist/commands/mode.d.ts +0 -25
- package/dist/commands/mode.d.ts.map +0 -1
- package/dist/commands/parallel.d.ts +0 -7
- package/dist/commands/parallel.d.ts.map +0 -1
- package/dist/commands/plan.d.ts +0 -49
- package/dist/commands/plan.d.ts.map +0 -1
- package/dist/commands/provenance.d.ts +0 -32
- package/dist/commands/provenance.d.ts.map +0 -1
- package/dist/commands/quality-gates.d.ts +0 -6
- package/dist/commands/quality-gates.d.ts.map +0 -1
- package/dist/commands/quality-gates.js +0 -444
- package/dist/commands/quality-monitor.d.ts +0 -17
- package/dist/commands/quality-monitor.d.ts.map +0 -1
- package/dist/commands/session.d.ts +0 -7
- package/dist/commands/session.d.ts.map +0 -1
- package/dist/commands/specs.d.ts +0 -77
- package/dist/commands/specs.d.ts.map +0 -1
- package/dist/commands/status.d.ts +0 -44
- package/dist/commands/status.d.ts.map +0 -1
- package/dist/commands/templates.d.ts +0 -74
- package/dist/commands/templates.d.ts.map +0 -1
- package/dist/commands/tool.d.ts +0 -13
- package/dist/commands/tool.d.ts.map +0 -1
- package/dist/commands/troubleshoot.d.ts +0 -8
- package/dist/commands/troubleshoot.d.ts.map +0 -1
- package/dist/commands/troubleshoot.js +0 -104
- package/dist/commands/tutorial.d.ts +0 -55
- package/dist/commands/tutorial.d.ts.map +0 -1
- package/dist/commands/validate.d.ts +0 -15
- package/dist/commands/validate.d.ts.map +0 -1
- package/dist/commands/waivers.d.ts +0 -8
- package/dist/commands/waivers.d.ts.map +0 -1
- package/dist/commands/workflow.d.ts +0 -85
- package/dist/commands/workflow.d.ts.map +0 -1
- package/dist/commands/worktree.d.ts +0 -7
- package/dist/commands/worktree.d.ts.map +0 -1
- package/dist/config/index.d.ts +0 -29
- package/dist/config/index.d.ts.map +0 -1
- package/dist/config/lite-scope.d.ts +0 -33
- package/dist/config/lite-scope.d.ts.map +0 -1
- package/dist/config/modes.d.ts +0 -264
- package/dist/config/modes.d.ts.map +0 -1
- package/dist/constants/spec-types.d.ts +0 -93
- package/dist/constants/spec-types.d.ts.map +0 -1
- package/dist/error-handler.d.ts +0 -151
- package/dist/error-handler.d.ts.map +0 -1
- package/dist/generators/jest-config-generator.d.ts +0 -32
- package/dist/generators/jest-config-generator.d.ts.map +0 -1
- package/dist/generators/jest-config.d.ts +0 -32
- package/dist/generators/jest-config.d.ts.map +0 -1
- package/dist/generators/jest-config.js +0 -242
- package/dist/generators/working-spec.d.ts +0 -13
- package/dist/generators/working-spec.d.ts.map +0 -1
- package/dist/index-new.d.ts +0 -5
- package/dist/index-new.d.ts.map +0 -1
- package/dist/index-new.js +0 -317
- package/dist/index.d.ts +0 -5
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.backup +0 -4711
- package/dist/minimal-cli.d.ts +0 -3
- package/dist/minimal-cli.d.ts.map +0 -1
- package/dist/parallel/parallel-manager.d.ts +0 -67
- package/dist/parallel/parallel-manager.d.ts.map +0 -1
- package/dist/policy/PolicyManager.d.ts +0 -104
- package/dist/policy/PolicyManager.d.ts.map +0 -1
- package/dist/scaffold/claude-hooks.d.ts +0 -28
- package/dist/scaffold/claude-hooks.d.ts.map +0 -1
- package/dist/scaffold/cursor-hooks.d.ts +0 -7
- package/dist/scaffold/cursor-hooks.d.ts.map +0 -1
- package/dist/scaffold/git-hooks.d.ts +0 -38
- package/dist/scaffold/git-hooks.d.ts.map +0 -1
- package/dist/scaffold/index.d.ts +0 -17
- package/dist/scaffold/index.d.ts.map +0 -1
- package/dist/session/session-manager.d.ts +0 -94
- package/dist/session/session-manager.d.ts.map +0 -1
- package/dist/spec/SpecFileManager.d.ts +0 -146
- package/dist/spec/SpecFileManager.d.ts.map +0 -1
- package/dist/templates/.cursor/hooks/caws-tool-validation.sh +0 -121
- package/dist/templates/.github/copilot/instructions.md +0 -311
- package/dist/test-analysis.d.ts +0 -231
- package/dist/test-analysis.d.ts.map +0 -1
- package/dist/tool-interface.d.ts +0 -236
- package/dist/tool-interface.d.ts.map +0 -1
- package/dist/tool-loader.d.ts +0 -77
- package/dist/tool-loader.d.ts.map +0 -1
- package/dist/tool-validator.d.ts +0 -72
- package/dist/tool-validator.d.ts.map +0 -1
- package/dist/utils/async-utils.d.ts +0 -73
- package/dist/utils/async-utils.d.ts.map +0 -1
- package/dist/utils/command-wrapper.d.ts +0 -66
- package/dist/utils/command-wrapper.d.ts.map +0 -1
- package/dist/utils/detection.d.ts +0 -14
- package/dist/utils/detection.d.ts.map +0 -1
- package/dist/utils/error-categories.d.ts +0 -52
- package/dist/utils/error-categories.d.ts.map +0 -1
- package/dist/utils/finalization.d.ts +0 -17
- package/dist/utils/finalization.d.ts.map +0 -1
- package/dist/utils/git-lock.d.ts +0 -13
- package/dist/utils/git-lock.d.ts.map +0 -1
- package/dist/utils/gitignore-updater.d.ts +0 -39
- package/dist/utils/gitignore-updater.d.ts.map +0 -1
- package/dist/utils/ide-detection.d.ts +0 -89
- package/dist/utils/ide-detection.d.ts.map +0 -1
- package/dist/utils/project-analysis.d.ts +0 -34
- package/dist/utils/project-analysis.d.ts.map +0 -1
- package/dist/utils/promise-utils.d.ts +0 -30
- package/dist/utils/promise-utils.d.ts.map +0 -1
- package/dist/utils/quality-gates-utils.d.ts +0 -49
- package/dist/utils/quality-gates-utils.d.ts.map +0 -1
- package/dist/utils/quality-gates.d.ts +0 -49
- package/dist/utils/quality-gates.d.ts.map +0 -1
- package/dist/utils/quality-gates.js +0 -402
- package/dist/utils/spec-resolver.d.ts +0 -80
- package/dist/utils/spec-resolver.d.ts.map +0 -1
- package/dist/utils/typescript-detector.d.ts +0 -66
- package/dist/utils/typescript-detector.d.ts.map +0 -1
- package/dist/utils/yaml-validation.d.ts +0 -32
- package/dist/utils/yaml-validation.d.ts.map +0 -1
- package/dist/validation/spec-validation.d.ts +0 -43
- package/dist/validation/spec-validation.d.ts.map +0 -1
- package/dist/waivers-manager.d.ts +0 -167
- package/dist/waivers-manager.d.ts.map +0 -1
- package/dist/worktree/worktree-manager.d.ts +0 -54
- package/dist/worktree/worktree-manager.d.ts.map +0 -1
|
@@ -8,9 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const chalk = require('chalk');
|
|
11
|
-
const fs = require('fs');
|
|
12
11
|
const path = require('path');
|
|
13
|
-
const
|
|
12
|
+
const { resolveSpec } = require('../utils/spec-resolver');
|
|
14
13
|
|
|
15
14
|
/**
|
|
16
15
|
* Analyze quality impact of an action
|
|
@@ -79,52 +78,58 @@ function analyzeQualityImpact(action, files = [], context = {}) {
|
|
|
79
78
|
analysis.recommendations = ['Run CAWS evaluation to assess impact'];
|
|
80
79
|
}
|
|
81
80
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const spec = yaml.load(fs.readFileSync(specPath, 'utf8'));
|
|
87
|
-
const projectTier = spec.risk_tier;
|
|
88
|
-
|
|
89
|
-
analysis.project_tier = projectTier;
|
|
90
|
-
|
|
91
|
-
// Add context-specific recommendations for high-risk projects
|
|
92
|
-
if (projectTier <= 2) {
|
|
93
|
-
analysis.recommendations.unshift(
|
|
94
|
-
'High-risk project (Tier ' + projectTier + '): Run comprehensive validation'
|
|
95
|
-
);
|
|
96
|
-
if (analysis.risk_level === 'low') {
|
|
97
|
-
analysis.risk_level = 'medium';
|
|
98
|
-
} else if (analysis.risk_level === 'medium') {
|
|
99
|
-
analysis.risk_level = 'high';
|
|
100
|
-
}
|
|
101
|
-
}
|
|
81
|
+
// Add context-based recommendations
|
|
82
|
+
if (context.project_tier) {
|
|
83
|
+
analysis.project_tier = context.project_tier;
|
|
84
|
+
}
|
|
102
85
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
86
|
+
return analysis;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Enrich analysis with project tier and resolved spec metadata.
|
|
91
|
+
* @param {Object} analysis
|
|
92
|
+
* @param {Object|null} resolvedSpec
|
|
93
|
+
* @returns {Object}
|
|
94
|
+
*/
|
|
95
|
+
function applySpecContext(analysis, resolvedSpec) {
|
|
96
|
+
if (!resolvedSpec?.spec) {
|
|
97
|
+
return analysis;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const projectTier = resolvedSpec.spec.risk_tier;
|
|
101
|
+
analysis.project_tier = projectTier;
|
|
102
|
+
analysis.spec_id = resolvedSpec.spec.id || null;
|
|
103
|
+
analysis.spec_path = resolvedSpec.path
|
|
104
|
+
? path.relative(process.cwd(), resolvedSpec.path)
|
|
105
|
+
: null;
|
|
106
|
+
|
|
107
|
+
if (projectTier <= 2) {
|
|
108
|
+
analysis.recommendations.unshift(
|
|
109
|
+
'High-risk project (Tier ' + projectTier + '): Run comprehensive validation'
|
|
110
|
+
);
|
|
111
|
+
if (analysis.risk_level === 'low') {
|
|
112
|
+
analysis.risk_level = 'medium';
|
|
113
|
+
} else if (analysis.risk_level === 'medium') {
|
|
114
|
+
analysis.risk_level = 'high';
|
|
120
115
|
}
|
|
121
|
-
} catch (error) {
|
|
122
|
-
// Ignore if we can't load spec
|
|
123
116
|
}
|
|
124
117
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
118
|
+
if (projectTier === 1) {
|
|
119
|
+
analysis.quality_gates = [
|
|
120
|
+
'Branch coverage ≥ 90%',
|
|
121
|
+
'Mutation score ≥ 70%',
|
|
122
|
+
'All contract tests passing',
|
|
123
|
+
'Manual code review required',
|
|
124
|
+
];
|
|
125
|
+
} else if (projectTier === 2) {
|
|
126
|
+
analysis.quality_gates = [
|
|
127
|
+
'Branch coverage ≥ 80%',
|
|
128
|
+
'Mutation score ≥ 50%',
|
|
129
|
+
'Contract tests passing (if applicable)',
|
|
130
|
+
];
|
|
131
|
+
} else {
|
|
132
|
+
analysis.quality_gates = ['Branch coverage ≥ 70%', 'Mutation score ≥ 30%'];
|
|
128
133
|
}
|
|
129
134
|
|
|
130
135
|
return analysis;
|
|
@@ -170,7 +175,18 @@ async function qualityMonitorCommand(action, options = {}) {
|
|
|
170
175
|
}
|
|
171
176
|
|
|
172
177
|
// Analyze quality impact
|
|
173
|
-
|
|
178
|
+
let analysis = analyzeQualityImpact(action, files, context);
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const resolved = await resolveSpec({
|
|
182
|
+
specId: options.specId,
|
|
183
|
+
warnLegacy: false,
|
|
184
|
+
interactive: false,
|
|
185
|
+
});
|
|
186
|
+
analysis = applySpecContext(analysis, resolved);
|
|
187
|
+
} catch {
|
|
188
|
+
// Best-effort enrichment only
|
|
189
|
+
}
|
|
174
190
|
|
|
175
191
|
// Display results
|
|
176
192
|
console.log(chalk.bold('\nCAWS Quality Monitor\n'));
|
|
@@ -219,6 +235,9 @@ async function qualityMonitorCommand(action, options = {}) {
|
|
|
219
235
|
// Project tier
|
|
220
236
|
if (analysis.project_tier) {
|
|
221
237
|
console.log(chalk.bold(`\nProject Tier: ${analysis.project_tier}`));
|
|
238
|
+
if (analysis.spec_id) {
|
|
239
|
+
console.log(chalk.gray(`Spec: ${analysis.spec_id}${analysis.spec_path ? ` (${analysis.spec_path})` : ''}`));
|
|
240
|
+
}
|
|
222
241
|
}
|
|
223
242
|
|
|
224
243
|
// Quality gates
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview CAWS Sidecar Command
|
|
3
|
+
* CLI interface for bounded governance sidecars — advisory analysis modules
|
|
4
|
+
* that consume working state and produce structured recommendations.
|
|
5
|
+
* @author @darianrosebrook
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
const { resolveSpec } = require('../utils/spec-resolver');
|
|
10
|
+
const { loadState } = require('../utils/working-state');
|
|
11
|
+
const { commandWrapper } = require('../utils/command-wrapper');
|
|
12
|
+
const { SIDECARS, formatSidecarText } = require('../sidecars');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Run a sidecar analysis.
|
|
16
|
+
* @param {string} subcommand - Sidecar name (drift, gaps, waiver-draft, provenance)
|
|
17
|
+
* @param {Object} options - Command options
|
|
18
|
+
* @param {string} [options.specId] - Target spec ID
|
|
19
|
+
* @param {boolean} [options.json] - Output as JSON
|
|
20
|
+
* @param {string} [options.gate] - Gate name filter (waiver-draft only)
|
|
21
|
+
*/
|
|
22
|
+
async function sidecarCommand(subcommand, options = {}) {
|
|
23
|
+
return commandWrapper(
|
|
24
|
+
async () => {
|
|
25
|
+
const sidecar = SIDECARS[subcommand];
|
|
26
|
+
if (!sidecar) {
|
|
27
|
+
const available = Object.keys(SIDECARS).join(', ');
|
|
28
|
+
console.error(chalk.red(`Unknown sidecar: ${subcommand}`));
|
|
29
|
+
console.error(chalk.yellow(`Available: ${available}`));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Resolve spec
|
|
34
|
+
let spec = null;
|
|
35
|
+
try {
|
|
36
|
+
const resolved = await resolveSpec({
|
|
37
|
+
specId: options.specId,
|
|
38
|
+
warnLegacy: false,
|
|
39
|
+
quiet: Boolean(options.json),
|
|
40
|
+
});
|
|
41
|
+
spec = resolved.spec;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error(chalk.red(`Could not resolve spec: ${err.message}`));
|
|
44
|
+
console.error(chalk.yellow('Use --spec-id <id> to target a specific spec.'));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Load working state (may be null — sidecars handle that)
|
|
49
|
+
const state = loadState(spec.id);
|
|
50
|
+
|
|
51
|
+
// Build sidecar-specific options
|
|
52
|
+
const sidecarOptions = {};
|
|
53
|
+
if (options.gate) sidecarOptions.gateName = options.gate;
|
|
54
|
+
|
|
55
|
+
// Run sidecar
|
|
56
|
+
const result = sidecar.fn(state, spec, sidecarOptions);
|
|
57
|
+
|
|
58
|
+
// Output
|
|
59
|
+
if (options.json) {
|
|
60
|
+
console.log(JSON.stringify(result, null, 2));
|
|
61
|
+
} else {
|
|
62
|
+
console.log(formatSidecarText(result));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return result;
|
|
66
|
+
},
|
|
67
|
+
{ commandName: `sidecar ${subcommand}` }
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { sidecarCommand };
|
package/dist/commands/specs.js
CHANGED
|
@@ -15,6 +15,36 @@ const { SPEC_TYPES } = require('../constants/spec-types');
|
|
|
15
15
|
// Import suggestFeatureBreakdown from spec-resolver
|
|
16
16
|
const { suggestFeatureBreakdown } = require('../utils/spec-resolver');
|
|
17
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
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if a spec is referenced by any active worktree.
|
|
24
|
+
* Returns the list of worktree names that reference it, or empty array.
|
|
25
|
+
* @param {string} specId - Spec identifier to check
|
|
26
|
+
* @returns {string[]} Names of worktrees referencing this spec
|
|
27
|
+
*/
|
|
28
|
+
function getWorktreesReferencingSpec(specId) {
|
|
29
|
+
try {
|
|
30
|
+
const root = getRepoRoot();
|
|
31
|
+
const registry = loadWorktreeRegistry(root);
|
|
32
|
+
const matches = [];
|
|
33
|
+
for (const [name, entry] of Object.entries(registry.worktrees || {})) {
|
|
34
|
+
if (
|
|
35
|
+
entry.specId === specId &&
|
|
36
|
+
entry.status !== 'destroyed' &&
|
|
37
|
+
entry.status !== 'merged'
|
|
38
|
+
) {
|
|
39
|
+
matches.push(name);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return matches;
|
|
43
|
+
} catch {
|
|
44
|
+
// If worktree registry can't be loaded (e.g., no .caws dir), no conflict
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
18
48
|
|
|
19
49
|
/**
|
|
20
50
|
* Specs directory structure — anchored to the CAWS project root,
|
|
@@ -68,6 +98,98 @@ async function saveSpecsRegistry(registry) {
|
|
|
68
98
|
await fs.writeFile(registryPath, JSON.stringify(registry, null, 2));
|
|
69
99
|
}
|
|
70
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Read and validate a spec YAML file that was just written.
|
|
103
|
+
* This catches malformed YAML and duplicate keys before registry sync.
|
|
104
|
+
* @param {string} filePath - Absolute path to the spec file
|
|
105
|
+
* @returns {Promise<Object>} Parsed spec object
|
|
106
|
+
*/
|
|
107
|
+
async function validateAndReadSpecFile(filePath) {
|
|
108
|
+
const writtenContent = await fs.readFile(filePath, 'utf8');
|
|
109
|
+
const parsed = yaml.load(writtenContent);
|
|
110
|
+
|
|
111
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
112
|
+
throw new Error('Failed to parse written spec file - invalid YAML structure');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const { validateWorkingSpec } = require('../validation/spec-validation');
|
|
116
|
+
const validation = validateWorkingSpec(parsed);
|
|
117
|
+
|
|
118
|
+
if (!validation.valid) {
|
|
119
|
+
const errorMessages = validation.errors
|
|
120
|
+
.map((e) => `${e.instancePath}: ${e.message}`)
|
|
121
|
+
.join('; ');
|
|
122
|
+
throw new Error(`Spec validation failed: ${errorMessages}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return parsed;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Build the registry entry from the parsed spec content instead of caller assumptions.
|
|
130
|
+
* @param {Object} spec - Parsed spec object
|
|
131
|
+
* @param {string} fileName - Registry path for the spec
|
|
132
|
+
* @param {string|null} owner - Session owner for the registry entry
|
|
133
|
+
* @returns {Object} Registry entry
|
|
134
|
+
*/
|
|
135
|
+
function buildRegistryEntryFromSpec(spec, fileName, owner = null) {
|
|
136
|
+
return {
|
|
137
|
+
path: fileName,
|
|
138
|
+
type: spec.type || 'feature',
|
|
139
|
+
status: spec.status || 'draft',
|
|
140
|
+
created_at: spec.created_at || new Date().toISOString(),
|
|
141
|
+
updated_at: spec.updated_at || new Date().toISOString(),
|
|
142
|
+
owner,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Backfill legacy sparse specs so write-time validation can succeed when
|
|
148
|
+
* update/merge flows touch older files created before the stricter schema.
|
|
149
|
+
* @param {Object} spec - Spec content to normalize
|
|
150
|
+
* @returns {Object} Normalized spec content
|
|
151
|
+
*/
|
|
152
|
+
function normalizeSpecForValidation(spec = {}) {
|
|
153
|
+
const normalizedRiskTier =
|
|
154
|
+
typeof spec.risk_tier === 'string'
|
|
155
|
+
? parseInt(spec.risk_tier.replace(/^T/i, ''), 10) || 3
|
|
156
|
+
: spec.risk_tier || 3;
|
|
157
|
+
|
|
158
|
+
const acceptanceVal = Array.isArray(spec.acceptance)
|
|
159
|
+
? spec.acceptance
|
|
160
|
+
: Array.isArray(spec.acceptance_criteria)
|
|
161
|
+
? spec.acceptance_criteria
|
|
162
|
+
: [];
|
|
163
|
+
|
|
164
|
+
const defaults = {
|
|
165
|
+
type: 'feature',
|
|
166
|
+
status: 'draft',
|
|
167
|
+
risk_tier: normalizedRiskTier,
|
|
168
|
+
mode: 'standard',
|
|
169
|
+
blast_radius: { modules: [], data_migration: false },
|
|
170
|
+
operational_rollback_slo: '5m',
|
|
171
|
+
scope: { in: ['src/', 'tests/'], out: ['node_modules/', 'dist/', 'build/'] },
|
|
172
|
+
invariants: ['System maintains data consistency'],
|
|
173
|
+
acceptance: [],
|
|
174
|
+
acceptance_criteria: [],
|
|
175
|
+
non_functional: { a11y: [], perf: {}, security: [] },
|
|
176
|
+
contracts: [],
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
...defaults,
|
|
181
|
+
...spec,
|
|
182
|
+
risk_tier: normalizedRiskTier,
|
|
183
|
+
blast_radius: { ...defaults.blast_radius, ...(spec.blast_radius || {}) },
|
|
184
|
+
scope: { ...defaults.scope, ...(spec.scope || {}) },
|
|
185
|
+
non_functional: { ...defaults.non_functional, ...(spec.non_functional || {}) },
|
|
186
|
+
acceptance: acceptanceVal,
|
|
187
|
+
acceptance_criteria: Array.isArray(spec.acceptance_criteria)
|
|
188
|
+
? spec.acceptance_criteria
|
|
189
|
+
: acceptanceVal,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
71
193
|
/**
|
|
72
194
|
* List all spec files in the specs directory
|
|
73
195
|
* @returns {Promise<Array>} Array of spec file info
|
|
@@ -192,8 +314,28 @@ async function createSpec(id, options = {}) {
|
|
|
192
314
|
}
|
|
193
315
|
}
|
|
194
316
|
|
|
195
|
-
// If we got here via override choice,
|
|
317
|
+
// If we got here via override choice, check ownership and worktree associations
|
|
196
318
|
if (specExists && (force || answer === 'override')) {
|
|
319
|
+
// Check session ownership — only the creator session can override
|
|
320
|
+
const registry = await loadSpecsRegistry();
|
|
321
|
+
const existingEntry = registry.specs[id];
|
|
322
|
+
const currentSession = getAgentSessionId(findProjectRoot());
|
|
323
|
+
if (existingEntry?.owner && currentSession && existingEntry.owner !== currentSession) {
|
|
324
|
+
throw new Error(
|
|
325
|
+
`Cannot override spec '${id}': owned by another session (${existingEntry.owner}). ` +
|
|
326
|
+
`Only the creator session can override a spec. Create a new spec with a different ID instead.`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Check for active worktree associations
|
|
331
|
+
const referencingWorktrees = getWorktreesReferencingSpec(id);
|
|
332
|
+
if (referencingWorktrees.length > 0) {
|
|
333
|
+
const names = referencingWorktrees.join(', ');
|
|
334
|
+
throw new Error(
|
|
335
|
+
`Cannot override spec '${id}': active worktree(s) [${names}] reference it. ` +
|
|
336
|
+
`Destroy the worktree(s) first with 'caws worktree destroy <name>', or create a new spec with a different ID.`
|
|
337
|
+
);
|
|
338
|
+
}
|
|
197
339
|
console.log(chalk.yellow('Overriding existing spec...'));
|
|
198
340
|
}
|
|
199
341
|
|
|
@@ -280,27 +422,9 @@ async function createSpec(id, options = {}) {
|
|
|
280
422
|
await fs.writeFile(filePath, yamlContent);
|
|
281
423
|
|
|
282
424
|
// Validate written file (YAML syntax and structure)
|
|
425
|
+
let parsedSpec;
|
|
283
426
|
try {
|
|
284
|
-
|
|
285
|
-
const parsed = yaml.load(writtenContent);
|
|
286
|
-
|
|
287
|
-
// Validate YAML syntax was preserved
|
|
288
|
-
if (!parsed || typeof parsed !== 'object') {
|
|
289
|
-
await fs.remove(filePath);
|
|
290
|
-
throw new Error('Failed to parse written spec file - invalid YAML structure');
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Validate spec structure using CAWS validation
|
|
294
|
-
const { validateWorkingSpec } = require('../validation/spec-validation');
|
|
295
|
-
const validation = validateWorkingSpec(parsed);
|
|
296
|
-
|
|
297
|
-
if (!validation.valid) {
|
|
298
|
-
await fs.remove(filePath);
|
|
299
|
-
const errorMessages = validation.errors
|
|
300
|
-
.map((e) => `${e.instancePath}: ${e.message}`)
|
|
301
|
-
.join('; ');
|
|
302
|
-
throw new Error(`Spec validation failed: ${errorMessages}`);
|
|
303
|
-
}
|
|
427
|
+
parsedSpec = await validateAndReadSpecFile(filePath);
|
|
304
428
|
} catch (error) {
|
|
305
429
|
// Clean up invalid file if it exists
|
|
306
430
|
if (await fs.pathExists(filePath)) {
|
|
@@ -319,25 +443,29 @@ async function createSpec(id, options = {}) {
|
|
|
319
443
|
|
|
320
444
|
// Update registry
|
|
321
445
|
const registry = await loadSpecsRegistry();
|
|
322
|
-
registry.specs[id] =
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
updated_at: specContent.updated_at,
|
|
328
|
-
};
|
|
446
|
+
registry.specs[id] = buildRegistryEntryFromSpec(
|
|
447
|
+
parsedSpec,
|
|
448
|
+
fileName,
|
|
449
|
+
getAgentSessionId(findProjectRoot())
|
|
450
|
+
);
|
|
329
451
|
await saveSpecsRegistry(registry);
|
|
330
452
|
|
|
453
|
+
// Initialize working state for new spec
|
|
454
|
+
try {
|
|
455
|
+
const initialState = initializeState(id);
|
|
456
|
+
saveState(id, initialState, findProjectRoot());
|
|
457
|
+
} catch { /* non-fatal */ }
|
|
458
|
+
|
|
331
459
|
return {
|
|
332
460
|
id,
|
|
333
461
|
path: fileName,
|
|
334
|
-
type,
|
|
335
|
-
title,
|
|
336
|
-
status: 'draft',
|
|
337
|
-
risk_tier: numericRiskTier,
|
|
338
|
-
mode,
|
|
339
|
-
created_at: specContent.created_at,
|
|
340
|
-
updated_at: specContent.updated_at,
|
|
462
|
+
type: parsedSpec.type || type,
|
|
463
|
+
title: parsedSpec.title || title,
|
|
464
|
+
status: parsedSpec.status || 'draft',
|
|
465
|
+
risk_tier: parsedSpec.risk_tier || numericRiskTier,
|
|
466
|
+
mode: parsedSpec.mode || mode,
|
|
467
|
+
created_at: parsedSpec.created_at || specContent.created_at,
|
|
468
|
+
updated_at: parsedSpec.updated_at || specContent.updated_at,
|
|
341
469
|
};
|
|
342
470
|
}
|
|
343
471
|
|
|
@@ -359,7 +487,7 @@ async function loadSpec(id) {
|
|
|
359
487
|
const content = await fs.readFile(specPath, 'utf8');
|
|
360
488
|
return yaml.load(content);
|
|
361
489
|
} catch (error) {
|
|
362
|
-
|
|
490
|
+
throw new Error(`Failed to load spec '${id}' from ${specPath}: ${error.message}`);
|
|
363
491
|
}
|
|
364
492
|
}
|
|
365
493
|
|
|
@@ -392,18 +520,28 @@ async function updateSpec(id, updates = {}) {
|
|
|
392
520
|
...updates,
|
|
393
521
|
updated_at: new Date().toISOString(),
|
|
394
522
|
};
|
|
523
|
+
const normalizedSpec = normalizeSpecForValidation(updatedSpec);
|
|
395
524
|
|
|
396
|
-
//
|
|
525
|
+
// Write back to file
|
|
397
526
|
const registry = await loadSpecsRegistry();
|
|
398
|
-
registry.specs[id].
|
|
399
|
-
|
|
400
|
-
|
|
527
|
+
const specPath = path.join(getSpecsDir(), registry.specs[id].path);
|
|
528
|
+
const previousContent = await fs.readFile(specPath, 'utf8');
|
|
529
|
+
await fs.writeFile(specPath, yaml.dump(normalizedSpec, { indent: 2 }));
|
|
530
|
+
|
|
531
|
+
let parsedSpec;
|
|
532
|
+
try {
|
|
533
|
+
parsedSpec = await validateAndReadSpecFile(specPath);
|
|
534
|
+
} catch (error) {
|
|
535
|
+
await fs.writeFile(specPath, previousContent);
|
|
536
|
+
throw new Error(`Failed to update spec '${id}': ${error.message}`);
|
|
401
537
|
}
|
|
402
|
-
await saveSpecsRegistry(registry);
|
|
403
538
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
539
|
+
registry.specs[id] = buildRegistryEntryFromSpec(
|
|
540
|
+
parsedSpec,
|
|
541
|
+
registry.specs[id].path,
|
|
542
|
+
registry.specs[id].owner || null
|
|
543
|
+
);
|
|
544
|
+
await saveSpecsRegistry(registry);
|
|
407
545
|
|
|
408
546
|
return true;
|
|
409
547
|
}
|
|
@@ -547,11 +685,34 @@ async function deleteSpec(id) {
|
|
|
547
685
|
return false;
|
|
548
686
|
}
|
|
549
687
|
|
|
688
|
+
// Block deletion if owned by another session
|
|
689
|
+
const currentSession = getAgentSessionId(findProjectRoot());
|
|
690
|
+
const existingEntry = registry.specs[id];
|
|
691
|
+
if (existingEntry?.owner && currentSession && existingEntry.owner !== currentSession) {
|
|
692
|
+
throw new Error(
|
|
693
|
+
`Cannot delete spec '${id}': owned by another session (${existingEntry.owner}). ` +
|
|
694
|
+
`Only the creator session can delete a spec.`
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Block deletion if active worktrees reference this spec
|
|
699
|
+
const referencingWorktrees = getWorktreesReferencingSpec(id);
|
|
700
|
+
if (referencingWorktrees.length > 0) {
|
|
701
|
+
const names = referencingWorktrees.join(', ');
|
|
702
|
+
throw new Error(
|
|
703
|
+
`Cannot delete spec '${id}': active worktree(s) [${names}] reference it. ` +
|
|
704
|
+
`Destroy the worktree(s) first with 'caws worktree destroy <name>'.`
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
550
708
|
const specPath = path.join(getSpecsDir(), registry.specs[id].path);
|
|
551
709
|
|
|
552
710
|
// Remove file
|
|
553
711
|
await fs.remove(specPath);
|
|
554
712
|
|
|
713
|
+
// Clean up working state
|
|
714
|
+
try { deleteState(id, findProjectRoot()); } catch { /* non-fatal */ }
|
|
715
|
+
|
|
555
716
|
// Update registry
|
|
556
717
|
delete registry.specs[id];
|
|
557
718
|
await saveSpecsRegistry(registry);
|
|
@@ -580,6 +741,34 @@ async function closeSpec(id) {
|
|
|
580
741
|
return false;
|
|
581
742
|
}
|
|
582
743
|
|
|
744
|
+
// Block closure if owned by another session
|
|
745
|
+
const registry = await loadSpecsRegistry();
|
|
746
|
+
const existingEntry = registry.specs[id];
|
|
747
|
+
const currentSession = getAgentSessionId(findProjectRoot());
|
|
748
|
+
if (existingEntry?.owner && currentSession && existingEntry.owner !== currentSession) {
|
|
749
|
+
console.error(
|
|
750
|
+
chalk.red(
|
|
751
|
+
`Cannot close spec '${id}': owned by another session (${existingEntry.owner}). ` +
|
|
752
|
+
`Only the creator session can close a spec.`
|
|
753
|
+
)
|
|
754
|
+
);
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Block closure if active worktrees reference this spec (closing removes scope enforcement)
|
|
759
|
+
const referencingWorktrees = getWorktreesReferencingSpec(id);
|
|
760
|
+
if (referencingWorktrees.length > 0) {
|
|
761
|
+
const names = referencingWorktrees.join(', ');
|
|
762
|
+
console.error(
|
|
763
|
+
chalk.red(
|
|
764
|
+
`Cannot close spec '${id}': active worktree(s) [${names}] reference it. ` +
|
|
765
|
+
`Closing would remove scope enforcement while work is in progress. ` +
|
|
766
|
+
`Destroy the worktree(s) first with 'caws worktree destroy <name>'.`
|
|
767
|
+
)
|
|
768
|
+
);
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
|
|
583
772
|
return await updateSpec(id, { status: 'closed' });
|
|
584
773
|
}
|
|
585
774
|
|