@paths.design/caws-cli 10.0.1 → 10.2.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.
Files changed (60) hide show
  1. package/README.md +13 -5
  2. package/dist/budget-derivation.js +221 -74
  3. package/dist/commands/agents.js +124 -0
  4. package/dist/commands/evaluate.js +26 -12
  5. package/dist/commands/gates.js +31 -4
  6. package/dist/commands/init.js +7 -4
  7. package/dist/commands/iterate.js +7 -3
  8. package/dist/commands/scope.js +264 -0
  9. package/dist/commands/sidecar.js +6 -3
  10. package/dist/commands/specs.js +359 -4
  11. package/dist/commands/status.js +29 -4
  12. package/dist/commands/templates.js +0 -8
  13. package/dist/commands/validate.js +34 -13
  14. package/dist/commands/verify-acs.js +25 -10
  15. package/dist/commands/waivers.js +147 -5
  16. package/dist/commands/worktree.js +200 -4
  17. package/dist/gates/budget-limit.js +6 -1
  18. package/dist/gates/scope-boundary.js +26 -7
  19. package/dist/gates/spec-completeness.js +8 -1
  20. package/dist/index.js +56 -0
  21. package/dist/policy/PolicyManager.js +14 -7
  22. package/dist/session/session-manager.js +34 -0
  23. package/dist/templates/.caws/schemas/policy.schema.json +101 -34
  24. package/dist/templates/.caws/schemas/scope.schema.json +3 -3
  25. package/dist/templates/.caws/schemas/waivers.schema.json +91 -21
  26. package/dist/templates/.caws/schemas/working-spec.schema.json +253 -89
  27. package/dist/templates/.caws/templates/working-spec.template.yml +3 -1
  28. package/dist/templates/.caws/tools/scope-guard.js +66 -15
  29. package/dist/templates/.claude/README.md +1 -1
  30. package/dist/templates/.claude/hooks/protected-paths.sh +39 -0
  31. package/dist/templates/.claude/hooks/scope-guard.sh +106 -27
  32. package/dist/templates/.claude/hooks/worktree-write-guard.sh +96 -3
  33. package/dist/templates/.claude/rules/worktree-isolation.md +21 -3
  34. package/dist/templates/.claude/settings.json +5 -0
  35. package/dist/templates/CLAUDE.md +56 -0
  36. package/dist/templates/agents.md +47 -0
  37. package/dist/utils/agent-display.js +210 -0
  38. package/dist/utils/agent-session.js +142 -0
  39. package/dist/utils/event-log.js +584 -0
  40. package/dist/utils/event-renderer.js +521 -0
  41. package/dist/utils/schema-validator.js +10 -2
  42. package/dist/utils/working-state.js +25 -0
  43. package/dist/validation/spec-validation.js +102 -9
  44. package/dist/waivers-manager.js +84 -0
  45. package/dist/worktree/worktree-manager.js +593 -26
  46. package/package.json +5 -4
  47. package/templates/.caws/schemas/policy.schema.json +101 -34
  48. package/templates/.caws/schemas/scope.schema.json +3 -3
  49. package/templates/.caws/schemas/waivers.schema.json +91 -21
  50. package/templates/.caws/schemas/working-spec.schema.json +253 -89
  51. package/templates/.caws/templates/working-spec.template.yml +3 -1
  52. package/templates/.caws/tools/scope-guard.js +66 -15
  53. package/templates/.claude/README.md +1 -1
  54. package/templates/.claude/hooks/protected-paths.sh +39 -0
  55. package/templates/.claude/hooks/scope-guard.sh +106 -27
  56. package/templates/.claude/hooks/worktree-write-guard.sh +96 -3
  57. package/templates/.claude/rules/worktree-isolation.md +21 -3
  58. package/templates/.claude/settings.json +5 -0
  59. package/templates/CLAUDE.md +56 -0
  60. package/templates/agents.md +47 -0
