@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.
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/parallel.d.ts +7 -0
- package/dist/commands/parallel.d.ts.map +1 -0
- package/dist/commands/parallel.js +238 -0
- package/dist/commands/session.d.ts +7 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/specs.d.ts +6 -0
- package/dist/commands/specs.d.ts.map +1 -1
- package/dist/commands/specs.js +55 -2
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +13 -3
- package/dist/commands/tutorial.js +0 -2
- package/dist/commands/waivers.d.ts.map +1 -1
- package/dist/constants/spec-types.d.ts +52 -0
- package/dist/constants/spec-types.d.ts.map +1 -1
- package/dist/constants/spec-types.js +25 -2
- package/dist/index.js +43 -2
- package/dist/parallel/parallel-manager.d.ts +67 -0
- package/dist/parallel/parallel-manager.d.ts.map +1 -0
- package/dist/parallel/parallel-manager.js +440 -0
- package/dist/scaffold/claude-hooks.d.ts.map +1 -1
- package/dist/scaffold/claude-hooks.js +78 -2
- package/dist/scaffold/git-hooks.d.ts.map +1 -1
- package/dist/scaffold/git-hooks.js +237 -73
- package/dist/scaffold/index.d.ts.map +1 -1
- package/dist/session/session-manager.d.ts +94 -0
- package/dist/session/session-manager.d.ts.map +1 -0
- package/dist/session/session-manager.js +14 -0
- package/dist/templates/.claude/hooks/scope-guard.sh +67 -21
- package/dist/templates/.claude/hooks/session-caws-status.sh +117 -0
- package/dist/templates/.claude/hooks/session-log.sh +528 -0
- package/dist/templates/.claude/hooks/stop-worktree-check.sh +46 -0
- package/dist/templates/.claude/hooks/worktree-guard.sh +207 -0
- package/dist/templates/.claude/hooks/worktree-write-guard.sh +84 -0
- package/dist/templates/.claude/rules/git-safety.md +26 -0
- package/dist/templates/.claude/rules/worktree-isolation.md +51 -0
- package/dist/templates/.claude/settings.json +15 -0
- package/dist/utils/gitignore-updater.d.ts +1 -1
- package/dist/utils/gitignore-updater.d.ts.map +1 -1
- package/dist/utils/gitignore-updater.js +3 -0
- package/dist/utils/ide-detection.d.ts +89 -0
- package/dist/utils/ide-detection.d.ts.map +1 -0
- package/dist/validation/spec-validation.d.ts.map +1 -1
- package/dist/validation/spec-validation.js +16 -0
- package/dist/worktree/worktree-manager.d.ts.map +1 -1
- package/dist/worktree/worktree-manager.js +31 -21
- package/package.json +2 -2
- package/templates/.claude/hooks/scope-guard.sh +67 -21
- package/templates/.claude/hooks/session-caws-status.sh +117 -0
- package/templates/.claude/hooks/session-log.sh +528 -0
- package/templates/.claude/hooks/stop-worktree-check.sh +46 -0
- package/templates/.claude/hooks/worktree-guard.sh +207 -0
- package/templates/.claude/hooks/worktree-write-guard.sh +84 -0
- package/templates/.claude/rules/git-safety.md +26 -0
- package/templates/.claude/rules/worktree-isolation.md +51 -0
- 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
|
|
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
|
|
483
|
-
|
|
484
|
-
#
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
echo ""
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
-
#
|
|
517
|
-
|
|
518
|
-
if
|
|
519
|
-
|
|
520
|
-
|
|
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 "
|
|
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
|
|
545
|
-
echo " 4.
|
|
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 "
|
|
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
|
|
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)(
|
|
689
|
-
|
|
856
|
+
if [[ $COMMIT_MSG =~ ^(feat|fix|docs|style|refactor|test|chore|merge|perf|wip)(\\(.*\\))?: ]]; then
|
|
857
|
+
: # valid format
|
|
690
858
|
else
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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":"
|
|
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
|
|
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
|
|
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
|
-
//
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
149
|
-
const regex = new RegExp(pattern.replace(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|