@paths.design/caws-cli 8.3.0 → 9.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.
Files changed (56) hide show
  1. package/dist/commands/init.d.ts.map +1 -1
  2. package/dist/commands/parallel.d.ts +7 -0
  3. package/dist/commands/parallel.d.ts.map +1 -0
  4. package/dist/commands/parallel.js +238 -0
  5. package/dist/commands/session.d.ts +7 -0
  6. package/dist/commands/session.d.ts.map +1 -0
  7. package/dist/commands/specs.d.ts +6 -0
  8. package/dist/commands/specs.d.ts.map +1 -1
  9. package/dist/commands/specs.js +55 -2
  10. package/dist/commands/status.d.ts.map +1 -1
  11. package/dist/commands/status.js +13 -3
  12. package/dist/commands/tutorial.js +0 -2
  13. package/dist/commands/waivers.d.ts.map +1 -1
  14. package/dist/constants/spec-types.d.ts +52 -0
  15. package/dist/constants/spec-types.d.ts.map +1 -1
  16. package/dist/constants/spec-types.js +25 -2
  17. package/dist/index.js +43 -2
  18. package/dist/parallel/parallel-manager.d.ts +67 -0
  19. package/dist/parallel/parallel-manager.d.ts.map +1 -0
  20. package/dist/parallel/parallel-manager.js +440 -0
  21. package/dist/scaffold/claude-hooks.d.ts.map +1 -1
  22. package/dist/scaffold/claude-hooks.js +78 -2
  23. package/dist/scaffold/git-hooks.d.ts.map +1 -1
  24. package/dist/scaffold/git-hooks.js +237 -73
  25. package/dist/scaffold/index.d.ts.map +1 -1
  26. package/dist/session/session-manager.d.ts +94 -0
  27. package/dist/session/session-manager.d.ts.map +1 -0
  28. package/dist/session/session-manager.js +14 -0
  29. package/dist/templates/.claude/hooks/scope-guard.sh +67 -21
  30. package/dist/templates/.claude/hooks/session-caws-status.sh +117 -0
  31. package/dist/templates/.claude/hooks/session-log.sh +528 -0
  32. package/dist/templates/.claude/hooks/stop-worktree-check.sh +46 -0
  33. package/dist/templates/.claude/hooks/worktree-guard.sh +207 -0
  34. package/dist/templates/.claude/hooks/worktree-write-guard.sh +84 -0
  35. package/dist/templates/.claude/rules/git-safety.md +26 -0
  36. package/dist/templates/.claude/rules/worktree-isolation.md +51 -0
  37. package/dist/templates/.claude/settings.json +15 -0
  38. package/dist/utils/gitignore-updater.d.ts +1 -1
  39. package/dist/utils/gitignore-updater.d.ts.map +1 -1
  40. package/dist/utils/gitignore-updater.js +3 -0
  41. package/dist/utils/ide-detection.d.ts +89 -0
  42. package/dist/utils/ide-detection.d.ts.map +1 -0
  43. package/dist/validation/spec-validation.d.ts.map +1 -1
  44. package/dist/validation/spec-validation.js +16 -0
  45. package/dist/worktree/worktree-manager.d.ts.map +1 -1
  46. package/dist/worktree/worktree-manager.js +31 -21
  47. package/package.json +2 -2
  48. package/templates/.claude/hooks/scope-guard.sh +67 -21
  49. package/templates/.claude/hooks/session-caws-status.sh +117 -0
  50. package/templates/.claude/hooks/session-log.sh +528 -0
  51. package/templates/.claude/hooks/stop-worktree-check.sh +46 -0
  52. package/templates/.claude/hooks/worktree-guard.sh +207 -0
  53. package/templates/.claude/hooks/worktree-write-guard.sh +84 -0
  54. package/templates/.claude/rules/git-safety.md +26 -0
  55. package/templates/.claude/rules/worktree-isolation.md +51 -0
  56. package/templates/.claude/settings.json +15 -0
@@ -236,6 +236,128 @@ if [ "$YAML_VALIDATION_FAILED" = true ]; then
236
236
  exit 1
237
237
  fi
238
238
 
