create-claude-cabinet 0.44.0 → 0.45.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 (63) hide show
  1. package/README.md +5 -0
  2. package/lib/cli.js +51 -6
  3. package/lib/copy.js +56 -10
  4. package/lib/mux-setup.js +1 -0
  5. package/package.json +1 -1
  6. package/templates/cabinet/checklist-stats-schema.md +104 -0
  7. package/templates/cabinet/checkpoint-protocol.md +17 -5
  8. package/templates/cabinet/qa-dimensions-template.yaml +7 -0
  9. package/templates/cabinet/watchtower-contracts.md +38 -0
  10. package/templates/engagement/pib-db-patches/pib-db-lib.mjs +4 -1
  11. package/templates/hooks/action-completion-gate.sh +17 -0
  12. package/templates/hooks/watchtower-session-start.sh +80 -5
  13. package/templates/mux/__tests__/claude-carveout.fixture.sh +136 -0
  14. package/templates/mux/__tests__/claude-carveout.test.mjs +38 -0
  15. package/templates/mux/__tests__/mux-fail-loud.fixture.sh +254 -0
  16. package/templates/mux/__tests__/mux-fail-loud.test.mjs +41 -0
  17. package/templates/mux/__tests__/worktree-dirty-check.fixture.sh +184 -0
  18. package/templates/mux/__tests__/worktree-dirty-check.test.mjs +35 -0
  19. package/templates/mux/bin/mux +212 -60
  20. package/templates/mux/config/worktree-cleanup.sh +55 -9
  21. package/templates/mux/config/worktree-dirty-check.sh +128 -0
  22. package/templates/mux/config/worktree-session-health.sh +62 -35
  23. package/templates/scripts/__tests__/qa-handoff-aging.e2e.test.mjs +108 -0
  24. package/templates/scripts/__tests__/qa-handoff-gate.test.mjs +335 -0
  25. package/templates/scripts/__tests__/resolve-project.test.mjs +144 -0
  26. package/templates/scripts/__tests__/ring-state-ownership.test.mjs +228 -0
  27. package/templates/scripts/pib-db-lib.mjs +4 -1
  28. package/templates/scripts/pib-db.mjs +4 -1
  29. package/templates/scripts/validate-memory.mjs +6 -2
  30. package/templates/scripts/watchtower-build-context.mjs +12 -8
  31. package/templates/scripts/watchtower-lib.mjs +265 -2
  32. package/templates/scripts/watchtower-migrate-keys.mjs +305 -0
  33. package/templates/scripts/watchtower-queue.mjs +226 -1
  34. package/templates/scripts/watchtower-ring1.mjs +19 -3
  35. package/templates/scripts/watchtower-ring2.mjs +4 -2
  36. package/templates/scripts/watchtower-ring3-close.mjs +92 -88
  37. package/templates/skills/audit/SKILL.md +25 -6
  38. package/templates/skills/audit/phases/checklist-pruning.md +108 -0
  39. package/templates/skills/briefing/SKILL.md +12 -1
  40. package/templates/skills/cabinet/SKILL.md +2 -2
  41. package/templates/skills/collab-consultant/SKILL.md +1 -1
  42. package/templates/skills/debrief/SKILL.md +33 -3
  43. package/templates/skills/debrief/phases/checklist-feedback.md +10 -3
  44. package/templates/skills/debrief/phases/qa-handoff-sweep.md +78 -0
  45. package/templates/skills/engagement-create/SKILL.md +1 -1
  46. package/templates/skills/engagement-help/SKILL.md +1 -1
  47. package/templates/skills/execute/SKILL.md +1 -1
  48. package/templates/skills/execute/phases/post-impl-checklist.md +18 -0
  49. package/templates/skills/execute-group/SKILL.md +76 -24
  50. package/templates/skills/inbox/SKILL.md +30 -7
  51. package/templates/skills/orient/SKILL.md +100 -6
  52. package/templates/skills/orient/phases/checklist-status.md +12 -0
  53. package/templates/skills/plan/SKILL.md +14 -6
  54. package/templates/skills/qa-handoff/SKILL.md +132 -5
  55. package/templates/skills/session-handoff/SKILL.md +165 -0
  56. package/templates/skills/setup-accounts/SKILL.md +1 -1
  57. package/templates/skills/unwrap/SKILL.md +1 -1
  58. package/templates/skills/verify/SKILL.md +2 -2
  59. package/templates/skills/watchtower/SKILL.md +19 -1
  60. package/templates/watchtower/queue/items/item.json.schema +9 -0
  61. package/templates/workflows/deliberative-audit.js +3 -0
  62. package/templates/workflows/execute-group-complete.js +93 -16
  63. package/templates/workflows/execute-group-implement.js +164 -19
