@paths.design/caws-cli 9.2.0 → 9.3.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/dist/commands/specs.js +28 -15
- package/dist/commands/status.js +1 -1
- package/dist/commands/verify-acs.js +471 -0
- package/dist/index.js +13 -1
- package/dist/parallel/parallel-manager.js +5 -12
- package/dist/scaffold/cursor-hooks.js +0 -1
- package/dist/scaffold/git-hooks.js +18 -1
- package/dist/templates/.caws/tools/README.md +4 -7
- package/dist/templates/.caws/tools/scope-guard.js +115 -171
- package/dist/templates/.claude/hooks/audit.sh +25 -0
- package/dist/templates/.claude/hooks/block-dangerous.sh +39 -0
- package/dist/templates/.claude/hooks/lite-sprawl-check.sh +30 -2
- package/dist/templates/.claude/hooks/naming-check.sh +5 -2
- package/dist/templates/.claude/hooks/scope-guard.sh +66 -4
- package/dist/templates/.claude/hooks/session-log.sh +38 -5
- package/dist/templates/.claude/rules/worktree-isolation.md +4 -1
- package/dist/templates/.cursor/README.md +0 -9
- package/dist/templates/.cursor/hooks/audit.sh +1 -1
- package/dist/templates/.cursor/hooks/block-dangerous.sh +1 -0
- package/dist/templates/.cursor/hooks/scan-secrets.sh +8 -3
- package/dist/templates/.cursor/hooks.json +0 -8
- package/dist/templates/.vscode/launch.json +0 -12
- package/dist/utils/detection.js +37 -0
- package/dist/utils/project-analysis.js +0 -1
- package/dist/utils/spec-resolver.js +23 -10
- package/dist/worktree/worktree-manager.js +18 -1
- package/package.json +1 -1
- package/templates/.caws/tools/README.md +4 -7
- package/templates/.caws/tools/scope-guard.js +115 -171
- package/templates/.claude/hooks/audit.sh +25 -0
- package/templates/.claude/hooks/block-dangerous.sh +39 -0
- package/templates/.claude/hooks/lite-sprawl-check.sh +30 -2
- package/templates/.claude/hooks/naming-check.sh +5 -2
- package/templates/.claude/hooks/scope-guard.sh +66 -4
- package/templates/.claude/hooks/session-log.sh +38 -5
- package/templates/.claude/rules/worktree-isolation.md +4 -1
- package/templates/.cursor/README.md +0 -9
- package/templates/.cursor/hooks/audit.sh +1 -1
- package/templates/.cursor/hooks/block-dangerous.sh +1 -0
- package/templates/.cursor/hooks/scan-secrets.sh +8 -3
- package/templates/.cursor/hooks.json +0 -8
- package/templates/.vscode/launch.json +0 -12
- package/templates/.cursor/hooks/caws-tool-validation.sh +0 -121
|
@@ -157,6 +157,33 @@ def rel(path):
|
|
|
157
157
|
return path[len(cwd) + 1:]
|
|
158
158
|
return path or ""
|
|
159
159
|
|
|
160
|
+
def decode_structured_text_payload(raw):
|
|
161
|
+
"""Decode JSON-escaped text payloads (e.g., Agent/Task tool outputs)."""
|
|
162
|
+
if not isinstance(raw, str):
|
|
163
|
+
return raw
|
|
164
|
+
payload = raw.strip()
|
|
165
|
+
if not payload or payload[0] not in "[{":
|
|
166
|
+
return raw
|
|
167
|
+
try:
|
|
168
|
+
parsed = json.loads(payload)
|
|
169
|
+
except Exception:
|
|
170
|
+
return raw
|
|
171
|
+
|
|
172
|
+
text_blocks = []
|
|
173
|
+
if isinstance(parsed, dict):
|
|
174
|
+
parsed = [parsed]
|
|
175
|
+
if isinstance(parsed, list):
|
|
176
|
+
for item in parsed:
|
|
177
|
+
if not isinstance(item, dict):
|
|
178
|
+
continue
|
|
179
|
+
text = item.get("text")
|
|
180
|
+
if isinstance(text, str) and text.strip():
|
|
181
|
+
text_blocks.append(text)
|
|
182
|
+
|
|
183
|
+
if text_blocks:
|
|
184
|
+
return "\n\n".join(text_blocks)
|
|
185
|
+
return raw
|
|
186
|
+
|
|
160
187
|
# ---- Accumulate turns as chronological event timelines ----
|
|
161
188
|
turns = []
|
|
162
189
|
# Each turn: {user, timeline: [{kind, ...}, ...], edits, reads, searches, commands}
|
|
@@ -242,18 +269,24 @@ for line in sys.stdin:
|
|
|
242
269
|
tool_info = pending_tools.get(tid, {})
|
|
243
270
|
name = tool_info.get("name", "unknown")
|
|
244
271
|
|
|
245
|
-
#
|
|
246
|
-
#
|
|
247
|
-
|
|
272
|
+
# Always capture tool results for Bash, Task, Agent.
|
|
273
|
+
# For Read/Write/Edit, only capture if notable (errors, test output, etc.)
|
|
274
|
+
# to avoid dumping entire file contents into turn logs.
|
|
275
|
+
always_capture = name in ("Bash", "Task", "Agent")
|
|
276
|
+
notable = is_error
|
|
248
277
|
if not notable and content:
|
|
249
278
|
content_lower = content.lower()
|
|
250
279
|
notable = any(kw.lower() in content_lower for kw in NOTABLE_KW)
|
|
251
280
|
|
|
252
|
-
if notable and content:
|
|
281
|
+
if (always_capture or notable) and content:
|
|
253
282
|
# Cap file-content tools (full file reads/writes blow out turn files)
|
|
254
283
|
display = content
|
|
255
284
|
if name in ("Read", "Write", "Edit") and len(content) > 2000:
|
|
256
285
|
display = content[:2000] + "\n...(file content truncated)"
|
|
286
|
+
elif name in ("Task", "Agent"):
|
|
287
|
+
display = decode_structured_text_payload(content)
|
|
288
|
+
elif name == "Bash" and len(content) > 5000:
|
|
289
|
+
display = content[:5000] + "\n...(output truncated at 5000 chars)"
|
|
257
290
|
# Graft result onto the original tool_call entry (not a separate timeline item)
|
|
258
291
|
if tool_info:
|
|
259
292
|
tool_info["output"] = display
|
|
@@ -281,7 +314,7 @@ for i, turn in enumerate(turns):
|
|
|
281
314
|
md_lines = [f"# Turn {num}", ""]
|
|
282
315
|
|
|
283
316
|
if turn["user"]:
|
|
284
|
-
md_lines.extend([f"> ---user---\n{turn['user']}\n
|
|
317
|
+
md_lines.extend([f"> ---user---\n{turn['user']}\n---\/user---", ""])
|
|
285
318
|
|
|
286
319
|
for event in turn["timeline"]:
|
|
287
320
|
kind = event["kind"]
|
|
@@ -12,16 +12,19 @@ When multiple agents are working on this project, each agent MUST work in its ow
|
|
|
12
12
|
1. Check if worktrees exist: `caws worktree list` shows all active worktrees with last commit time and owner
|
|
13
13
|
2. If worktrees are active and you are on the base branch, switch to your assigned worktree
|
|
14
14
|
3. If no worktree exists for you, create one with `caws worktree create <name>` or `caws parallel setup <plan-file>`
|
|
15
|
+
4. **Never touch a worktree you did not create.** Do not destroy, prune, stash, or "clean up" another agent's worktree — even if it looks stale. Another agent may be actively working in it. If you think a worktree is abandoned, leave it alone and let the user decide.
|
|
15
16
|
|
|
16
17
|
## Forbidden operations when worktrees are active
|
|
17
18
|
|
|
18
19
|
- `git commit --amend` -- rewrites history that other agents depend on
|
|
20
|
+
- `git rebase` -- rewrites branch history; the hook blocks this automatically while worktrees are active. If you need code from main, create a new worktree from current main instead
|
|
21
|
+
- `git cherry-pick` -- replays commits across branches; blocked while worktrees are active to prevent cross-boundary contamination
|
|
19
22
|
- `git stash` / `git stash pop` -- stash is shared across all worktrees; using it can destroy another agent's uncommitted work
|
|
20
23
|
- `git reset --hard` -- discards work that other agents may depend on
|
|
21
24
|
- `git push --force` -- rewrites remote history
|
|
22
25
|
- Direct commits to the base branch -- only `merge(worktree):` and `wip(checkpoint):` formats are allowed
|
|
23
26
|
- Copying files between your worktree and the main repo directory -- defeats isolation
|
|
24
|
-
- Destroying another agent's
|
|
27
|
+
- Destroying another agent's worktree -- even with `--force`. If you did not create it, do not destroy it. Period.
|
|
25
28
|
|
|
26
29
|
## Merging worktree branches back to base
|
|
27
30
|
|
|
@@ -9,7 +9,6 @@ Cursor hooks enable seamless integration between CAWS and the Cursor IDE, provid
|
|
|
9
9
|
- **Real-time quality validation** as you code
|
|
10
10
|
- **Automatic spec validation** when editing working specs
|
|
11
11
|
- **Scope enforcement** preventing out-of-scope file access
|
|
12
|
-
- **Tool validation** for safe MCP execution
|
|
13
12
|
- **Quality monitoring** after file edits
|
|
14
13
|
|
|
15
14
|
## Hook Configuration
|
|
@@ -19,7 +18,6 @@ The `hooks.json` file defines when each hook runs:
|
|
|
19
18
|
```json
|
|
20
19
|
{
|
|
21
20
|
"beforeShellExecution": ["block-dangerous.sh", "audit.sh"],
|
|
22
|
-
"beforeMCPExecution": ["audit.sh", "caws-tool-validation.sh"],
|
|
23
21
|
"beforeReadFile": ["scan-secrets.sh", "caws-scope-guard.sh"],
|
|
24
22
|
"afterFileEdit": ["format.sh", "naming-check.sh", "validate-spec.sh", "caws-quality-check.sh", "audit.sh"],
|
|
25
23
|
"beforeSubmitPrompt": ["caws-scope-guard.sh", "audit.sh"],
|
|
@@ -43,12 +41,6 @@ The `hooks.json` file defines when each hook runs:
|
|
|
43
41
|
- **Blocks**: Yes (for out-of-scope file access)
|
|
44
42
|
- **Requires**: `.caws/working-spec.yaml`
|
|
45
43
|
|
|
46
|
-
#### `caws-tool-validation.sh`
|
|
47
|
-
- **Trigger**: `beforeMCPExecution`
|
|
48
|
-
- **Purpose**: Validates CAWS MCP tool calls for security
|
|
49
|
-
- **Blocks**: Yes (for dangerous operations or invalid waivers)
|
|
50
|
-
- **Validates**: Waiver creation, tool permissions, command safety
|
|
51
|
-
|
|
52
44
|
### General Security Hooks
|
|
53
45
|
|
|
54
46
|
#### `block-dangerous.sh`
|
|
@@ -242,7 +234,6 @@ Cursor Hooks ←→ CAWS CLI ←→ VS Code Extension
|
|
|
242
234
|
|
|
243
235
|
- **Git Hooks**: `.git/hooks/` for commit/push validation
|
|
244
236
|
- **VS Code Extension**: Rich UI for CAWS operations
|
|
245
|
-
- **MCP Server**: Agent tool integration
|
|
246
237
|
- **CAWS CLI**: Core functionality
|
|
247
238
|
|
|
248
239
|
### Data Flow
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# Cursor Hook: Audit Trail
|
|
3
3
|
#
|
|
4
4
|
# Purpose: Log all Cursor AI events for provenance tracking
|
|
5
|
-
# Event: All (beforeShellExecution,
|
|
5
|
+
# Event: All (beforeShellExecution, beforeReadFile,
|
|
6
6
|
# afterFileEdit, beforeSubmitPrompt, stop)
|
|
7
7
|
#
|
|
8
8
|
# @author @darianrosebrook
|
|
@@ -29,6 +29,7 @@ HARD_BLOCKS=(
|
|
|
29
29
|
"git commit --amend --no-edit" # Can rewrite commit history destructively
|
|
30
30
|
"git reset --hard" # Can lose uncommitted work and stashed changes
|
|
31
31
|
"git push --force" # Can overwrite remote repository history
|
|
32
|
+
"git rebase" # Rewrites branch history; blocked while worktrees are active
|
|
32
33
|
"dd if="
|
|
33
34
|
"mkfs"
|
|
34
35
|
"format c:"
|
|
@@ -28,14 +28,19 @@ if [[ "$FILE_PATH" =~ \.(pem|key|p12|pfx|cert|crt)$ ]]; then
|
|
|
28
28
|
fi
|
|
29
29
|
|
|
30
30
|
# Scan content for common secret patterns
|
|
31
|
-
|
|
31
|
+
# bearer requires 20+ chars to avoid false positives on short tokens in docs
|
|
32
|
+
# AKIA prefix is specific to AWS access keys
|
|
33
|
+
if echo "$CONTENT" | grep -qiE "(api[_-]?key\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{16,}|secret[_-]?key\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{16,}|password\s*[:=]\s*['\"]?[^\s'\"]{8,}|private[_-]?key\s*[:=]|access[_-]?token\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{16,}|[Bb]earer\s+[A-Za-z0-9_\-\.]{20,}|AKIA[0-9A-Z]{16})"; then
|
|
32
34
|
# Don't block, but warn
|
|
33
35
|
echo '{"permission":"allow","userMessage":"⚠️ Warning: Potential secrets detected in file. Ensure they are not committed.","agentMessage":"This file may contain secrets. Use placeholder values or environment variables."}' 2>/dev/null
|
|
34
36
|
exit 0
|
|
35
37
|
fi
|
|
36
38
|
|
|
37
|
-
# Check for common PII patterns (SSN, credit card
|
|
38
|
-
|
|
39
|
+
# Check for common PII patterns (SSN, credit card)
|
|
40
|
+
# SSN: exactly 3-2-4 digit pattern (not inside longer numbers)
|
|
41
|
+
# Credit card: require at least a Luhn-plausible 13-19 digit sequence with separators
|
|
42
|
+
if echo "$CONTENT" | grep -qE "(^|[^0-9])[0-9]{3}-[0-9]{2}-[0-9]{4}($|[^0-9])" || \
|
|
43
|
+
echo "$CONTENT" | grep -qE "(^|[^0-9])[0-9]{4}[- ][0-9]{4}[- ][0-9]{4}[- ][0-9]{4}($|[^0-9])"; then
|
|
39
44
|
echo '{"permission":"allow","userMessage":"⚠️ Warning: Potential PII detected. Ensure compliance with data protection policies.","agentMessage":"This file may contain PII (SSN, credit card). Use anonymized test data."}' 2>/dev/null
|
|
40
45
|
exit 0
|
|
41
46
|
fi
|
|
@@ -9,14 +9,6 @@
|
|
|
9
9
|
"command": "./.cursor/hooks/audit.sh"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
|
-
"beforeMCPExecution": [
|
|
13
|
-
{
|
|
14
|
-
"command": "./.cursor/hooks/audit.sh"
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
"command": "./.cursor/hooks/caws-tool-validation.sh"
|
|
18
|
-
}
|
|
19
|
-
],
|
|
20
12
|
"beforeReadFile": [
|
|
21
13
|
{
|
|
22
14
|
"command": "./.cursor/hooks/scan-secrets.sh"
|
|
@@ -1,18 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "0.2.0",
|
|
3
3
|
"configurations": [
|
|
4
|
-
{
|
|
5
|
-
"name": "Debug MCP Server",
|
|
6
|
-
"type": "node",
|
|
7
|
-
"request": "launch",
|
|
8
|
-
"program": "${workspaceFolder}/packages/caws-mcp-server/index.js",
|
|
9
|
-
"args": [],
|
|
10
|
-
"env": {
|
|
11
|
-
"NODE_ENV": "development",
|
|
12
|
-
"CAWS_DEBUG": "true"
|
|
13
|
-
},
|
|
14
|
-
"console": "integratedTerminal"
|
|
15
|
-
},
|
|
16
4
|
{
|
|
17
5
|
"name": "Debug CAWS CLI",
|
|
18
6
|
"type": "node",
|
package/dist/utils/detection.js
CHANGED
|
@@ -196,7 +196,44 @@ function detectCAWSSetup(cwd = process.cwd()) {
|
|
|
196
196
|
};
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Find the CAWS project root by walking up from startDir looking for .caws/
|
|
201
|
+
* Falls back to git root, then to process.cwd()
|
|
202
|
+
* @param {string} [startDir] - Directory to start searching from
|
|
203
|
+
* @returns {string} Project root directory path
|
|
204
|
+
*/
|
|
205
|
+
function findProjectRoot(startDir = process.cwd()) {
|
|
206
|
+
// Walk up looking for .caws/ directory
|
|
207
|
+
let dir = path.resolve(startDir);
|
|
208
|
+
const root = path.parse(dir).root;
|
|
209
|
+
while (dir !== root) {
|
|
210
|
+
if (fs.existsSync(path.join(dir, '.caws'))) {
|
|
211
|
+
return dir;
|
|
212
|
+
}
|
|
213
|
+
dir = path.dirname(dir);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Fallback: try git root
|
|
217
|
+
try {
|
|
218
|
+
const { execFileSync } = require('child_process');
|
|
219
|
+
const gitRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
220
|
+
encoding: 'utf8',
|
|
221
|
+
cwd: startDir,
|
|
222
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
223
|
+
}).trim();
|
|
224
|
+
if (gitRoot && fs.existsSync(path.join(gitRoot, '.caws'))) {
|
|
225
|
+
return gitRoot;
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
// Not a git repo or git not available
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Final fallback: cwd
|
|
232
|
+
return process.cwd();
|
|
233
|
+
}
|
|
234
|
+
|
|
199
235
|
module.exports = {
|
|
200
236
|
detectCAWSSetup,
|
|
201
237
|
findPackageRoot,
|
|
238
|
+
findProjectRoot,
|
|
202
239
|
};
|
|
@@ -347,7 +347,6 @@ function getTodoAnalyzerSuggestion(cwd = process.cwd()) {
|
|
|
347
347
|
suggestions.push(
|
|
348
348
|
' - Install Node.js: https://nodejs.org/ (then use: npx --yes @paths.design/quality-gates)'
|
|
349
349
|
);
|
|
350
|
-
suggestions.push(' - Use CAWS MCP server: caws quality-gates (via MCP)');
|
|
351
350
|
}
|
|
352
351
|
|
|
353
352
|
// Check for project-specific scripts (language-agnostic - if they exist, suggest them)
|
|
@@ -12,6 +12,7 @@ const chalk = require('chalk');
|
|
|
12
12
|
|
|
13
13
|
// Import SPEC_TYPES from constants for consistent display
|
|
14
14
|
const { SPEC_TYPES } = require('../constants/spec-types');
|
|
15
|
+
const { findProjectRoot } = require('./detection');
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Spec resolution priority:
|
|
@@ -22,6 +23,18 @@ const SPECS_DIR = '.caws/specs';
|
|
|
22
23
|
const LEGACY_SPEC = '.caws/working-spec.yaml';
|
|
23
24
|
const SPECS_REGISTRY = '.caws/specs/registry.json';
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Get the project root for spec resolution.
|
|
28
|
+
* Caches per process to avoid repeated filesystem walks.
|
|
29
|
+
*/
|
|
30
|
+
let _cachedProjectRoot = null;
|
|
31
|
+
function getProjectRoot() {
|
|
32
|
+
if (!_cachedProjectRoot) {
|
|
33
|
+
_cachedProjectRoot = findProjectRoot();
|
|
34
|
+
}
|
|
35
|
+
return _cachedProjectRoot;
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
/**
|
|
26
39
|
* Resolve spec file path based on priority
|
|
27
40
|
* @param {Object} options - Resolution options
|
|
@@ -36,7 +49,7 @@ async function resolveSpec(options = {}) {
|
|
|
36
49
|
|
|
37
50
|
// 1. Explicit file path takes highest priority
|
|
38
51
|
if (specFile) {
|
|
39
|
-
const explicitPath = path.isAbsolute(specFile) ? specFile : path.join(
|
|
52
|
+
const explicitPath = path.isAbsolute(specFile) ? specFile : path.join(getProjectRoot(), specFile);
|
|
40
53
|
|
|
41
54
|
if (await fs.pathExists(explicitPath)) {
|
|
42
55
|
const yaml = require('js-yaml');
|
|
@@ -55,7 +68,7 @@ async function resolveSpec(options = {}) {
|
|
|
55
68
|
|
|
56
69
|
// 2. Feature-specific spec (preferred for multi-agent)
|
|
57
70
|
if (specId) {
|
|
58
|
-
const featurePath = path.join(
|
|
71
|
+
const featurePath = path.join(getProjectRoot(), SPECS_DIR, `${specId}.yaml`);
|
|
59
72
|
|
|
60
73
|
if (await fs.pathExists(featurePath)) {
|
|
61
74
|
const yaml = require('js-yaml');
|
|
@@ -83,7 +96,7 @@ async function resolveSpec(options = {}) {
|
|
|
83
96
|
if (specIds.length === 1) {
|
|
84
97
|
// Single spec - use it automatically
|
|
85
98
|
const singleSpecId = specIds[0];
|
|
86
|
-
const singleSpecPath = path.join(
|
|
99
|
+
const singleSpecPath = path.join(getProjectRoot(), SPECS_DIR, registry.specs[singleSpecId].path);
|
|
87
100
|
|
|
88
101
|
if (await fs.pathExists(singleSpecPath)) {
|
|
89
102
|
const yaml = require('js-yaml');
|
|
@@ -179,7 +192,7 @@ async function resolveSpec(options = {}) {
|
|
|
179
192
|
}
|
|
180
193
|
|
|
181
194
|
// 4. Fall back to legacy working-spec.yaml (with warning)
|
|
182
|
-
const legacyPath = path.join(
|
|
195
|
+
const legacyPath = path.join(getProjectRoot(), LEGACY_SPEC);
|
|
183
196
|
|
|
184
197
|
if (await fs.pathExists(legacyPath)) {
|
|
185
198
|
const yaml = require('js-yaml');
|
|
@@ -211,7 +224,7 @@ async function resolveSpec(options = {}) {
|
|
|
211
224
|
* @returns {Promise<Object>} Registry data
|
|
212
225
|
*/
|
|
213
226
|
async function loadSpecsRegistry() {
|
|
214
|
-
const registryPath = path.join(
|
|
227
|
+
const registryPath = path.join(getProjectRoot(), SPECS_REGISTRY);
|
|
215
228
|
|
|
216
229
|
if (!(await fs.pathExists(registryPath))) {
|
|
217
230
|
return {
|
|
@@ -241,7 +254,7 @@ async function listAvailableSpecs() {
|
|
|
241
254
|
const specs = [];
|
|
242
255
|
|
|
243
256
|
// Check feature-specific specs
|
|
244
|
-
const specsDir = path.join(
|
|
257
|
+
const specsDir = path.join(getProjectRoot(), SPECS_DIR);
|
|
245
258
|
if (await fs.pathExists(specsDir)) {
|
|
246
259
|
const files = await fs.readdir(specsDir);
|
|
247
260
|
const yamlFiles = files.filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'));
|
|
@@ -257,7 +270,7 @@ async function listAvailableSpecs() {
|
|
|
257
270
|
|
|
258
271
|
specs.push({
|
|
259
272
|
id: spec.id || path.basename(file, path.extname(file)),
|
|
260
|
-
path: path.relative(
|
|
273
|
+
path: path.relative(getProjectRoot(), specPath),
|
|
261
274
|
type: 'feature',
|
|
262
275
|
title: spec.title || 'Untitled',
|
|
263
276
|
});
|
|
@@ -268,7 +281,7 @@ async function listAvailableSpecs() {
|
|
|
268
281
|
}
|
|
269
282
|
|
|
270
283
|
// Check legacy working-spec.yaml
|
|
271
|
-
const legacyPath = path.join(
|
|
284
|
+
const legacyPath = path.join(getProjectRoot(), LEGACY_SPEC);
|
|
272
285
|
if (await fs.pathExists(legacyPath)) {
|
|
273
286
|
try {
|
|
274
287
|
const yaml = require('js-yaml');
|
|
@@ -342,7 +355,7 @@ async function interactiveSpecSelection(specIds) {
|
|
|
342
355
|
async function checkMultiSpecStatus() {
|
|
343
356
|
const registry = await loadSpecsRegistry();
|
|
344
357
|
const hasFeatureSpecs = Object.keys(registry.specs ?? {}).length > 0;
|
|
345
|
-
const legacyPath = path.join(
|
|
358
|
+
const legacyPath = path.join(getProjectRoot(), LEGACY_SPEC);
|
|
346
359
|
const hasLegacySpec = await fs.pathExists(legacyPath);
|
|
347
360
|
|
|
348
361
|
return {
|
|
@@ -374,7 +387,7 @@ async function checkScopeConflicts(specIds) {
|
|
|
374
387
|
try {
|
|
375
388
|
spec = yaml.load(content);
|
|
376
389
|
} catch (yamlError) {
|
|
377
|
-
const relativePath = path.relative(
|
|
390
|
+
const relativePath = path.relative(getProjectRoot(), specPath);
|
|
378
391
|
throw new Error(
|
|
379
392
|
`Invalid YAML syntax in ${relativePath}: ${yamlError.message}\n` +
|
|
380
393
|
(yamlError.mark
|
|
@@ -343,10 +343,27 @@ function destroyWorktree(name, options = {}) {
|
|
|
343
343
|
`Worktree '${name}' belongs to another session${recency}.\n` +
|
|
344
344
|
` Owner: ${entry.owner}\n` +
|
|
345
345
|
` You: ${currentSession}\n` +
|
|
346
|
-
`Another agent may be actively working here
|
|
346
|
+
`Another agent may be actively working here.\n` +
|
|
347
|
+
`Do NOT destroy worktrees you did not create. Ask the user if cleanup is needed.`
|
|
347
348
|
);
|
|
348
349
|
}
|
|
349
350
|
|
|
351
|
+
// Even with --force, warn loudly when destroying another session's worktree
|
|
352
|
+
if (
|
|
353
|
+
force &&
|
|
354
|
+
entry.status === 'active' &&
|
|
355
|
+
entry.owner &&
|
|
356
|
+
currentSession &&
|
|
357
|
+
entry.owner !== currentSession
|
|
358
|
+
) {
|
|
359
|
+
const lastCommit = entry.branch ? getLastCommitInfo(entry.branch, root) : null;
|
|
360
|
+
const recency = lastCommit ? ` (last commit: ${lastCommit.age})` : '';
|
|
361
|
+
console.log(chalk.red(`\n ⚠ WARNING: Force-destroying worktree '${name}' owned by another session${recency}`));
|
|
362
|
+
console.log(chalk.red(` Owner: ${entry.owner}`));
|
|
363
|
+
console.log(chalk.red(` You: ${currentSession}`));
|
|
364
|
+
console.log(chalk.red(` If the other agent is still running, this WILL break their work.\n`));
|
|
365
|
+
}
|
|
366
|
+
|
|
350
367
|
// Auto-force when the branch is already merged to its base branch.
|
|
351
368
|
// Dirty files in a merged worktree are definitionally stale.
|
|
352
369
|
const merged = entry.branch && entry.baseBranch
|
package/package.json
CHANGED
|
@@ -4,18 +4,15 @@ This directory contains CAWS-specific tools that aren't available in the CLI.
|
|
|
4
4
|
|
|
5
5
|
## scope-guard.js
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Checks whether a file is within scope of active working-spec and feature specs. Used by Cursor hooks for scope validation on file attachments.
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
#
|
|
11
|
-
node .caws/tools/scope-guard.js
|
|
10
|
+
# Check if a file is in scope
|
|
11
|
+
node .caws/tools/scope-guard.js check src/index.js
|
|
12
12
|
|
|
13
|
-
#
|
|
14
|
-
node .caws/tools/scope-guard.js check .caws/working-spec.yaml
|
|
13
|
+
# Exit code 0 = in scope, 1 = out of scope
|
|
15
14
|
```
|
|
16
15
|
|
|
17
16
|
**Usage in Cursor Hooks:**
|
|
18
17
|
|
|
19
18
|
The `.cursor/hooks/scope-guard.sh` hook automatically uses this tool to validate file attachments against working spec scope boundaries.
|
|
20
|
-
|
|
21
|
-
|