239
+ # ===== CAWS Multi-Agent Safety Guard =====
240
+ # Prevents unsafe concurrent operations on shared branches
241
+
242
+ if [ -d ".caws" ]; then
243
+ CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
244
+
245
+ # Guard 1a: Block commits on base branch when parallel worktrees are active (caws parallel)
246
+ if [ -f ".caws/parallel.json" ] && command -v node >/dev/null 2>&1; then
247
+ PARALLEL_BASE=$(node -e "
248
+ try {
249
+ var reg = JSON.parse(require('fs').readFileSync('.caws/parallel.json', 'utf8'));
250
+ console.log(reg.baseBranch || '');
251
+ } catch(e) { console.log(''); }
252
+ " 2>/dev/null)
253
+
254
+ if [ -n "$PARALLEL_BASE" ] && [ "$CURRENT_BRANCH" = "$PARALLEL_BASE" ]; then
255
+ AGENT_COUNT=$(node -e "
256
+ try {
257
+ var reg = JSON.parse(require('fs').readFileSync('.caws/parallel.json', 'utf8'));
258
+ console.log((reg.agents || []).length);
259
+ } catch(e) { console.log('0'); }
260
+ " 2>/dev/null)
261
+
262
+ if [ "$AGENT_COUNT" -gt 0 ] 2>/dev/null; then
263
+ echo "BLOCKED: Committing to '$CURRENT_BRANCH' while $AGENT_COUNT parallel agent worktree(s) are active."
264
+ echo " Active agents are working in isolated worktrees."
265
+ echo " Committing to the base branch risks interleaved history and merge conflicts."
266
+ echo ""
267
+ echo " To see parallel status: caws parallel status"
268
+ echo " To merge agent work: caws parallel merge"
269
+ echo " To override (unsafe): git commit --no-verify"
270
+ exit 1
271
+ fi
272
+ fi
273
+ fi
274
+
275
+ # Guard 1b: Block commits on base branch when ANY active worktrees exist (caws worktree create)
276
+ if [ -f ".caws/worktrees.json" ] && command -v node >/dev/null 2>&1; then
277
+ ACTIVE_WORKTREES=$(node -e "
278
+ try {
279
+ var reg = JSON.parse(require('fs').readFileSync('.caws/worktrees.json', 'utf8'));
280
+ var wts = Object.values(reg.worktrees || {});
281
+ var active = wts.filter(function(w) {
282
+ return w.status === 'active' && w.baseBranch === '$CURRENT_BRANCH';
283
+ });
284
+ console.log(active.length + ':' + active.map(function(w) { return w.name; }).join(','));
285
+ } catch(e) { console.log('0:'); }
286
+ " 2>/dev/null)
287
+
288
+ WT_COUNT=$(echo "$ACTIVE_WORKTREES" | cut -d: -f1)
289
+ WT_NAMES=$(echo "$ACTIVE_WORKTREES" | cut -d: -f2)
290
+
291
+ if [ "$WT_COUNT" -gt 0 ] 2>/dev/null; then
292
+ echo "BLOCKED: Committing to '$CURRENT_BRANCH' while $WT_COUNT active worktree(s) exist: $WT_NAMES"
293
+ echo " You should be working in your worktree, not on the base branch."
294
+ echo " Committing here risks interleaved history with agents in worktrees."
295
+ echo ""
296
+ echo " To work in your worktree: cd .caws/worktrees/<name>/"
297
+ echo " To see worktrees: caws worktree list"
298
+ echo " To override (unsafe): git commit --no-verify"
299
+ exit 1
300
+ fi
301
+ fi
302
+
303
+ # Guard 2: Warn if multiple active sessions exist on same branch
304
+ if [ -f ".caws/sessions.json" ] && command -v node >/dev/null 2>&1; then
305
+ ACTIVE_ON_BRANCH=$(node -e "
306
+ try {
307
+ var reg = JSON.parse(require('fs').readFileSync('.caws/sessions.json', 'utf8'));
308
+ var count = Object.values(reg.sessions || {}).filter(
309
+ function(s) { return s.status === 'active' && s.branch === '$CURRENT_BRANCH'; }
310
+ ).length;
311
+ console.log(count);
312
+ } catch(e) { console.log('0'); }
313
+ " 2>/dev/null)
314
+
315
+ if [ "$ACTIVE_ON_BRANCH" -gt 1 ] 2>/dev/null; then
316
+ echo "WARNING: $ACTIVE_ON_BRANCH active sessions detected on branch '$CURRENT_BRANCH'."
317
+ echo " Multiple agents committing to the same branch risks interleaved history."
318
+ echo " Consider using worktrees: caws parallel setup <plan-file>"
319
+ echo ""
320
+ fi
321
+ fi
322
+
323
+ # Guard 3: Block --amend when HEAD commit may not belong to current session
324
+ # Detect --amend by inspecting the parent git process arguments
325
+ AMEND_FLAG=false
326
+ if command -v ps >/dev/null 2>&1; then
327
+ PARENT_ARGS=$(ps -o args= -p $PPID 2>/dev/null || echo "")
328
+ case "$PARENT_ARGS" in
329
+ *--amend*) AMEND_FLAG=true ;;
330
+ esac
331
+ fi
332
+
333
+ if [ "$AMEND_FLAG" = true ]; then
334
+ BLOCK_AMEND=false
335
+ if [ -f ".caws/parallel.json" ]; then
336
+ BLOCK_AMEND=true
337
+ elif [ -f ".caws/worktrees.json" ] && command -v node >/dev/null 2>&1; then
338
+ HAS_ACTIVE_WT=$(node -e "
339
+ try {
340
+ var reg = JSON.parse(require('fs').readFileSync('.caws/worktrees.json', 'utf8'));
341
+ var active = Object.values(reg.worktrees || {}).filter(function(w) { return w.status === 'active'; });
342
+ console.log(active.length > 0 ? 'yes' : 'no');
343
+ } catch(e) { console.log('no'); }
344
+ " 2>/dev/null)
345
+ if [ "$HAS_ACTIVE_WT" = "yes" ]; then
346
+ BLOCK_AMEND=true
347
+ fi
348
+ fi
349
+
350
+ if [ "$BLOCK_AMEND" = true ]; then
351
+ echo "BLOCKED: --amend is not allowed while worktrees are active."
352
+ echo " Amending commits risks rewriting another agent's work."
353
+ echo " Create a new commit instead."
354
+ echo " To override (dangerous): git commit --amend --no-verify"
355
+ exit 1
356
+ fi
357
+ fi
358
+ fi
359
+ # ===== End Multi-Agent Safety Guard =====
360
+
239
361
  # Fallback chain for quality gates:
240
362
  # 1. Try Node.js script (if exists)
241
363
  # 2. Try CAWS CLI
@@ -477,73 +599,63 @@ if [ ! -d ".caws" ]; then
477
599
  exit 0
478
600
  fi
479
601
 
480
- # Run full validation suite
602
+ # Run CAWS validation (supports multi-spec projects)
603
+ CAWS_VALIDATION_FAILED=false
481
604
  if command -v caws >/dev/null 2>&1; then
482
- echo "Running comprehensive CAWS validation..."
483
-
484
- # Run validation and capture output
485
- VALIDATION_OUTPUT=$(caws validate 2>&1)
486
- VALIDATION_EXIT=$?
487
-
488
- if [ $VALIDATION_EXIT -eq 0 ]; then
489
- echo "CAWS validation passed"
490
- else
491
- echo "CAWS validation failed"
492
- echo ""
493
- echo "==================================================="
494
- echo "Validation Errors:"
495
- echo "==================================================="
496
- echo "$VALIDATION_OUTPUT" | grep -E "(|error|Error|Missing|required)" || echo "$VALIDATION_OUTPUT"
497
- echo ""
498
-
499
- # Check for contract-related errors
500
- if echo "$VALIDATION_OUTPUT" | grep -qi "contract"; then
501
- echo "Contract Requirements:"
502
- echo " - Tier 1 & 2 changes require at least one contract"
503
- echo " - For infrastructure/setup work, use 'chore' mode or add a minimal contract:"
504
- echo ""
505
- echo " Example minimal contract (.caws/working-spec.yaml):"
506
- echo " contracts:"
507
- echo " - type: 'project_setup'"
508
- echo " path: '.caws/working-spec.yaml'"
509
- echo " description: 'Project-level CAWS configuration'"
510
- echo ""
511
- echo " Or change mode to 'chore' for maintenance work:"
512
- echo " mode: chore"
513
- echo ""
605
+ echo "Running CAWS validation..."
606
+
607
+ # Multi-spec project: validate each open spec individually
608
+ if [ -d ".caws/specs" ] && command -v node >/dev/null 2>&1; then
609
+ OPEN_SPECS=$(node -e "
610
+ var fs = require('fs'), path = require('path'), dir = '.caws/specs';
611
+ try {
612
+ fs.readdirSync(dir).filter(function(f) { return f.endsWith('.yaml'); }).forEach(function(f) {
613
+ var content = fs.readFileSync(path.join(dir, f), 'utf8');
614
+ if (content.indexOf('status: closed') === -1) {
615
+ var match = content.match(/^id:\\s*(.+)$/m);
616
+ if (match) console.log(match[1].trim());
617
+ }
618
+ });
619
+ } catch(e) {}
620
+ " 2>/dev/null || echo "")
621
+
622
+ if [ -n "$OPEN_SPECS" ]; then
623
+ echo " Multi-spec project detected, validating open specs..."
624
+ while IFS= read -r spec_id; do
625
+ [ -z "$spec_id" ] && continue
626
+ echo " Validating spec: $spec_id"
627
+ if ! caws validate --spec-id "$spec_id" --quiet 2>&1; then
628
+ echo " Validation failed for spec: $spec_id"
629
+ CAWS_VALIDATION_FAILED=true
630
+ fi
631
+ done <<< "$OPEN_SPECS"
632
+ if [ "$CAWS_VALIDATION_FAILED" = false ]; then
633
+ echo "CAWS validation passed (all open specs)"
634
+ fi
635
+ else
636
+ echo " No open specs found, skipping CAWS validation"
514
637
  fi
515
-
516
- # Check for active waivers
517
- echo "Checking for active waivers..."
518
- if command -v caws >/dev/null 2>&1 && caws waivers list --status=active --format=count 2>/dev/null | grep -q "[1-9]"; then
519
- ACTIVE_WAIVERS=$(caws waivers list --status=active 2>/dev/null)
520
- echo "Active waivers found:"
521
- echo "$ACTIVE_WAIVERS" | head -5
522
- echo ""
523
- echo "Note: Waivers may not cover all validation failures"
524
- echo " Review waiver coverage: caws waivers list --status=active"
638
+ else
639
+ # Single-spec project: validate working-spec directly
640
+ VALIDATION_OUTPUT=$(caws validate --quiet 2>&1)
641
+ if [ $? -ne 0 ]; then
642
+ echo "$VALIDATION_OUTPUT"
643
+ CAWS_VALIDATION_FAILED=true
525
644
  else
526
- echo " No active waivers found"
527
- echo ""
528
- echo "If this is infrastructure/setup work, you can create a waiver:"
529
- echo " caws waivers create \\\\"
530
- echo " --title='Initial CAWS setup' \\\\"
531
- echo " --reason=infrastructure_limitation \\\\"
532
- echo " --gates=contracts \\\\"
533
- echo " --expires-at='2024-12-31T23:59:59Z' \\\\"
534
- echo " --approved-by='@your-team' \\\\"
535
- echo " --impact-level=low \\\\"
536
- echo " --mitigation-plan='Contracts will be added as features are developed'"
645
+ echo "CAWS validation passed"
537
646
  fi
538
-
647
+ fi
648
+
649
+ if [ "$CAWS_VALIDATION_FAILED" = true ]; then
539
650
  echo ""
540
651
  echo "==================================================="
652
+ echo "CAWS validation failed"
653
+ echo "==================================================="
541
654
  echo "Next Steps:"
542
655
  echo " 1. Review errors above"
543
- echo " 2. Fix issues in .caws/working-spec.yaml"
544
- echo " 3. Run: caws validate \\(to verify fixes\\)"
545
- echo " 4. Commit fixes: git commit --no-verify \\(allowed\\)"
546
- echo " 5. Push again: git push"
656
+ echo " 2. Fix issues in .caws/working-spec.yaml or .caws/specs/"
657
+ echo " 3. Run: caws validate (to verify fixes)"
658
+ echo " 4. Push again: git push"
547
659
  echo "==================================================="
548
660
  exit 1
549
661
  fi
@@ -665,37 +777,89 @@ echo "All quality gates passed - ready to push"
665
777
  function generateCommitMsgHook() {
666
778
  return `#!/bin/bash
667
779
  # CAWS Commit Message Hook
668
- # Validates commit message format
780
+ # Validates commit message format and enforces merge(worktree): convention
669
781
 
670
782
  COMMIT_MSG_FILE=$1
671
783
 
672
784
  # Read the commit message
673
785
  COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
674
786
 
787
+ # Resolve CAWS root (works from worktrees too)
788
+ CAWS_ROOT="."
789
+ if command -v git >/dev/null 2>&1; then
790
+ _GIT_COMMON=$(git rev-parse --git-common-dir 2>/dev/null || echo ".git")
791
+ if [ "$_GIT_COMMON" != ".git" ]; then
792
+ _CANDIDATE=$(cd "$_GIT_COMMON/.." 2>/dev/null && pwd || echo "")
793
+ if [ -n "$_CANDIDATE" ] && [ -d "$_CANDIDATE/.caws" ]; then
794
+ CAWS_ROOT="$_CANDIDATE"
795
+ fi
796
+ fi
797
+ fi
798
+
675
799
  # Check if CAWS is initialized
676
- if [ ! -d ".caws" ]; then
800
+ if [ ! -d "$CAWS_ROOT/.caws" ]; then
677
801
  exit 0
678
802
  fi
679
803
 
804
+ # ===== Worktree merge message guard =====
805
+ CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
806
+ GIT_DIR=$(git rev-parse --git-dir 2>/dev/null || echo ".git")
807
+ HAS_ACTIVE_WORKTREES=false
808
+
809
+ if [ -f "$CAWS_ROOT/.caws/worktrees.json" ] && command -v node >/dev/null 2>&1; then
810
+ WT_COUNT=$(node -e "
811
+ try {
812
+ var reg = JSON.parse(require('fs').readFileSync('$CAWS_ROOT/.caws/worktrees.json', 'utf8'));
813
+ var active = Object.values(reg.worktrees || {}).filter(function(w) {
814
+ return w.status === 'active' && w.baseBranch === '$CURRENT_BRANCH';
815
+ });
816
+ console.log(active.length);
817
+ } catch(e) { console.log('0'); }
818
+ " 2>/dev/null)
819
+ if [ "$WT_COUNT" -gt 0 ] 2>/dev/null; then
820
+ HAS_ACTIVE_WORKTREES=true
821
+ fi
822
+ fi
823
+
824
+ if [ "$HAS_ACTIVE_WORKTREES" = true ]; then
825
+ IS_GIT_MERGE=false
826
+ if [ -f "$GIT_DIR/MERGE_HEAD" ]; then
827
+ IS_GIT_MERGE=true
828
+ fi
829
+
830
+ if [[ "$COMMIT_MSG" =~ ^merge\\(worktree\\): ]] || [ "$IS_GIT_MERGE" = true ]; then
831
+ echo "Merge commit to base branch allowed (worktrees active)"
832
+ elif [[ "$COMMIT_MSG" =~ ^wip\\(checkpoint\\): ]]; then
833
+ echo "Checkpoint commit allowed (prior-session cleanup)"
834
+ else
835
+ echo "BLOCKED: Direct commit to '$CURRENT_BRANCH' while worktrees are active."
836
+ echo " Only these commit types are allowed on the base branch during parallel work:"
837
+ echo ""
838
+ echo " merge(worktree): <description> — merge a completed worktree branch"
839
+ echo " wip(checkpoint): <description> — commit prior-session dirty files"
840
+ echo " git merge --no-ff <branch> — git merge commit"
841
+ echo ""
842
+ echo " To override (unsafe): git commit --no-verify"
843
+ exit 1
844
+ fi
845
+ fi
846
+ # ===== End worktree merge message guard =====
847
+
680
848
  # Basic commit message validation
681
849
  if [ \${#COMMIT_MSG} -lt 10 ]; then
682
- echo "Commit message too short \\(minimum 10 characters\\)"
683
- echo "Write descriptive commit messages"
850
+ echo "Commit message too short (minimum 10 characters)"
851
+ echo " Write descriptive commit messages"
684
852
  exit 1
685
853
  fi
686
854
 
687
855
  # Check for conventional commit format (optional but encouraged)
688
- if [[ $COMMIT_MSG =~ ^(feat|fix|docs|style|refactor|test|chore)(.+)? ]]; then
689
- echo "Conventional commit format detected"
856
+ if [[ $COMMIT_MSG =~ ^(feat|fix|docs|style|refactor|test|chore|merge|perf|wip)(\\(.*\\))?: ]]; then
857
+ : # valid format
690
858
  else
691
- echo "Consider using conventional commit format:"
692
- echo " feat: add new feature"
693
- echo " fix: bug fix"
694
- echo " docs: documentation"
695
- echo " style: formatting"
696
- echo " refactor: code restructuring"
697
- echo " test: testing"
698
- echo " chore: maintenance"
859
+ if [[ ! $COMMIT_MSG =~ ^Merge\\ (branch|remote) ]]; then
860
+ echo "Consider using conventional commit format:"
861
+ echo " feat: / fix: / docs: / refactor: / chore: / merge(worktree):"
862
+ fi
699
863
  fi
700
864
 
701
865
  echo "Commit message validation passed"
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scaffold/index.js"],"names":[],"mappings":"AA8MA;;;GAGG;AACH,6DA6jBC;AAztBD;;;GAyIC;;AAMD;;;GAGG;AACH,yDAGC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scaffold/index.js"],"names":[],"mappings":"AA4MA;;;GAGG;AACH,6DAikBC;AAxtBD;;;GAoIC;;AAMD;;;GAGG;AACH,yDAGC"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Start a new session, creating the initial capsule with baseline state
3
+ * @param {Object} options - Session options
4
+ * @param {string} [options.role] - Agent role (worker, integrator, qa)
5
+ * @param {string} [options.specId] - Associated feature spec ID
6
+ * @param {string[]} [options.allowedGlobs] - Allowed file patterns
7
+ * @param {string[]} [options.forbiddenGlobs] - Forbidden file patterns
8
+ * @param {string} [options.intent] - What this session intends to accomplish
9
+ * @returns {Object} Created capsule
10
+ */
11
+ export function startSession(options?: {
12
+ role?: string;
13
+ specId?: string;
14
+ allowedGlobs?: string[];
15
+ forbiddenGlobs?: string[];
16
+ intent?: string;
17
+ }): any;
18
+ /**
19
+ * Add a checkpoint to the current (most recent active) session
20
+ * @param {Object} data - Checkpoint data
21
+ * @param {string} [data.sessionId] - Specific session ID (uses latest active if omitted)
22
+ * @param {string[]} [data.pathsTouched] - Files changed
23
+ * @param {string[]} [data.artifactsWritten] - Generated artifacts
24
+ * @param {Object[]} [data.testsRun] - Test results { name, status, evidence }
25
+ * @param {Object[]} [data.determinismChecks] - Determinism checks { name, status, total }
26
+ * @param {Object[]} [data.knownIssues] - Issues discovered { type, description }
27
+ * @param {string} [data.intent] - Updated intent description
28
+ * @returns {Object} Updated capsule
29
+ */
30
+ export function checkpointSession(data?: {
31
+ sessionId?: string;
32
+ pathsTouched?: string[];
33
+ artifactsWritten?: string[];
34
+ testsRun?: any[];
35
+ determinismChecks?: any[];
36
+ knownIssues?: any[];
37
+ intent?: string;
38
+ }): any;
39
+ /**
40
+ * End a session, finalizing the capsule with handoff information
41
+ * @param {Object} data - End session data
42
+ * @param {string} [data.sessionId] - Specific session ID (uses latest active if omitted)
43
+ * @param {string[]} [data.nextActions] - What the next session should do
44
+ * @param {string[]} [data.riskNotes] - Risk notes for handoff
45
+ * @returns {Object} Finalized capsule
46
+ */
47
+ export function endSession(data?: {
48
+ sessionId?: string;
49
+ nextActions?: string[];
50
+ riskNotes?: string[];
51
+ }): any;
52
+ /**
53
+ * List all sessions
54
+ * @param {Object} [options] - List options
55
+ * @param {string} [options.status] - Filter by status (active, completed)
56
+ * @param {number} [options.limit] - Max entries to return
57
+ * @returns {Object[]} Session entries
58
+ */
59
+ export function listSessions(options?: {
60
+ status?: string;
61
+ limit?: number;
62
+ }): any[];
63
+ /**
64
+ * Show a specific session's full capsule
65
+ * @param {string} sessionId - Session ID (or "latest" for most recent)
66
+ * @returns {Object} Full capsule
67
+ */
68
+ export function showSession(sessionId: string): any;
69
+ /**
70
+ * Briefing output for session start hooks - returns structured text
71
+ * @returns {string} Briefing text
72
+ */
73
+ export function getBriefing(): string;
74
+ /**
75
+ * Load the session registry
76
+ * @param {string} root - Repository root
77
+ * @returns {Object} Registry object
78
+ */
79
+ export function loadRegistry(root: string): any;
80
+ /**
81
+ * Get the git repository root
82
+ * @returns {string} Absolute path to repo root
83
+ */
84
+ export function getRepoRoot(): string;
85
+ export const SESSIONS_DIR: ".caws/sessions";
86
+ export const REGISTRY_FILE: ".caws/sessions.json";
87
+ export const CAPSULE_SCHEMA_VERSION: "caws.capsule.v1";
88
+ /**
89
+ * Find all active sessions on a specific branch
90
+ * @param {string} branch - Branch name to search
91
+ * @returns {Object[]} Active sessions on that branch with id and metadata
92
+ */
93
+ export function findActiveSessionsOnBranch(branch: string): any[];
94
+ //# sourceMappingURL=session-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../../src/session/session-manager.js"],"names":[],"mappings":"AA4JA;;;;;;;;;GASG;AACH,uCAPG;IAAyB,IAAI,GAArB,MAAM;IACW,MAAM,GAAvB,MAAM;IACa,YAAY,GAA/B,MAAM,EAAE;IACW,cAAc,GAAjC,MAAM,EAAE;IACS,MAAM,GAAvB,MAAM;CACd,OAyFF;AAED;;;;;;;;;;;GAWG;AACH,yCATG;IAAsB,SAAS,GAAvB,MAAM;IACU,YAAY,GAA5B,MAAM,EAAE;IACQ,gBAAgB,GAAhC,MAAM,EAAE;IACQ,QAAQ,GAAxB,KAAQ;IACQ,iBAAiB,GAAjC,KAAQ;IACQ,WAAW,GAA3B,KAAQ;IACM,MAAM,GAApB,MAAM;CACd,OAwDF;AAED;;;;;;;GAOG;AACH,kCALG;IAAsB,SAAS,GAAvB,MAAM;IACU,WAAW,GAA3B,MAAM,EAAE;IACQ,SAAS,GAAzB,MAAM,EAAE;CAChB,OAgEF;AAED;;;;;;GAMG;AACH,uCAJG;IAAyB,MAAM,GAAvB,MAAM;IACW,KAAK,GAAtB,MAAM;CACd,GAAU,KAAQ,CAuBpB;AAED;;;;GAIG;AACH,uCAHW,MAAM,OA2BhB;AAED;;;GAGG;AACH,+BAFa,MAAM,CA8DlB;AAlZD;;;;GAIG;AACH,mCAHW,MAAM,OAahB;AApHD;;;GAGG;AACH,+BAFa,MAAM,CAMlB;AAZD,2BAAqB,gBAAgB,CAAC;AACtC,4BAAsB,qBAAqB,CAAC;AAC5C,qCAA+B,iBAAiB,CAAC;AAwgBjD;;;;GAIG;AACH,mDAHW,MAAM,GACJ,KAAQ,CAQpB"}
@@ -533,6 +533,19 @@ function findActiveSession(registry) {
533
533
  return active.length > 0 ? active[0][0] : null;
534
534
  }
535
535
 
536
+ /**
537
+ * Find all active sessions on a specific branch
538
+ * @param {string} branch - Branch name to search
539
+ * @returns {Object[]} Active sessions on that branch with id and metadata
540
+ */
541
+ function findActiveSessionsOnBranch(branch) {
542
+ const root = getRepoRoot();
543
+ const registry = loadRegistry(root);
544
+ return Object.entries(registry.sessions)
545
+ .filter(([, meta]) => meta.status === 'active' && meta.branch === branch)
546
+ .map(([id, meta]) => ({ id, ...meta }));
547
+ }
548
+
536
549
  module.exports = {
537
550
  startSession,
538
551
  checkpointSession,
@@ -545,4 +558,5 @@ module.exports = {
545
558
  SESSIONS_DIR,
546
559
  REGISTRY_FILE,
547
560
  CAPSULE_SCHEMA_VERSION,
561
+ findActiveSessionsOnBranch,
548
562
  };
@@ -1,6 +1,7 @@
1
1
  #!/bin/bash
2
2
  # CAWS Scope Guard Hook for Claude Code
3
- # Validates file edits against the working spec's scope boundaries
3
+ # Validates file edits against scope boundaries from working-spec + feature specs
4
+ # Specs with terminal status (completed, closed, archived) are skipped
4
5
  # @author @darianrosebrook
5
6
 
6
7
  set -euo pipefail
@@ -25,8 +26,8 @@ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
25
26
  SPEC_FILE="$PROJECT_DIR/.caws/working-spec.yaml"
26
27
  SCOPE_FILE="$PROJECT_DIR/.caws/scope.json"
27
28
 
28
- # Check if spec file or scope.json exists
29
- if [[ ! -f "$SPEC_FILE" ]] && [[ ! -f "$SCOPE_FILE" ]]; then
29
+ # Check if any spec infrastructure exists
30
+ if [[ ! -f "$SPEC_FILE" ]] && [[ ! -f "$SCOPE_FILE" ]] && [[ ! -d "$PROJECT_DIR/.caws/specs" ]]; then
30
31
  exit 0
31
32
  fi
32
33
 
@@ -119,7 +120,9 @@ if [[ ! -f "$SPEC_FILE" ]] && [[ -f "$SCOPE_FILE" ]]; then
119
120
  fi
120
121
  fi
121
122
 
122
- # Use Node.js to parse YAML and check scope
123
+ # Use Node.js to parse YAML and check scope across working spec + active feature specs
124
+ SPECS_DIR="$PROJECT_DIR/.caws/specs"
125
+
123
126
  if command -v node >/dev/null 2>&1; then
124
127
  SCOPE_CHECK=$(node -e "
125
128
  const yaml = require('js-yaml');
@@ -127,26 +130,67 @@ if command -v node >/dev/null 2>&1; then
127
130
  const path = require('path');
128
131
 
129
132
  try {
130
- const spec = yaml.load(fs.readFileSync('$SPEC_FILE', 'utf8'));
131
133
  const filePath = '$REL_PATH';
132
134
 
133
- // Check if file is explicitly out of scope
134
- const outOfScope = spec.scope?.out || [];
135
- for (const pattern of outOfScope) {
136
- // Simple glob-like matching
137
- const regex = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.'));
138
- if (regex.test(filePath)) {
139
- console.log('out_of_scope:' + pattern);
140
- process.exit(0);
135
+ // Terminal statuses: specs that are done scope no longer enforced
136
+ const TERMINAL = new Set(['completed', 'closed', 'archived']);
137
+
138
+ // Smart allowlist: root-level files, .caws/, .claude/ always pass
139
+ if (!filePath.includes('/') || filePath.startsWith('.caws/') || filePath.startsWith('.claude/')) {
140
+ console.log('in_scope');
141
+ process.exit(0);
142
+ }
143
+
144
+ // Collect all active specs (working-spec + feature specs)
145
+ const specs = [];
146
+
147
+ // Load working-spec.yaml if present
148
+ const mainSpec = '$SPEC_FILE';
149
+ if (fs.existsSync(mainSpec)) {
150
+ try {
151
+ const s = yaml.load(fs.readFileSync(mainSpec, 'utf8'));
152
+ if (s && !TERMINAL.has(s.status)) {
153
+ specs.push({ source: 'working-spec', spec: s });
154
+ }
155
+ } catch (_) {}
156
+ }
157
+
158
+ // Load feature specs from .caws/specs/
159
+ const specsDir = '$SPECS_DIR';
160
+ if (fs.existsSync(specsDir)) {
161
+ for (const f of fs.readdirSync(specsDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))) {
162
+ try {
163
+ const s = yaml.load(fs.readFileSync(path.join(specsDir, f), 'utf8'));
164
+ if (s && !TERMINAL.has(s.status)) {
165
+ specs.push({ source: f, spec: s });
166
+ }
167
+ } catch (_) {}
141
168
  }
142
169
  }
143
170
 
144
- // Check if file is in scope (if scope is explicitly defined)
145
- const inScope = spec.scope?.in || [];
146
- if (inScope.length > 0) {
171
+ // No active specs allow everything
172
+ if (specs.length === 0) {
173
+ console.log('in_scope');
174
+ process.exit(0);
175
+ }
176
+
177
+ // Check scope.out across ALL active specs — any match blocks
178
+ for (const { source, spec } of specs) {
179
+ for (const pattern of (spec.scope?.out || [])) {
180
+ const regex = new RegExp(pattern.replace(/\\*/g, '.*').replace(/\\?/g, '.'));
181
+ if (regex.test(filePath)) {
182
+ console.log('out_of_scope:' + source + ':' + pattern);
183
+ process.exit(0);
184
+ }
185
+ }
186
+ }
187
+
188
+ // Union all scope.in patterns — file must match at least one
189
+ const allInScope = specs.flatMap(({ spec }) => spec.scope?.in || []);
190
+ if (allInScope.length > 0) {
147
191
  let found = false;
148
- for (const pattern of inScope) {
149
- const regex = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.'));
192
+ for (const pattern of allInScope) {
193
+ const regex = new RegExp(pattern.replace(/\\*/g, '.*').replace(/\\?/g, '.'));
150
194
  if (regex.test(filePath)) {
151
195
  found = true;
152
196
  break;
@@ -165,12 +209,14 @@ if command -v node >/dev/null 2>&1; then
165
209
  " 2>&1)
166
210
 
167
211
  if [[ "$SCOPE_CHECK" == out_of_scope:* ]]; then
168
- PATTERN="${SCOPE_CHECK#out_of_scope:}"
212
+ DETAIL="${SCOPE_CHECK#out_of_scope:}"
213
+ SOURCE="${DETAIL%%:*}"
214
+ PATTERN="${DETAIL#*:}"
169
215
  echo '{
170
216
  "hookSpecificOutput": {
171
217
  "hookEventName": "PreToolUse",
172
218
  "permissionDecision": "ask",
173
- "permissionDecisionReason": "This file ('"$REL_PATH"') is marked as out-of-scope in the working spec (pattern: '"$PATTERN"'). Editing it may cause scope creep. Please confirm this edit is intentional."
219
+ "permissionDecisionReason": "This file ('"$REL_PATH"') is marked as out-of-scope in '"$SOURCE"' (pattern: '"$PATTERN"'). Editing it may cause scope creep. Please confirm this edit is intentional."
174
220
  }
175
221
  }'
176
222
  exit 0
@@ -181,7 +227,7 @@ if command -v node >/dev/null 2>&1; then
181
227
  "hookSpecificOutput": {
182
228
  "hookEventName": "PreToolUse",
183
229
  "permissionDecision": "ask",
184
- "permissionDecisionReason": "This file ('"$REL_PATH"') is not in the defined scope of the working spec. Editing it may cause scope creep. Please confirm this edit is intentional."
230
+ "permissionDecisionReason": "This file ('"$REL_PATH"') is not in the defined scope of any active spec. Editing it may cause scope creep. Please confirm this edit is intentional."
185
231
  }
186
232
  }'
187
233
  exit 0