@@ -0,0 +1,136 @@
1
+ #!/bin/bash
2
+ # Sandboxed AC verification for act:14305d16 — the .claude/ authored/infra
3
+ # carve-out generalized from a static plans/methodology pair to "any tracked
4
+ # .claude/ file is authored; only gitignored infra is disposable"
5
+ # (.claude/rules/artifacts-of-thought.md).
6
+ #
7
+ # Drives templates/mux/config/worktree-session-health.sh directly against a
8
+ # throwaway repo + worktree under mktemp with HOME overridden — never touches
9
+ # the real ~/.mux/worktrees, ~/.config/mux, or any live worktree.
10
+ set -uo pipefail
11
+
12
+ REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
13
+ SANDBOX=$(mktemp -d /tmp/cc-carveout-test.XXXXXX)
14
+ FAKE_HOME="$SANDBOX/home"
15
+ PROJ="$SANDBOX/proj"
16
+ WT="$SANDBOX/wt"
17
+ HEALTH="$REPO_ROOT/templates/mux/config/worktree-session-health.sh"
18
+ DIRTY="$REPO_ROOT/templates/mux/config/worktree-dirty-check.sh"
19
+
20
+ pass=0; fail=0
21
+ ok() { echo " PASS: $1"; pass=$((pass+1)); }
22
+ bad() { echo " FAIL: $1"; fail=$((fail+1)); }
23
+ assert() { # assert <desc> <cmd...>
24
+ local desc="$1"; shift
25
+ if "$@" >/dev/null 2>&1; then ok "$desc"; else bad "$desc"; fi
26
+ }
27
+ run_health() { HOME="$FAKE_HOME" bash "$HEALTH" "$PROJ" "$WT" "$@" 2>&1; }
28
+
29
+ mkdir -p "$FAKE_HOME/.local/share/mux/wt-health" "$FAKE_HOME/.claude/projects"
30
+ trap 'rm -rf "$SANDBOX"' EXIT
31
+
32
+ # --- fixture main repo --------------------------------------------------------
33
+ # Tracked (= authored): plans/, methodology/, rules/, cabinet/c.md.
34
+ # Gitignored (= infra): skills/ entirely; cabinet/_briefing.md inside the
35
+ # otherwise-tracked cabinet/ (the mixed-dir case).
36
+ git init -q "$PROJ"
37
+ git -C "$PROJ" config user.email t@t.t; git -C "$PROJ" config user.name t
38
+ mkdir -p "$PROJ/.claude/plans" "$PROJ/.claude/rules" "$PROJ/.claude/cabinet" "$PROJ/.claude/skills"
39
+ printf '%s\n' '.claude/skills/' '.claude/cabinet/_briefing*.md' > "$PROJ/.gitignore"
40
+ echo plan > "$PROJ/.claude/plans/p.md"
41
+ echo rule > "$PROJ/.claude/rules/r.md"
42
+ echo cabinet-doc > "$PROJ/.claude/cabinet/c.md"
43
+ echo briefing-v1 > "$PROJ/.claude/cabinet/_briefing.md"
44
+ echo skill-v1 > "$PROJ/.claude/skills/s.md"
45
+ echo code > "$PROJ/file.txt"
46
+ git -C "$PROJ" add -A && git -C "$PROJ" commit -qm init
47
+ MAIN_SLUG=$(echo "$PROJ" | sed 's|[/.]|-|g')
48
+ mkdir -p "$FAKE_HOME/.claude/projects/$MAIN_SLUG"
49
+
50
+ git -C "$PROJ" worktree add -q "$WT" -b mux/carveout HEAD
51
+
52
+ frozen() { git -C "$WT" ls-files -v .claude/ | grep -c '^h '; }
53
+ excl() { cat "$(git -C "$WT" rev-parse --git-dir)/info/exclude" 2>/dev/null; }
54
+
55
+ echo "== T1: health run leaves every tracked .claude/ file unfrozen (AC1)"
56
+ run_health --refresh >"$SANDBOX/t1.out"
57
+ assert "zero tracked .claude/ files carry the assume-unchanged bit" \
58
+ test "$(frozen)" -eq 0
59
+ assert "tracked rules/ file shows H in ls-files -v" \
60
+ bash -c "git -C '$WT' ls-files -v .claude/rules/r.md | grep -q '^H '"
61
+ assert "tracked cabinet/ file shows H in ls-files -v" \
62
+ bash -c "git -C '$WT' ls-files -v .claude/cabinet/c.md | grep -q '^H '"
63
+ assert "gitignored skills/ infra copied from main" \
64
+ grep -qx skill-v1 "$WT/.claude/skills/s.md"
65
+ assert "gitignored briefing inside mixed cabinet/ copied from main" \
66
+ grep -qx briefing-v1 "$WT/.claude/cabinet/_briefing.md"
67
+
68
+ echo "== T2: exclude negations are derived from the index, not a static pair"
69
+ assert "exclude hides untracked infra (.claude/*)" \
70
+ bash -c "excl() { cat \"\$(git -C '$WT' rev-parse --git-dir)/info/exclude\"; }; excl | grep -qxF '.claude/*'"
71
+ for d in plans rules cabinet; do
72
+ assert "exclude negates tracked dir $d/" \
73
+ bash -c "cat \"\$(git -C '$WT' rev-parse --git-dir)/info/exclude\" | grep -qxF '!.claude/$d/'"
74
+ done
75
+ assert "no negation for pure-infra skills/" \
76
+ bash -c "! cat \"\$(git -C '$WT' rev-parse --git-dir)/info/exclude\" | grep -qxF '!.claude/skills/'"
77
+ run_health >"$SANDBOX/t2.out"
78
+ assert "second run does not duplicate managed exclude lines" \
79
+ bash -c "[ \"\$(cat \"\$(git -C '$WT' rev-parse --git-dir)/info/exclude\" | grep -cxF '.claude/*')\" -eq 1 ]"
80
+
81
+ echo "== T3: legacy frozen worktree heals (the 2026-06-11 trap)"
82
+ git -C "$WT" update-index --assume-unchanged .claude/rules/r.md
83
+ echo trapped-edit > "$WT/.claude/rules/r.md"
84
+ assert "precondition: frozen edit invisible to git status" \
85
+ bash -c "! git -C '$WT' status --porcelain | grep -q rules/r.md"
86
+ run_health >"$SANDBOX/t3.out"
87
+ assert "health run clears the bit" test "$(frozen)" -eq 0
88
+ assert "edit to tracked rules/ file now visible to git" \
89
+ bash -c "git -C '$WT' status --porcelain | grep -q 'M .claude/rules/r.md'"
90
+
91
+ echo "== T4: --refresh never overwrites tracked, locally-modified files (AC2)"
92
+ echo main-side-edit > "$PROJ/.claude/rules/r.md" # uncommitted main edit
93
+ echo skill-v2 > "$PROJ/.claude/skills/s.md" # infra changed in main
94
+ echo briefing-v2 > "$PROJ/.claude/cabinet/_briefing.md"
95
+ run_health --refresh >"$SANDBOX/t4.out"
96
+ assert "worktree's modified tracked rules/ file survives refresh" \
97
+ grep -qx trapped-edit "$WT/.claude/rules/r.md"
98
+ assert "tracked cabinet doc untouched by refresh" \
99
+ grep -qx cabinet-doc "$WT/.claude/cabinet/c.md"
100
+ assert "pure-infra skills/ refreshed to main's version" \
101
+ grep -qx skill-v2 "$WT/.claude/skills/s.md"
102
+ assert "mixed-dir gitignored briefing refreshed to main's version" \
103
+ grep -qx briefing-v2 "$WT/.claude/cabinet/_briefing.md"
104
+
105
+ echo "== T5: freshness check ignores tracked files (no refresh churn loop)"
106
+ sleep 1; touch "$PROJ/.claude/rules/r.md" "$PROJ/.claude/plans/p.md"
107
+ out=$(run_health)
108
+ assert "newer tracked main files do not count as stale infra" \
109
+ grep -q 'current with main repo' <<<"$out"
110
+
111
+ echo "== T6: new doc beside tracked files is visible + dirty"
112
+ echo new-rule > "$WT/.claude/rules/new-rule.md"
113
+ assert "new untracked rules/ doc appears in git status" \
114
+ bash -c "git -C '$WT' status --porcelain --untracked-files=all | grep -q 'new-rule.md'"
115
+ out=$(bash "$DIRTY" "$WT" "$PROJ")
116
+ assert "dirty-check counts it as authored record ($out)" \
117
+ grep -q 'verdict=dirty' <<<"$out"
118
+ rm "$WT/.claude/rules/new-rule.md"
119
+ git -C "$WT" checkout -q -- .claude/rules/r.md
120
+
121
+ echo "== T7: infra-only worktree is clean (churn never blocks cleanup)"
122
+ out=$(bash "$DIRTY" "$WT" "$PROJ")
123
+ assert "worktree with only copied infra => clean ($out)" \
124
+ grep -q 'verdict=clean' <<<"$out"
125
+
126
+ echo "== T8: pure-infra deletions in main reconcile on refresh"
127
+ rm "$PROJ/.claude/skills/s.md"; echo skill-new > "$PROJ/.claude/skills/s2.md"
128
+ run_health --refresh >"$SANDBOX/t8.out"
129
+ assert "deleted main infra file removed from worktree" \
130
+ test ! -e "$WT/.claude/skills/s.md"
131
+ assert "new main infra file arrives in worktree" \
132
+ grep -qx skill-new "$WT/.claude/skills/s2.md"
133
+
134
+ echo ""
135
+ echo "RESULT: $pass passed, $fail failed (sandbox: $SANDBOX)"
136
+ exit $fail
@@ -0,0 +1,38 @@
1
+ // Guard test for the .claude/ authored/infra carve-out (act:14305d16).
2
+ //
3
+ // Drives claude-carveout.fixture.sh, which builds a throwaway repo +
4
+ // worktree under mktemp with HOME overridden — it never touches the real
5
+ // ~/.mux/worktrees, ~/.config/mux, or any live worktree.
6
+ //
7
+ // The rule under test (.claude/rules/artifacts-of-thought.md): ANY tracked
8
+ // .claude/ file is authored project record; only gitignored infra is
9
+ // disposable. Covers:
10
+ // - no tracked .claude/ file ever carries the assume-unchanged bit
11
+ // - legacy frozen worktrees heal (the bit that hid an edit to
12
+ // .claude/rules/enforcement-pipeline.md on 2026-06-11)
13
+ // - --refresh never overwrites a tracked, locally-modified .claude/ file
14
+ // - exclude negations derive from the index, not a static plans/methodology pair
15
+ // - pure-infra entries still reconcile deletions; mixed dirs copy only
16
+ // their gitignored files
17
+
18
+ import { test } from 'node:test';
19
+ import assert from 'node:assert';
20
+ import { spawnSync } from 'node:child_process';
21
+ import { fileURLToPath } from 'node:url';
22
+ import path from 'node:path';
23
+
24
+ const fixture = path.join(
25
+ path.dirname(fileURLToPath(import.meta.url)),
26
+ 'claude-carveout.fixture.sh'
27
+ );
28
+
29
+ test('.claude/ carve-out: tracked = authored, gitignored = infra (sandboxed fixture suite)', () => {
30
+ const res = spawnSync('bash', [fixture], { encoding: 'utf8', timeout: 120_000 });
31
+ const output = `${res.stdout}\n${res.stderr}`;
32
+ assert.strictEqual(
33
+ res.status,
34
+ 0,
35
+ `fixture suite reported failures:\n${output}`
36
+ );
37
+ assert.match(output, /RESULT: \d+ passed, 0 failed/);
38
+ });
@@ -0,0 +1,254 @@
1
+ #!/bin/bash
2
+ # Sandboxed AC verification for act:fe6aab6f (mux fail-loud sweep).
3
+ #
4
+ # Everything runs under a mktemp dir with HOME overridden AND a private
5
+ # throwaway tmux server (tmux -L mux-test-$$ -f /dev/null) — it never touches
6
+ # the real ~/.mux/worktrees, ~/.config/mux, or the operator's live tmux
7
+ # server (the test runner itself may be running inside one). The test server
8
+ # is killed in cleanup even on assertion failure.
9
+ set -uo pipefail
10
+
11
+ REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
12
+ SANDBOX=$(mktemp -d /tmp/cc-mux-failloud-test.XXXXXX)
13
+ FAKE_HOME="$SANDBOX/home"
14
+ PROJ="$SANDBOX/proj"
15
+ WT_DIR="$FAKE_HOME/.mux/worktrees"
16
+ MUX="$REPO_ROOT/templates/mux/bin/mux"
17
+
18
+ pass=0; fail=0
19
+ ok() { echo " PASS: $1"; pass=$((pass+1)); }
20
+ bad() { echo " FAIL: $1"; fail=$((fail+1)); }
21
+ assert() { # assert <desc> <cmd...>
22
+ local desc="$1"; shift
23
+ if "$@" >/dev/null 2>&1; then ok "$desc"; else bad "$desc"; fi
24
+ }
25
+
26
+ # --- private throwaway tmux server -------------------------------------------
27
+ # Never the operator's live server: dedicated socket, no config, killed on exit.
28
+ unset TMUX TMUX_PANE
29
+ SOCK="mux-test-$$"
30
+ TMX() { tmux -L "$SOCK" -f /dev/null "$@"; }
31
+ cleanup() { TMX kill-server 2>/dev/null || true; rm -rf "$SANDBOX"; }
32
+ trap cleanup EXIT
33
+
34
+ # --- sandbox home -------------------------------------------------------------
35
+ mkdir -p "$FAKE_HOME/.config/mux" "$WT_DIR" "$FAKE_HOME/.local/share/mux/wt-health" \
36
+ "$FAKE_HOME/.claude/projects"
37
+ cp "$REPO_ROOT/templates/mux/config/worktree-session-health.sh" "$FAKE_HOME/.config/mux/"
38
+ cp "$REPO_ROOT/templates/mux/config/worktree-dirty-check.sh" "$FAKE_HOME/.config/mux/"
39
+ chmod +x "$FAKE_HOME/.config/mux/"*.sh
40
+
41
+ # Stub muxlib.py — answers only what cmd_new/cmd_resume/create_worktree ask.
42
+ # project-path mimics the real muxlib: prints the path, or exits 1 when the
43
+ # project isn't registered (STUB_PROJ_PATH empty/unset).
44
+ cat > "$FAKE_HOME/.config/mux/muxlib.py" <<'PY'
45
+ import sys, os
46
+ cmd = sys.argv[1] if len(sys.argv) > 1 else ""
47
+ if cmd == "project-path":
48
+ p = os.environ.get("STUB_PROJ_PATH", "")
49
+ if p:
50
+ print(p)
51
+ else:
52
+ sys.exit(1)
53
+ elif cmd == "project-names":
54
+ print("sess")
55
+ elif cmd in ("worktree-add", "worktree-remove"):
56
+ pass
57
+ elif cmd == "worktree-is-active":
58
+ print("yes")
59
+ elif cmd == "narrate-check":
60
+ print("yes")
61
+ PY
62
+ echo '{"active":[]}' > "$FAKE_HOME/.config/mux/worktrees.json"
63
+ echo '{}' > "$FAKE_HOME/.config/mux/projects.json"
64
+
65
+ # --- fixture projects ----------------------------------------------------------
66
+ # .claude/rules/ is tracked (= authored record), .claude/skills/ is gitignored
67
+ # (= disposable infra) — lets T3 assert the creation-path carve-out.
68
+ git init -q "$PROJ"
69
+ git -C "$PROJ" config user.email t@t.t; git -C "$PROJ" config user.name t
70
+ echo code > "$PROJ/file.txt"
71
+ mkdir -p "$PROJ/.claude/rules" "$PROJ/.claude/skills"
72
+ echo '.claude/skills/' > "$PROJ/.gitignore"
73
+ echo rule > "$PROJ/.claude/rules/r.md"
74
+ echo skill > "$PROJ/.claude/skills/s.md"
75
+ git -C "$PROJ" add -A && git -C "$PROJ" commit -qm init
76
+ MAIN_SLUG=$(echo "$PROJ" | sed 's|[/.]|-|g')
77
+ mkdir -p "$FAKE_HOME/.claude/projects/$MAIN_SLUG"
78
+
79
+ EMPTY_PROJ="$SANDBOX/empty-proj" # git repo, NO commits → worktree add must fail
80
+ git init -q "$EMPTY_PROJ"
81
+
82
+ PLAIN_DIR="$SANDBOX/plain-dir" # registered project that is not a git repo
83
+ mkdir -p "$PLAIN_DIR"
84
+
85
+ # --- test desk on the private server -------------------------------------------
86
+ HOME="$FAKE_HOME" TMX new-session -d -s sess -x 200 -y 50 -c "$PROJ" \
87
+ || { echo "FATAL: could not start private tmux server"; exit 1; }
88
+ SOCKET_PATH=$(TMX display-message -p -t sess '#{socket_path}')
89
+ SERVER_PID=$(TMX display-message -p -t sess '#{pid}')
90
+ PANE_ID=$(TMX display-message -p -t sess:0 '#{pane_id}')
91
+
92
+ # Run mux "inside" the private server: TMUX points at the test socket so
93
+ # in_tmux passes and every bare `tmux` call lands on the throwaway server.
94
+ run_mux() { # run_mux <stub-proj-path> <args...>
95
+ local stub="$1"; shift
96
+ env HOME="$FAKE_HOME" STUB_PROJ_PATH="$stub" \
97
+ TMUX="${SOCKET_PATH},${SERVER_PID},0" TMUX_PANE="$PANE_ID" \
98
+ bash "$MUX" "$@"
99
+ }
100
+ win_count() { TMX list-windows -t sess 2>/dev/null | wc -l | tr -d ' '; }
101
+ win_opt() { TMX show-options -wv -t "sess:$1" "$2" 2>/dev/null; }
102
+
103
+ ID_A="aaaaaaaa-1111-2222-3333-444444444444"
104
+ ID_B="bbbbbbbb-1111-2222-3333-444444444444"
105
+ ID_C="cccccccc-1111-2222-3333-444444444444"
106
+ ID_D="dddddddd-1111-2222-3333-444444444444"
107
+ ID_E="eeeeeeee-1111-2222-3333-444444444444"
108
+ ID_F="ffffffff-1111-2222-3333-444444444444"
109
+
110
+ echo "== T0: no surviving silent fallbacks in bin/mux (AC1)"
111
+ assert "zero '|| win_path=\"\$path\"' fallbacks remain" \
112
+ bash -c "! grep -q '|| win_path=\"\\\$path\"' '$MUX'"
113
+
114
+ echo "== T1: cmd_resume input guard (id flows into branch/path/send-keys)"
115
+ before=$(win_count)
116
+ out=$(run_mux "$PROJ" resume abc 2>&1); rc=$?
117
+ assert "id shorter than 8 chars dies ($rc)" test "$rc" -ne 0
118
+ out=$(run_mux "$PROJ" resume 'not$a-valid;id!!' 2>&1); rc=$?
119
+ assert "non-hex id dies ($rc)" test "$rc" -ne 0
120
+ assert "guard failures open no window" test "$(win_count)" -eq "$before"
121
+
122
+ echo "== T2: no active Claude => by-design main landing (stays working)"
123
+ before=$(win_count)
124
+ out=$(run_mux "$PROJ" resume "$ID_A" 2>&1); rc=$?
125
+ assert "resume with no active Claude exits 0 ($rc)" test "$rc" -eq 0
126
+ assert "window resume-aaaaaaaa opened" test "$(win_count)" -eq $((before + 1))
127
+ assert "main landing carries NO @mux_worktree flag" \
128
+ test -z "$(win_opt resume-aaaaaaaa @mux_worktree)"
129
+
130
+ # Activate the desk: a non-shell pane command makes has_active_claude true.
131
+ TMX new-window -t sess -d -n fakeclaude 'sleep 300'
132
+
133
+ echo "== T3: active Claude + git project => worktree + window options (AC3)"
134
+ out=$(run_mux "$PROJ" resume "$ID_B" 2>&1); rc=$?
135
+ assert "resume exits 0 ($rc)" test "$rc" -eq 0
136
+ assert "worktree created" test -d "$WT_DIR/sess-resume-bbbbbbbb"
137
+ assert "@mux_worktree returns 1 (AC3)" \
138
+ test "$(win_opt resume-bbbbbbbb @mux_worktree)" = "1"
139
+ assert "@wt_healthy set alongside (·wt indicator not red)" \
140
+ test "$(win_opt resume-bbbbbbbb @wt_healthy)" = "1"
141
+
142
+ echo "== T3b: creation-path carve-out — tracked .claude/ never frozen (act:14305d16)"
143
+ WT_B="$WT_DIR/sess-resume-bbbbbbbb"
144
+ assert "no tracked .claude/ file carries the assume-unchanged bit" \
145
+ bash -c "! git -C '$WT_B' ls-files -v .claude/ | grep -q '^h '"
146
+ assert "gitignored .claude/skills/ infra copied into the worktree" \
147
+ test -f "$WT_B/.claude/skills/s.md"
148
+ assert "tracked .claude/rules/ file checked out, not clobbered" \
149
+ bash -c "grep -qx rule '$WT_B/.claude/rules/r.md'"
150
+
151
+ echo "== T4: re-resume of the same id REUSES the existing worktree"
152
+ TMX kill-window -t sess:resume-bbbbbbbb 2>/dev/null
153
+ out=$(run_mux "$PROJ" resume "$ID_B" 2>&1); rc=$?
154
+ assert "second resume of same id exits 0 ($rc)" test "$rc" -eq 0
155
+ assert "existing worktree reused (no uniquified sibling)" \
156
+ bash -c "! ls -d '$WT_DIR'/sess-resume-bbbbbbbb-* 2>/dev/null | grep -q ."
157
+ assert "reused window flagged @mux_worktree=1" \
158
+ test "$(win_opt resume-bbbbbbbb @mux_worktree)" = "1"
159
+ assert "reused window flagged @wt_healthy=1" \
160
+ test "$(win_opt resume-bbbbbbbb @wt_healthy)" = "1"
161
+
162
+ echo "== T5: slug path occupied by an INVALID dir => die naming the path"
163
+ mkdir -p "$WT_DIR/sess-resume-cccccccc"
164
+ before=$(win_count)
165
+ out=$(run_mux "$PROJ" resume "$ID_C" 2>&1); rc=$?
166
+ assert "invalid leftover dies ($rc)" test "$rc" -ne 0
167
+ assert "die message names the colliding path" \
168
+ grep -q "sess-resume-cccccccc" <<<"$out"
169
+ assert "die message offers a recovery command" grep -q "rm -rf" <<<"$out"
170
+ assert "nothing opened in main" test "$(win_count)" -eq "$before"
171
+
172
+ echo "== T6: real worktree-creation failure => die, nothing opens (AC2)"
173
+ before=$(win_count)
174
+ out=$(run_mux "$EMPTY_PROJ" resume "$ID_D" 2>&1); rc=$?
175
+ assert "cmd_resume: forced creation failure dies ($rc)" test "$rc" -ne 0
176
+ assert "cmd_resume: die message refuses main checkout" \
177
+ grep -q "refusing to run work on the main checkout" <<<"$out"
178
+ assert "cmd_resume: nothing opened in main" test "$(win_count)" -eq "$before"
179
+ out=$(run_mux "$EMPTY_PROJ" new 2>&1); rc=$?
180
+ assert "cmd_new no-prompt: forced creation failure dies ($rc)" test "$rc" -ne 0
181
+ assert "cmd_new no-prompt: die message refuses main checkout" \
182
+ grep -q "refusing to run work on the main checkout" <<<"$out"
183
+ assert "cmd_new no-prompt: nothing opened in main" test "$(win_count)" -eq "$before"
184
+
185
+ echo "== T7: isolation impossible BY DESIGN => loud fall-through, not die"
186
+ before=$(win_count)
187
+ out=$(run_mux "$PLAIN_DIR" resume "$ID_E" 2>&1); rc=$?
188
+ assert "non-git project dir: resume still works ($rc)" test "$rc" -eq 0
189
+ assert "non-git project dir: visible warning names why" \
190
+ grep -q "isn't a git repository" <<<"$out"
191
+ assert "non-git project dir: window opened on main" test "$(win_count)" -eq $((before + 1))
192
+ assert "non-git landing carries NO @mux_worktree flag" \
193
+ test -z "$(win_opt resume-eeeeeeee @mux_worktree)"
194
+ before=$(win_count)
195
+ out=$(run_mux "" resume "$ID_F" 2>&1); rc=$?
196
+ assert "unregistered desk: resume still works ($rc)" test "$rc" -eq 0
197
+ assert "unregistered desk: visible warning names why" \
198
+ grep -q "isn't a registered mux project" <<<"$out"
199
+ assert "unregistered desk: window opened" test "$(win_count)" -eq $((before + 1))
200
+
201
+ echo "== T8: cmd_new no-prompt success path still isolates + flags"
202
+ before=$(win_count)
203
+ out=$(run_mux "$PROJ" new 2>&1); rc=$?
204
+ assert "cmd_new no-prompt exits 0 ($rc)" test "$rc" -eq 0
205
+ assert "window opened" test "$(win_count)" -eq $((before + 1))
206
+ new_wt=$(ls -d "$WT_DIR"/sess-window-* 2>/dev/null | head -1)
207
+ assert "worktree created for the new window" test -n "$new_wt"
208
+ if [[ -n "$new_wt" ]]; then
209
+ wname=$(basename "$new_wt"); wname="${wname#sess-}"
210
+ assert "new window flagged @mux_worktree=1" \
211
+ test "$(win_opt "$wname" @mux_worktree)" = "1"
212
+ assert "new window flagged @wt_healthy=1" \
213
+ test "$(win_opt "$wname" @wt_healthy)" = "1"
214
+ fi
215
+
216
+ echo "== T9: mux handoff seeds the next session into a WORKTREE, not main"
217
+ # Design intent (worktree-to-worktree handoffs): the seeded session is a
218
+ # second session on the desk — the shared-main commit-sweep class (1adc5ef)
219
+ # is what isolation prevents. Safe since the 40cc831 carve-out.
220
+ DESC="$SANDBOX/seed.json"
221
+ cat > "$DESC" <<JSON
222
+ { "project": "sess", "project_path": "$PROJ", "name": "seeded-next",
223
+ "seed_prompt": "continue the thing" }
224
+ JSON
225
+ before=$(win_count)
226
+ out=$(run_mux "$PROJ" handoff "$DESC" 2>&1); rc=$?
227
+ assert "handoff exits 0 ($rc)" test "$rc" -eq 0
228
+ assert "window opened" test "$(win_count)" -eq $((before + 1))
229
+ assert "worktree created for the seeded session" test -d "$WT_DIR/sess-seeded-next"
230
+ assert "seeded window flagged @mux_worktree=1" \
231
+ test "$(win_opt seeded-next @mux_worktree)" = "1"
232
+ assert "seeded window flagged @wt_healthy=1" \
233
+ test "$(win_opt seeded-next @wt_healthy)" = "1"
234
+ assert "launch message says worktree" grep -q "fresh worktree" <<<"$out"
235
+ assert "launch message does not claim main checkout" \
236
+ bash -c "! grep -q \"main checkout\" <<<'$out'"
237
+
238
+ echo "== T9b: handoff without isolation falls through LOUDLY to main"
239
+ DESC2="$SANDBOX/seed2.json"
240
+ cat > "$DESC2" <<JSON
241
+ { "project": "sess", "project_path": "$PLAIN_DIR", "name": "seeded-plain",
242
+ "seed_prompt": "x" }
243
+ JSON
244
+ before=$(win_count)
245
+ out=$(run_mux "$PLAIN_DIR" handoff "$DESC2" 2>&1); rc=$?
246
+ assert "non-git project handoff exits 0 ($rc)" test "$rc" -eq 0
247
+ assert "window opened" test "$(win_count)" -eq $((before + 1))
248
+ assert "loud fall-through names why" grep -q "No worktree isolation" <<<"$out"
249
+ assert "main landing carries NO @mux_worktree" \
250
+ test -z "$(win_opt seeded-plain @mux_worktree)"
251
+
252
+ echo ""
253
+ echo "RESULT: $pass passed, $fail failed (sandbox: $SANDBOX)"
254
+ exit $fail
@@ -0,0 +1,41 @@
1
+ // Guard test for the mux fail-loud sweep (act:fe6aab6f).
2
+ //
3
+ // Drives mux-fail-loud.fixture.sh, which runs cmd_new / cmd_resume against a
4
+ // PRIVATE throwaway tmux server (tmux -L mux-test-$$ -f /dev/null) plus a
5
+ // sandboxed git repo with HOME overridden — it never touches the operator's
6
+ // live tmux server, ~/.mux/worktrees, or ~/.config/mux. The test server is
7
+ // killed in cleanup even when assertions fail.
8
+ //
9
+ // Covers:
10
+ // - zero `|| win_path="$path"` silent fallbacks remain in bin/mux
11
+ // - forced worktree-creation failure on cmd_new (no-prompt) and cmd_resume
12
+ // exits non-zero and opens nothing in main
13
+ // - successful `mux resume` sets @mux_worktree=1 AND @wt_healthy=1
14
+ // - re-resume of the same id reuses the deterministic resume-<id8>
15
+ // worktree; an invalid leftover at that path dies naming it
16
+ // - by-design main landings stay working: no-active-Claude resume,
17
+ // unregistered desks, and non-git project dirs fall through LOUDLY
18
+ // - cmd_resume rejects non-id-shaped input before it reaches branch
19
+ // names / paths / send-keys
20
+
21
+ import { test } from 'node:test';
22
+ import assert from 'node:assert';
23
+ import { spawnSync } from 'node:child_process';
24
+ import { fileURLToPath } from 'node:url';
25
+ import path from 'node:path';
26
+
27
+ const fixture = path.join(
28
+ path.dirname(fileURLToPath(import.meta.url)),
29
+ 'mux-fail-loud.fixture.sh'
30
+ );
31
+
32
+ test('mux fail-loud sweep: cmd_new/cmd_resume (sandboxed tmux fixture suite)', () => {
33
+ const res = spawnSync('bash', [fixture], { encoding: 'utf8', timeout: 120_000 });
34
+ const output = `${res.stdout}\n${res.stderr}`;
35
+ assert.strictEqual(
36
+ res.status,
37
+ 0,
38
+ `fixture suite reported failures:\n${output}`
39
+ );
40
+ assert.match(output, /RESULT: \d+ passed, 0 failed/);
41
+ });
@@ -0,0 +1,184 @@
1
+ #!/bin/bash
2
+ # Sandboxed AC verification for act:04bdcc6b.
3
+ # Everything runs under a mktemp dir with HOME overridden — never touches
4
+ # the real ~/.mux/worktrees, ~/.config/mux, or any live worktree.
5
+ set -uo pipefail
6
+
7
+ REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
8
+ SANDBOX=$(mktemp -d /tmp/cc-dirty-check-test.XXXXXX)
9
+ FAKE_HOME="$SANDBOX/home"
10
+ PROJ="$SANDBOX/proj"
11
+ WT_DIR="$FAKE_HOME/.mux/worktrees"
12
+
13
+ pass=0; fail=0
14
+ ok() { echo " PASS: $1"; pass=$((pass+1)); }
15
+ bad() { echo " FAIL: $1"; fail=$((fail+1)); }
16
+ assert() { # assert <desc> <cmd...>
17
+ local desc="$1"; shift
18
+ if "$@" >/dev/null 2>&1; then ok "$desc"; else bad "$desc"; fi
19
+ }
20
+
21
+ # --- sandbox home -----------------------------------------------------------
22
+ mkdir -p "$FAKE_HOME/.config/mux" "$WT_DIR" "$FAKE_HOME/.local/share/mux/wt-health" \
23
+ "$FAKE_HOME/.local/bin" "$FAKE_HOME/.claude/projects"
24
+ cp "$REPO_ROOT/templates/mux/config/worktree-dirty-check.sh" "$FAKE_HOME/.config/mux/"
25
+ cp "$REPO_ROOT/templates/mux/config/worktree-cleanup.sh" "$FAKE_HOME/.config/mux/"
26
+ cp "$REPO_ROOT/templates/mux/config/worktree-session-health.sh" "$FAKE_HOME/.config/mux/"
27
+ chmod +x "$FAKE_HOME/.config/mux/"*.sh
28
+
29
+ # Stub muxlib.py — answers only what the worktree code paths ask.
30
+ cat > "$FAKE_HOME/.config/mux/muxlib.py" <<'PY'
31
+ import sys, os
32
+ cmd = sys.argv[1] if len(sys.argv) > 1 else ""
33
+ if cmd == "worktree-is-active":
34
+ print("yes")
35
+ elif cmd == "project-path":
36
+ print(os.environ.get("STUB_PROJ_PATH", ""))
37
+ elif cmd == "project-names":
38
+ print("sess")
39
+ elif cmd == "narrate-check":
40
+ print("yes")
41
+ PY
42
+ # Stub manage-notes.py — log invocations.
43
+ cat > "$FAKE_HOME/.config/mux/manage-notes.py" <<'PY'
44
+ import sys, os
45
+ with open(os.path.join(os.environ["HOME"], "notes-log.txt"), "a") as f:
46
+ f.write(" ".join(sys.argv[1:]) + "\n")
47
+ PY
48
+ echo '{"active":[]}' > "$FAKE_HOME/.config/mux/worktrees.json"
49
+ echo '{}' > "$FAKE_HOME/.config/mux/projects.json"
50
+
51
+ # --- fixture main repo ------------------------------------------------------
52
+ # .claude/skills/ is gitignored INFRA (refreshed by file copy); plans/,
53
+ # methodology/, and rules/ are TRACKED = authored record (sync via git only).
54
+ git init -q "$PROJ"
55
+ git -C "$PROJ" config user.email t@t.t; git -C "$PROJ" config user.name t
56
+ mkdir -p "$PROJ/.claude/plans" "$PROJ/.claude/methodology" "$PROJ/.claude/rules" "$PROJ/.claude/skills"
57
+ echo '.claude/skills/' > "$PROJ/.gitignore"
58
+ echo plan > "$PROJ/.claude/plans/existing.md"
59
+ echo meth > "$PROJ/.claude/methodology/m.md"
60
+ echo rule > "$PROJ/.claude/rules/r.md"
61
+ echo skill-v1 > "$PROJ/.claude/skills/skill.md"
62
+ echo '{"mcpServers":{}}' > "$PROJ/.mcp.json"
63
+ echo lock-v1 > "$PROJ/package-lock.json"
64
+ echo code > "$PROJ/file.txt"
65
+ git -C "$PROJ" add -A && git -C "$PROJ" commit -qm init
66
+ MAIN_SLUG=$(echo "$PROJ" | sed 's|[/.]|-|g')
67
+ mkdir -p "$FAKE_HOME/.claude/projects/$MAIN_SLUG"
68
+
69
+ mkwt() { git -C "$PROJ" worktree add -q "$WT_DIR/sess-$1" -b "mux/$1" HEAD; }
70
+ RUN_ENV=(env HOME="$FAKE_HOME" STUB_PROJ_PATH="$PROJ")
71
+ MUX="$REPO_ROOT/templates/mux/bin/mux"
72
+ HELPER="$FAKE_HOME/.config/mux/worktree-dirty-check.sh"
73
+ CLEANUP_HOOK="$FAKE_HOME/.config/mux/worktree-cleanup.sh"
74
+
75
+ echo "== T1: mux worktree refresh preserves uncommitted authored records (AC1)"
76
+ mkwt task1
77
+ mkdir -p "$WT_DIR/sess-task1/.claude/plans"
78
+ echo precious > "$WT_DIR/sess-task1/.claude/plans/test-file.md" # new, uncommitted
79
+ echo edited > "$WT_DIR/sess-task1/.claude/plans/existing.md" # modified, uncommitted
80
+ echo skill-v2 > "$PROJ/.claude/skills/skill.md" # infra changed in main
81
+ "${RUN_ENV[@]}" bash "$MUX" worktree refresh sess task1 >"$SANDBOX/t1.out" 2>&1
82
+ assert "uncommitted new plans file survives refresh" test -f "$WT_DIR/sess-task1/.claude/plans/test-file.md"
83
+ assert "uncommitted plans edit survives refresh" grep -qx edited "$WT_DIR/sess-task1/.claude/plans/existing.md"
84
+ assert "infra (skills) actually refreshed from main" grep -qx skill-v2 "$WT_DIR/sess-task1/.claude/skills/skill.md"
85
+ assert "methodology untouched" grep -qx meth "$WT_DIR/sess-task1/.claude/methodology/m.md"
86
+
87
+ echo "== T2: helper verdicts (single implementation)"
88
+ mkwt task2 # modified tracked .mcp.json -> dirty (AC2)
89
+ echo '{"mcpServers":{"x":{}}}' > "$WT_DIR/sess-task2/.mcp.json"
90
+ out=$("$HELPER" "$WT_DIR/sess-task2" "$PROJ")
91
+ assert "modified .mcp.json => dirty ($out)" grep -q 'uncommitted=1 verdict=dirty' <<<"$out"
92
+
93
+ mkwt task3 # lockfile churn + .mcp.json typechange + untracked .claude infra
94
+ # + node_modules symlink (gitignore `node_modules/` matches only
95
+ # dirs, so the mux-made symlink shows `?? node_modules` — found
96
+ # live 2026-06-11: it made every mux worktree immortal) -> clean (AC3)
97
+ echo lock-rewritten > "$WT_DIR/sess-task3/package-lock.json"
98
+ rm "$WT_DIR/sess-task3/.mcp.json"; ln -s "$PROJ/.mcp.json" "$WT_DIR/sess-task3/.mcp.json"
99
+ mkdir -p "$WT_DIR/sess-task3/.claude/agents"; echo x > "$WT_DIR/sess-task3/.claude/agents/a.md"
100
+ ln -s "$PROJ/node_modules" "$WT_DIR/sess-task3/node_modules"
101
+ out=$("$HELPER" "$WT_DIR/sess-task3" "$PROJ")
102
+ assert "lockfile + infra + node_modules symlink churn => clean ($out)" grep -q 'verdict=clean' <<<"$out"
103
+
104
+ mkwt task4 # untracked authored record -> dirty
105
+ mkdir -p "$WT_DIR/sess-task4/.claude/plans"; echo new > "$WT_DIR/sess-task4/.claude/plans/new-doc.md"
106
+ out=$("$HELPER" "$WT_DIR/sess-task4" "$PROJ")
107
+ assert "untracked .claude/plans/ doc => dirty ($out)" grep -q 'verdict=dirty' <<<"$out"
108
+
109
+ # task4c/d: the authored set is ANY top-level .claude/ entry with tracked
110
+ # files, not a static plans/methodology list (act:14305d16 — the
111
+ # assume-unchanged trap fired on .claude/rules/enforcement-pipeline.md).
112
+ mkwt task4c # MODIFIED tracked .claude/rules/ file -> dirty
113
+ echo edited-rule > "$WT_DIR/sess-task4c/.claude/rules/r.md"
114
+ out=$("$HELPER" "$WT_DIR/sess-task4c" "$PROJ")
115
+ assert "modified tracked .claude/rules/ file => dirty ($out)" grep -q 'verdict=dirty' <<<"$out"
116
+
117
+ mkwt task4d # NEW untracked doc beside tracked .claude/rules/ files -> dirty
118
+ echo new-rule > "$WT_DIR/sess-task4d/.claude/rules/new-rule.md"
119
+ out=$("$HELPER" "$WT_DIR/sess-task4d" "$PROJ")
120
+ assert "untracked doc in tracked .claude/rules/ => dirty ($out)" grep -q 'verdict=dirty' <<<"$out"
121
+
122
+ # task4b: repo whose .claude/ has NO tracked files — without
123
+ # --untracked-files=all, porcelain collapses everything to one '?? .claude/'
124
+ # line and the authored record inside is invisible (deletable). CP3 finding,
125
+ # group 2026-06-10-1.
126
+ BARE_PROJ="$SANDBOX/bare-proj"
127
+ git init -q "$BARE_PROJ"
128
+ git -C "$BARE_PROJ" config user.email t@t.t; git -C "$BARE_PROJ" config user.name t
129
+ git -C "$BARE_PROJ" commit -q --allow-empty -m init
130
+ mkdir -p "$BARE_PROJ/.claude/plans"; echo new > "$BARE_PROJ/.claude/plans/hidden-doc.md"
131
+ out=$("$HELPER" "$BARE_PROJ" "$BARE_PROJ")
132
+ assert "authored record inside fully-untracked .claude/ => dirty ($out)" grep -q 'verdict=dirty' <<<"$out"
133
+ # ...while pure infra inside a fully-untracked .claude/ stays clean churn
134
+ BARE_PROJ2="$SANDBOX/bare-proj2"
135
+ git init -q "$BARE_PROJ2"
136
+ git -C "$BARE_PROJ2" config user.email t@t.t; git -C "$BARE_PROJ2" config user.name t
137
+ git -C "$BARE_PROJ2" commit -q --allow-empty -m init
138
+ mkdir -p "$BARE_PROJ2/.claude/agents"; echo x > "$BARE_PROJ2/.claude/agents/a.md"
139
+ out=$("$HELPER" "$BARE_PROJ2" "$BARE_PROJ2")
140
+ assert "pure infra inside fully-untracked .claude/ => clean ($out)" grep -q 'verdict=clean' <<<"$out"
141
+
142
+ out=$("$HELPER" "$SANDBOX/nonexistent" "$PROJ")
143
+ assert "broken worktree => fail-DIRTY ($out)" grep -q 'commits=? uncommitted=? verdict=dirty' <<<"$out"
144
+
145
+ echo "== T3: cleanup hook keeps dirty worktree (modified .mcp.json) (AC2)"
146
+ "${RUN_ENV[@]}" bash "$CLEANUP_HOOK" sess task2 >"$SANDBOX/t3.out" 2>&1
147
+ assert "worktree with real .mcp.json edit NOT deleted" test -d "$WT_DIR/sess-task2"
148
+ assert "sticky note left for dirty worktree" grep -q "task2" "$FAKE_HOME/notes-log.txt"
149
+
150
+ echo "== T4: cleanup hook silently removes lockfile-only worktree (AC3)"
151
+ "${RUN_ENV[@]}" bash "$CLEANUP_HOOK" sess task3 >"$SANDBOX/t4.out" 2>&1
152
+ assert "lockfile-churn-only worktree removed as clean" test ! -d "$WT_DIR/sess-task3"
153
+
154
+ echo "== T5: interactive mux worktree cleanup agrees (AC2 + AC3, same verdict)"
155
+ mkwt task5 # lockfile-only -> clean, auto-removed without prompt
156
+ echo lock-rewritten > "$WT_DIR/sess-task5/package-lock.json"
157
+ out=$("${RUN_ENV[@]}" bash "$MUX" worktree cleanup sess task5 </dev/null 2>&1)
158
+ assert "bin/mux: lockfile-only cleaned up as clean" grep -q "cleaned up (no changes)" <<<"$out"
159
+ assert "bin/mux: lockfile-only worktree removed" test ! -d "$WT_DIR/sess-task5"
160
+
161
+ mkwt task6 # modified .mcp.json -> dirty, prompts instead of deleting
162
+ echo '{"changed":true}' > "$WT_DIR/sess-task6/.mcp.json"
163
+ out=$(printf 'x\n' | "${RUN_ENV[@]}" bash "$MUX" worktree cleanup sess task6 2>&1) || true
164
+ assert "bin/mux: modified .mcp.json reported dirty" grep -q "has work to merge" <<<"$out"
165
+ assert "bin/mux: dirty worktree not deleted" test -d "$WT_DIR/sess-task6"
166
+
167
+ echo "== T6: missing helper degrades to fail-DIRTY (never deletes)"
168
+ mkwt task7 # truly clean worktree, but helper missing
169
+ mv "$HELPER" "$HELPER.disabled"
170
+ "${RUN_ENV[@]}" bash "$CLEANUP_HOOK" sess task7 >"$SANDBOX/t6.out" 2>&1
171
+ assert "hook without helper does NOT delete (fail-DIRTY)" test -d "$WT_DIR/sess-task7"
172
+ out=$(printf 'x\n' | "${RUN_ENV[@]}" bash "$MUX" worktree cleanup sess task7 2>&1) || true
173
+ assert "bin/mux without helper treats as dirty" grep -q "has work to merge" <<<"$out"
174
+ mv "$HELPER.disabled" "$HELPER"
175
+
176
+ echo "== T7: truly clean worktree still removed (baseline)"
177
+ mkwt task8
178
+ "${RUN_ENV[@]}" bash "$CLEANUP_HOOK" sess task8 >"$SANDBOX/t7.out" 2>&1
179
+ assert "clean worktree silently removed by hook" test ! -d "$WT_DIR/sess-task8"
180
+
181
+ echo ""
182
+ echo "RESULT: $pass passed, $fail failed (sandbox: $SANDBOX)"
183
+ [[ $fail -eq 0 ]] && rm -rf "$SANDBOX"
184
+ exit $fail