@paths.design/caws-cli 9.3.2 → 10.1.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 +71 -32
- package/dist/budget-derivation.js +221 -74
- 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 +41 -15
- package/dist/commands/gates.js +149 -0
- package/dist/commands/init.js +150 -19
- package/dist/commands/iterate.js +81 -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/scope.js +264 -0
- package/dist/commands/sidecar.js +74 -0
- package/dist/commands/specs.js +381 -45
- package/dist/commands/status.js +117 -9
- package/dist/commands/templates.js +0 -8
- package/dist/commands/tutorial.js +10 -9
- package/dist/commands/validate.js +70 -6
- package/dist/commands/verify-acs.js +48 -76
- package/dist/commands/waivers.js +212 -13
- package/dist/commands/worktree.js +131 -26
- package/dist/error-handler.js +2 -13
- package/dist/gates/budget-limit.js +121 -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 +109 -0
- package/dist/gates/todo-detection.js +205 -0
- package/dist/index.js +157 -151
- package/dist/parallel/parallel-manager.js +3 -3
- package/dist/policy/PolicyManager.js +51 -17
- 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 +105 -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 +112 -0
- package/dist/templates/.caws/schemas/scope.schema.json +3 -3
- package/dist/templates/.caws/schemas/waivers.schema.json +96 -20
- package/dist/templates/.caws/schemas/working-spec.schema.json +264 -57
- package/dist/templates/.caws/schemas/worktrees.schema.json +3 -1
- package/dist/templates/.caws/templates/working-spec.template.yml +10 -4
- package/dist/templates/.caws/tools/scope-guard.js +66 -15
- package/dist/templates/.claude/README.md +1 -1
- 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/protected-paths.sh +39 -0
- package/dist/templates/.claude/hooks/quality-check.sh +23 -10
- package/dist/templates/.claude/hooks/scope-guard.sh +136 -55
- 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 +97 -4
- package/dist/templates/.claude/settings.json +31 -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 +77 -8
- package/dist/templates/agents.md +50 -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/event-log.js +584 -0
- package/dist/utils/event-renderer.js +521 -0
- 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 +50 -0
- package/dist/utils/spec-resolver.js +93 -21
- package/dist/utils/working-state.js +530 -0
- package/dist/validation/spec-validation.js +191 -31
- package/dist/waivers-manager.js +144 -6
- package/dist/worktree/worktree-manager.js +598 -95
- package/package.json +9 -8
- package/templates/.caws/schemas/policy.schema.json +112 -0
- package/templates/.caws/schemas/scope.schema.json +3 -3
- package/templates/.caws/schemas/waivers.schema.json +96 -20
- package/templates/.caws/schemas/working-spec.schema.json +264 -57
- package/templates/.caws/schemas/worktrees.schema.json +3 -1
- package/templates/.caws/templates/working-spec.template.yml +10 -4
- package/templates/.caws/tools/scope-guard.js +66 -15
- package/templates/.claude/README.md +1 -1
- 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/protected-paths.sh +39 -0
- package/templates/.claude/hooks/quality-check.sh +23 -10
- package/templates/.claude/hooks/scope-guard.sh +136 -55
- 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 +97 -4
- package/templates/.claude/settings.json +31 -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 +77 -8
- package/templates/{AGENTS.md → agents.md} +50 -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
package/dist/commands/specs.js
CHANGED
|
@@ -15,6 +15,37 @@ 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
|
+
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
|
+
}
|
|
18
49
|
|
|
19
50
|
/**
|
|
20
51
|
* Specs directory structure — anchored to the CAWS project root,
|
|
@@ -26,6 +57,35 @@ function getSpecsDir() {
|
|
|
26
57
|
function getSpecsRegistry() {
|
|
27
58
|
return path.join(findProjectRoot(), '.caws', 'specs', 'registry.json');
|
|
28
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
|
+
}
|
|
29
89
|
// Legacy constants kept for backward compatibility in tests
|
|
30
90
|
const SPECS_DIR = '.caws/specs';
|
|
31
91
|
const SPECS_REGISTRY = '.caws/specs/registry.json';
|
|
@@ -68,6 +128,98 @@ async function saveSpecsRegistry(registry) {
|
|
|
68
128
|
await fs.writeFile(registryPath, JSON.stringify(registry, null, 2));
|
|
69
129
|
}
|
|
70
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
|
+
|
|
71
223
|
/**
|
|
72
224
|
* List all spec files in the specs directory
|
|
73
225
|
* @returns {Promise<Array>} Array of spec file info
|
|
@@ -192,8 +344,28 @@ async function createSpec(id, options = {}) {
|
|
|
192
344
|
}
|
|
193
345
|
}
|
|
194
346
|
|
|
195
|
-
// If we got here via override choice,
|
|
347
|
+
// If we got here via override choice, check ownership and worktree associations
|
|
196
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
|
+
}
|
|
197
369
|
console.log(chalk.yellow('Overriding existing spec...'));
|
|
198
370
|
}
|
|
199
371
|
|
|
@@ -232,6 +404,11 @@ async function createSpec(id, options = {}) {
|
|
|
232
404
|
contracts: [],
|
|
233
405
|
};
|
|
234
406
|
|
|
407
|
+
const detectedWorktree = detectCurrentWorktreeName();
|
|
408
|
+
if (detectedWorktree) {
|
|
409
|
+
defaultSpec.worktree = detectedWorktree;
|
|
410
|
+
}
|
|
411
|
+
|
|
235
412
|
// Merge template, but preserve required structure
|
|
236
413
|
// Map template.criteria to acceptance if present
|
|
237
414
|
const templateAcceptance = template?.criteria || template?.acceptance;
|
|
@@ -280,27 +457,9 @@ async function createSpec(id, options = {}) {
|
|
|
280
457
|
await fs.writeFile(filePath, yamlContent);
|
|
281
458
|
|
|
282
459
|
// Validate written file (YAML syntax and structure)
|
|
460
|
+
let parsedSpec;
|
|
283
461
|
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
|
-
}
|
|
462
|
+
parsedSpec = await validateAndReadSpecFile(filePath);
|
|
304
463
|
} catch (error) {
|
|
305
464
|
// Clean up invalid file if it exists
|
|
306
465
|
if (await fs.pathExists(filePath)) {
|
|
@@ -319,25 +478,86 @@ async function createSpec(id, options = {}) {
|
|
|
319
478
|
|
|
320
479
|
// Update registry
|
|
321
480
|
const registry = await loadSpecsRegistry();
|
|
322
|
-
registry.specs[id] =
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
updated_at: specContent.updated_at,
|
|
328
|
-
};
|
|
481
|
+
registry.specs[id] = buildRegistryEntryFromSpec(
|
|
482
|
+
parsedSpec,
|
|
483
|
+
fileName,
|
|
484
|
+
getAgentSessionId(findProjectRoot())
|
|
485
|
+
);
|
|
329
486
|
await saveSpecsRegistry(registry);
|
|
330
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
|
+
|
|
331
551
|
return {
|
|
332
552
|
id,
|
|
333
553
|
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,
|
|
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,
|
|
341
561
|
};
|
|
342
562
|
}
|
|
343
563
|
|
|
@@ -359,7 +579,7 @@ async function loadSpec(id) {
|
|
|
359
579
|
const content = await fs.readFile(specPath, 'utf8');
|
|
360
580
|
return yaml.load(content);
|
|
361
581
|
} catch (error) {
|
|
362
|
-
|
|
582
|
+
throw new Error(`Failed to load spec '${id}' from ${specPath}: ${error.message}`);
|
|
363
583
|
}
|
|
364
584
|
}
|
|
365
585
|
|
|
@@ -392,18 +612,28 @@ async function updateSpec(id, updates = {}) {
|
|
|
392
612
|
...updates,
|
|
393
613
|
updated_at: new Date().toISOString(),
|
|
394
614
|
};
|
|
615
|
+
const normalizedSpec = normalizeSpecForValidation(updatedSpec);
|
|
395
616
|
|
|
396
|
-
//
|
|
617
|
+
// Write back to file
|
|
397
618
|
const registry = await loadSpecsRegistry();
|
|
398
|
-
registry.specs[id].
|
|
399
|
-
|
|
400
|
-
|
|
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}`);
|
|
401
629
|
}
|
|
402
|
-
await saveSpecsRegistry(registry);
|
|
403
630
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
631
|
+
registry.specs[id] = buildRegistryEntryFromSpec(
|
|
632
|
+
parsedSpec,
|
|
633
|
+
registry.specs[id].path,
|
|
634
|
+
registry.specs[id].owner || null
|
|
635
|
+
);
|
|
636
|
+
await saveSpecsRegistry(registry);
|
|
407
637
|
|
|
408
638
|
return true;
|
|
409
639
|
}
|
|
@@ -547,15 +777,51 @@ async function deleteSpec(id) {
|
|
|
547
777
|
return false;
|
|
548
778
|
}
|
|
549
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
|
+
|
|
550
800
|
const specPath = path.join(getSpecsDir(), registry.specs[id].path);
|
|
551
801
|
|
|
552
802
|
// Remove file
|
|
553
803
|
await fs.remove(specPath);
|
|
554
804
|
|
|
805
|
+
// Clean up working state
|
|
806
|
+
try { deleteState(id, findProjectRoot()); } catch { /* non-fatal */ }
|
|
807
|
+
|
|
555
808
|
// Update registry
|
|
556
809
|
delete registry.specs[id];
|
|
557
810
|
await saveSpecsRegistry(registry);
|
|
558
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
|
+
|
|
559
825
|
return true;
|
|
560
826
|
}
|
|
561
827
|
|
|
@@ -580,7 +846,77 @@ async function closeSpec(id) {
|
|
|
580
846
|
return false;
|
|
581
847
|
}
|
|
582
848
|
|
|
583
|
-
|
|
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;
|
|
584
920
|
}
|
|
585
921
|
|
|
586
922
|
/**
|