@@ -0,0 +1,264 @@
1
+ /**
2
+ * @fileoverview CAWS Scope CLI Command
3
+ * Inspects and displays effective scope boundaries for the current context
4
+ * @author @darianrosebrook
5
+ */
6
+
7
+ const chalk = require('chalk');
8
+ const path = require('path');
9
+ const fs = require('fs-extra');
10
+ const yaml = require('js-yaml');
11
+ const {
12
+ getRepoRoot,
13
+ loadRegistry,
14
+ findFeatureSpecPath,
15
+ WORKTREES_DIR,
16
+ } = require('../worktree/worktree-manager');
17
+
18
+ /**
19
+ * Handle scope subcommands
20
+ * @param {string} subcommand - Subcommand name
21
+ * @param {Object} options - Command options
22
+ */
23
+ async function scopeCommand(subcommand, options = {}) {
24
+ try {
25
+ switch (subcommand) {
26
+ case 'show':
27
+ return handleShow(options);
28
+ default:
29
+ console.error(chalk.red(`Unknown scope subcommand: ${subcommand}`));
30
+ console.log(chalk.blue('Available: show'));
31
+ process.exit(1);
32
+ }
33
+ } catch (error) {
34
+ console.error(chalk.red(`${error.message}`));
35
+ process.exit(1);
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Detect if current working directory is inside a worktree
41
+ * @param {string} root - Repository root
42
+ * @returns {{ inWorktree: boolean, worktreeName: string|null }}
43
+ */
44
+ function detectWorktreeContext(root) {
45
+ const cwd = process.cwd();
46
+ const worktreesBase = path.join(root, WORKTREES_DIR);
47
+
48
+ if (!cwd.startsWith(worktreesBase + path.sep) && cwd !== worktreesBase) {
49
+ return { inWorktree: false, worktreeName: null };
50
+ }
51
+
52
+ // Extract worktree name: first path segment after the worktrees dir
53
+ const relative = path.relative(worktreesBase, cwd);
54
+ const worktreeName = relative.split(path.sep)[0];
55
+
56
+ if (!worktreeName) {
57
+ return { inWorktree: false, worktreeName: null };
58
+ }
59
+
60
+ return { inWorktree: true, worktreeName };
61
+ }
62
+
63
+ /**
64
+ * Load a spec file and return its parsed contents
65
+ * @param {string} specPath - Absolute path to spec YAML
66
+ * @returns {Object|null}
67
+ */
68
+ function loadSpec(specPath) {
69
+ try {
70
+ if (!fs.existsSync(specPath)) return null;
71
+ const content = fs.readFileSync(specPath, 'utf8');
72
+ return yaml.load(content);
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Find all active spec files in .caws/specs/
80
+ * @param {string} root - Repository root
81
+ * @returns {Array<{ id: string, path: string, data: Object }>}
82
+ */
83
+ function findAllActiveSpecs(root) {
84
+ const specsDir = path.join(root, '.caws', 'specs');
85
+ if (!fs.existsSync(specsDir)) return [];
86
+
87
+ const files = fs.readdirSync(specsDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
88
+ const specs = [];
89
+
90
+ for (const file of files) {
91
+ const specPath = path.join(specsDir, file);
92
+ const data = loadSpec(specPath);
93
+ if (!data) continue;
94
+
95
+ // Skip closed/archived specs
96
+ const status = (data.status || '').toLowerCase();
97
+ if (status === 'closed' || status === 'archived') continue;
98
+
99
+ const id = path.basename(file, path.extname(file));
100
+ specs.push({ id, path: specPath, data });
101
+ }
102
+
103
+ return specs;
104
+ }
105
+
106
+ /**
107
+ * Print scope patterns for a spec
108
+ * @param {Object} data - Parsed spec YAML
109
+ * @param {string} indent - Indentation prefix
110
+ */
111
+ function printScopePatterns(data, indent = ' ') {
112
+ const scope = data.scope || {};
113
+ const scopeIn = scope.in || scope.include || [];
114
+ const scopeOut = scope.out || scope.exclude || [];
115
+
116
+ if (scopeIn.length > 0) {
117
+ console.log(chalk.green(`${indent}scope.in:`));
118
+ for (const pattern of scopeIn) {
119
+ console.log(chalk.gray(`${indent} - ${pattern}`));
120
+ }
121
+ } else {
122
+ console.log(chalk.yellow(`${indent}scope.in: (none defined)`));
123
+ }
124
+
125
+ if (scopeOut.length > 0) {
126
+ console.log(chalk.red(`${indent}scope.out:`));
127
+ for (const pattern of scopeOut) {
128
+ console.log(chalk.gray(`${indent} - ${pattern}`));
129
+ }
130
+ } else {
131
+ console.log(chalk.gray(`${indent}scope.out: (none)`));
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Handle the 'show' subcommand
137
+ * @param {Object} options - Command options
138
+ */
139
+ function handleShow(_options) {
140
+ const root = getRepoRoot();
141
+ const { inWorktree, worktreeName } = detectWorktreeContext(root);
142
+
143
+ console.log(chalk.bold.cyan('CAWS Scope Inspector'));
144
+ console.log(chalk.cyan('='.repeat(50)));
145
+ console.log('');
146
+
147
+ if (inWorktree) {
148
+ return handleAuthoritativeMode(root, worktreeName);
149
+ } else {
150
+ return handleUnionMode(root);
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Handle authoritative mode: agent is inside a worktree with a bound spec
156
+ * @param {string} root - Repository root
157
+ * @param {string} worktreeName - Name of the worktree
158
+ */
159
+ function handleAuthoritativeMode(root, worktreeName) {
160
+ console.log(chalk.white(`Worktree: ${chalk.bold(worktreeName)}`));
161
+
162
+ const registry = loadRegistry(root);
163
+ const entry = registry.worktrees ? registry.worktrees[worktreeName] : null;
164
+
165
+ if (!entry) {
166
+ console.log(chalk.red(`Worktree '${worktreeName}' not found in registry.`));
167
+ console.log(chalk.yellow('The scope guard is operating in union mode (all active specs).'));
168
+ return handleUnionMode(root);
169
+ }
170
+
171
+ const specId = entry.specId;
172
+
173
+ if (!specId) {
174
+ console.log(chalk.yellow('Mode: union (no spec bound to this worktree)'));
175
+ console.log('');
176
+ console.log(chalk.yellow('This worktree has no spec binding. The scope guard checks'));
177
+ console.log(chalk.yellow('against the union of all active specs.'));
178
+ console.log('');
179
+ console.log(chalk.blue('To bind a spec: caws worktree bind <spec-id>'));
180
+ console.log('');
181
+ return handleUnionMode(root);
182
+ }
183
+
184
+ // Load the spec
185
+ const specPath = findFeatureSpecPath(root, specId);
186
+ if (!specPath) {
187
+ console.log(chalk.red(`Bound spec '${specId}' not found on disk.`));
188
+ console.log(chalk.yellow('Fix: recreate the spec or rebind with a valid spec ID.'));
189
+ console.log(chalk.blue(` caws worktree bind <valid-spec-id>`));
190
+ return;
191
+ }
192
+
193
+ const specData = loadSpec(specPath);
194
+ if (!specData) {
195
+ console.log(chalk.red(`Failed to parse spec file: ${specPath}`));
196
+ return;
197
+ }
198
+
199
+ console.log(chalk.green(`Mode: authoritative (single bound spec)`));
200
+ console.log(chalk.white(`Spec: ${chalk.bold(specId)}`));
201
+ if (specData.title) {
202
+ console.log(chalk.gray(`Title: ${specData.title}`));
203
+ }
204
+ console.log('');
205
+
206
+ // Print scope patterns
207
+ printScopePatterns(specData);
208
+ console.log('');
209
+
210
+ // Check binding health: mutual reference
211
+ const specWorktreeRef = specData.worktree || null;
212
+ const registrySpecRef = specId;
213
+
214
+ if (specWorktreeRef !== worktreeName) {
215
+ console.log(chalk.yellow('Binding health: BROKEN'));
216
+ console.log(chalk.yellow(` Registry points to spec '${registrySpecRef}'`));
217
+ console.log(chalk.yellow(` Spec 'worktree' field: ${specWorktreeRef || '(missing)'} (expected: ${worktreeName})`));
218
+ console.log('');
219
+ console.log(chalk.blue(`Fix: caws worktree bind ${specId}`));
220
+ } else {
221
+ console.log(chalk.green('Binding health: OK'));
222
+ console.log(chalk.gray(` Registry -> spec: ${registrySpecRef}`));
223
+ console.log(chalk.gray(` Spec -> worktree: ${specWorktreeRef}`));
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Handle union mode: no worktree or no spec binding
229
+ * @param {string} root - Repository root
230
+ */
231
+ function handleUnionMode(root) {
232
+ const specs = findAllActiveSpecs(root);
233
+
234
+ if (specs.length === 0) {
235
+ console.log(chalk.gray('Mode: union (no active specs found)'));
236
+ console.log('');
237
+ console.log(chalk.gray('No active feature specs in .caws/specs/.'));
238
+ console.log(chalk.gray('The scope guard has no patterns to enforce.'));
239
+ console.log('');
240
+ console.log(chalk.blue('Create a spec: caws specs create <id> --title "description"'));
241
+ return;
242
+ }
243
+
244
+ console.log(chalk.white('Mode: union (checking all active specs)'));
245
+ console.log(chalk.gray(`Active specs: ${specs.length}`));
246
+ console.log('');
247
+
248
+ for (const spec of specs) {
249
+ const statusLabel = spec.data.status || 'draft';
250
+ console.log(chalk.white(` ${chalk.bold(spec.id)} [${statusLabel}]`));
251
+ if (spec.data.title) {
252
+ console.log(chalk.gray(` Title: ${spec.data.title}`));
253
+ }
254
+ printScopePatterns(spec.data, ' ');
255
+
256
+ // Check if this spec has a worktree binding
257
+ if (spec.data.worktree) {
258
+ console.log(chalk.gray(` worktree: ${spec.data.worktree}`));
259
+ }
260
+ console.log('');
261
+ }
262
+ }
263
+
264
+ module.exports = { scopeCommand };
@@ -7,7 +7,10 @@
7
7
 
8
8
  const chalk = require('chalk');
9
9
  const { resolveSpec } = require('../utils/spec-resolver');
10
- const { loadState } = require('../utils/working-state');
10
+ // EVLOG-002 Phase 2 read flip: sidecars read state from the event log via the
11
+ // pure renderer. loadStateFromEvents matches loadState's null contract exactly,
12
+ // so the existing "state may be null — sidecars handle that" behavior stays.
13
+ const { loadStateFromEvents } = require('../utils/event-renderer');
11
14
  const { commandWrapper } = require('../utils/command-wrapper');
12
15
  const { SIDECARS, formatSidecarText } = require('../sidecars');
13
16
 
@@ -45,8 +48,8 @@ async function sidecarCommand(subcommand, options = {}) {
45
48
  process.exit(1);
46
49
  }
47
50
 
48
- // Load working state (may be null — sidecars handle that)
49
- const state = loadState(spec.id);
51
+ // Load working state (may be null — sidecars handle that; EVLOG-002: from event log)
52
+ const state = loadStateFromEvents(spec.id);
50
53
 
51
54
  // Build sidecar-specific options
52
55
  const sidecarOptions = {};