acstack 0.0.0-stage → 0.47.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 (143) hide show
  1. package/.claude-plugin/plugin.json +19 -0
  2. package/LICENSE +21 -0
  3. package/README.md +389 -2
  4. package/THIRD_PARTY_NOTICES +14 -0
  5. package/TROUBLESHOOTING.md +225 -0
  6. package/agents/codex/frontend-verifier.toml +352 -0
  7. package/agents/codex/task-checker.toml +345 -0
  8. package/agents/codex/task-executor.toml +157 -0
  9. package/agents/codex/test-writer.toml +149 -0
  10. package/agents/frontend-verifier.md +351 -0
  11. package/agents/task-checker.md +344 -0
  12. package/agents/task-executor.md +156 -0
  13. package/agents/test-writer.md +148 -0
  14. package/assets/playwriter-bridge/build-launcher.ps1 +32 -0
  15. package/assets/playwriter-bridge/doctor.ps1 +450 -0
  16. package/assets/playwriter-bridge/launcher-src.cs +131 -0
  17. package/assets/playwriter-bridge/opener.js +330 -0
  18. package/assets/playwriter-bridge/opener.ps1 +63 -0
  19. package/assets/playwriter-bridge/register-tasks.ps1 +328 -0
  20. package/assets/playwriter-bridge/relay.ps1 +50 -0
  21. package/assets/playwriter-bridge/stop.ps1 +125 -0
  22. package/assets/playwriter-bridge/sync.ps1 +271 -0
  23. package/assets/playwriter-bridge/tunnel.ps1 +325 -0
  24. package/assets/playwriter-bridge/vm-free-port.sh +166 -0
  25. package/assets/terminal-workspace/client-keybindings.md +87 -0
  26. package/assets/terminal-workspace/config.example.yaml +329 -0
  27. package/assets/terminal-workspace/setup.sh +4317 -0
  28. package/cli/README.md +47 -0
  29. package/cli/bin/acstack.js +3 -0
  30. package/cli/dist/index.js +25984 -0
  31. package/cli/dist/index.js.map +7 -0
  32. package/cli/package.json +59 -0
  33. package/docs/adr/0001-ste-writing-standard.md +16 -0
  34. package/docs/adr/0002-version-gate-not-auto-bump.md +35 -0
  35. package/docs/adr/0003-handoff-ack-command.md +19 -0
  36. package/docs/adr/0004-dev-setup-per-host.md +13 -0
  37. package/docs/adr/0005-two-orchestrators-per-host.md +25 -0
  38. package/docs/adr/0006-document-opener-service.md +89 -0
  39. package/docs/adr/0007-lead-check-reads-the-transcript.md +87 -0
  40. package/docs/adr/0008-cli-audits-skill-interviews.md +37 -0
  41. package/docs/adr/0009-verify-the-write-not-the-writer.md +51 -0
  42. package/docs/adr/0010-promote-dev-to-main-by-fast-forward.md +115 -0
  43. package/docs/adr/0011-node-header-is-the-edge-encoding.md +20 -0
  44. package/docs/adr/0012-node-kind-is-a-closed-set-of-five.md +31 -0
  45. package/docs/adr/0013-the-diagram-renderer-is-pure.md +23 -0
  46. package/docs/adr/0014-reading-a-node-hides-its-generated-diagram.md +29 -0
  47. package/docs/adr/0015-a-node-id-links-to-its-issue-through-an-anchor.md +37 -0
  48. package/docs/adr/0016-the-map-root-carries-no-status-of-its-own.md +21 -0
  49. package/docs/adr/0017-call-impeccable-copy-only-its-questions.md +13 -0
  50. package/docs/adr/index.md +17 -0
  51. package/docs/config-reference.md +572 -0
  52. package/docs/dev-setups.md +164 -0
  53. package/docs/migrate-0.8.md +28 -0
  54. package/docs/playwriter-bridge-windows.md +15 -0
  55. package/docs/terminal-workspace-keys.md +137 -0
  56. package/docs/terminal-workspace.md +1774 -0
  57. package/docs/workflows.md +309 -0
  58. package/hooks/ac-writing-style-reminder.sh +86 -0
  59. package/hooks/context-pressure.sh +583 -0
  60. package/hooks/hooks.json +50 -0
  61. package/hooks/session-start-handoff.sh +165 -0
  62. package/hooks/session-start.sh +214 -0
  63. package/orchestrator/AGENTS.md +532 -0
  64. package/output-styles/ac-writing-style.md +286 -0
  65. package/package.json +50 -4
  66. package/pi/agents/frontend-verifier.md +357 -0
  67. package/pi/agents/task-checker.md +348 -0
  68. package/pi/agents/task-executor.md +160 -0
  69. package/pi/agents/test-writer.md +154 -0
  70. package/pi/extension.ts +723 -0
  71. package/profiles/fullstack-python.yaml +29 -0
  72. package/profiles/node-typescript.yaml +23 -0
  73. package/profiles/python.yaml +24 -0
  74. package/skills/archive/SKILL.md +160 -0
  75. package/skills/bootstrap/SKILL.md +109 -0
  76. package/skills/bridge/HANDOFF.md +93 -0
  77. package/skills/bridge/SKILL-VM.md +292 -0
  78. package/skills/bridge/SKILL-WINDOWS.md +526 -0
  79. package/skills/bridge/SKILL.md +121 -0
  80. package/skills/browser-verify/SKILL.md +229 -0
  81. package/skills/code-review/SKILL.md +44 -0
  82. package/skills/commit/SKILL.md +106 -0
  83. package/skills/compact-and-continue/SKILL.md +241 -0
  84. package/skills/explore/SKILL.md +149 -0
  85. package/skills/grilling/SKILL.md +134 -0
  86. package/skills/handoff/SKILL.md +250 -0
  87. package/skills/handoff/launch-herdr.md +197 -0
  88. package/skills/handoff/launch-without-herdr.md +30 -0
  89. package/skills/handoff/lead-check.md +59 -0
  90. package/skills/handoff/outcomes.md +162 -0
  91. package/skills/handoff/running-agent.md +50 -0
  92. package/skills/handoff/watch.md +115 -0
  93. package/skills/host/SKILL.md +247 -0
  94. package/skills/host/detect-host.sh +297 -0
  95. package/skills/host/herdr.md +79 -0
  96. package/skills/host/install-herdr-block.sh +467 -0
  97. package/skills/host/install-orca.sh +512 -0
  98. package/skills/host/notes.md +97 -0
  99. package/skills/host/orca.md +160 -0
  100. package/skills/host/questions.md +163 -0
  101. package/skills/host/report.md +42 -0
  102. package/skills/implement/SKILL.md +230 -0
  103. package/skills/implement/decision-log.md +62 -0
  104. package/skills/implement/design-pass.md +32 -0
  105. package/skills/implement/relaxed-tdd.md +28 -0
  106. package/skills/map/SKILL.md +250 -0
  107. package/skills/map/frontend.md +227 -0
  108. package/skills/map/trackers/files.md +74 -0
  109. package/skills/map/trackers/github.md +180 -0
  110. package/skills/map/visuals.md +387 -0
  111. package/skills/new-worktree/SKILL.md +155 -0
  112. package/skills/new-worktree/detect-orchestrator.sh +328 -0
  113. package/skills/new-worktree/detect-orchestrator.test.sh +894 -0
  114. package/skills/new-worktree/herdr.md +84 -0
  115. package/skills/new-worktree/orca.md +101 -0
  116. package/skills/pre-commit-review/SKILL.md +242 -0
  117. package/skills/pre-commit-review/auto-fix.md +19 -0
  118. package/skills/pre-commit-review/interactive.md +35 -0
  119. package/skills/quick-fix/SKILL.md +125 -0
  120. package/skills/record-context/SKILL.md +106 -0
  121. package/skills/setup/SKILL.md +250 -0
  122. package/skills/setup/browser.md +123 -0
  123. package/skills/setup/design-review.md +97 -0
  124. package/skills/setup/first-run.md +114 -0
  125. package/skills/setup/report.md +68 -0
  126. package/skills/setup/row-fixes.md +196 -0
  127. package/skills/setup/terminal-workspace.md +153 -0
  128. package/skills/sync/SKILL.md +171 -0
  129. package/skills/teardown-worktree/SKILL.md +245 -0
  130. package/skills/teardown-worktree/detector.md +39 -0
  131. package/skills/teardown-worktree/herdr.md +113 -0
  132. package/skills/teardown-worktree/orca.md +61 -0
  133. package/skills/terminal-workspace/SKILL.md +249 -0
  134. package/skills/terminal-workspace/components.md +36 -0
  135. package/skills/terminal-workspace/config-choices.md +69 -0
  136. package/skills/terminal-workspace/emulator-keys.md +48 -0
  137. package/skills/terminal-workspace/headless-clipboard.md +175 -0
  138. package/skills/terminal-workspace/turn-off-and-update.md +104 -0
  139. package/skills/test-conventions/SKILL.md +59 -0
  140. package/skills/visual-docs/SKILL.md +246 -0
  141. package/skills/writing/SKILL.md +231 -0
  142. package/skills/writing/examples.md +153 -0
  143. package/skills/writing/vocabulary.md +96 -0
@@ -0,0 +1,894 @@
1
+ #!/usr/bin/env bash
2
+ # Test harness for detect-orchestrator.sh.
3
+ #
4
+ # detect-orchestrator.sh is a read-only report. It prints which orchestrators
5
+ # this host DECLARES it has, one boolean per orchestrator (via the ACStack
6
+ # CLI's `config get host.orchestrators.herdr` and
7
+ # `config get host.orchestrators.orca`), and which one is actually DETECTED as
8
+ # hosting the current session (via environment markers), then reconciles the
9
+ # two into an ACTIVE orchestrator plus an optional WARNING line.
10
+ #
11
+ # The two declarations are independent: a host can have both orchestrators
12
+ # installed, or neither, so there is no single "the declared orchestrator"
13
+ # any more.
14
+ #
15
+ # Each case runs the script in a subshell with HOME pointed at a scratch
16
+ # directory (containing a fake ACStack CLI, when the case needs one) and
17
+ # with HERDR_ENV / HERDR_PANE_ID / ORCA_PANE_KEY explicitly unset and then
18
+ # re-set as the case requires, so nothing leaks in from the real session
19
+ # this suite happens to be running inside.
20
+ #
21
+ # The report also names the OWNER of the checkout it is looking at. A checkout
22
+ # is one git worktree directory, and its path root names the owner: a path
23
+ # under $HOME/orca/workspaces/ belongs to orca, a path under herdr's worktree
24
+ # root belongs to herdr, and anything else belongs to plain git. Cases that
25
+ # care about the owner therefore run the script from a scratch directory built
26
+ # under the case's fake HOME, or pass that directory as the script's first
27
+ # argument.
28
+ #
29
+ # Run it: bash skills/new-worktree/detect-orchestrator.test.sh
30
+ # Exit code is 0 when every check passes, 1 otherwise.
31
+
32
+ set -u
33
+
34
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
35
+ SUT="${SCRIPT_DIR}/detect-orchestrator.sh"
36
+
37
+ WORK="$(mktemp -d)"
38
+ trap 'rm -rf "$WORK"' EXIT
39
+ cd "$WORK" || exit 1
40
+
41
+ pass=0
42
+ fail=0
43
+ ok() { printf ' \033[32mPASS\033[0m %s\n' "$1"; pass=$((pass + 1)); }
44
+ no() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; fail=$((fail + 1)); }
45
+ check() { if eval "$2"; then ok "$1"; else no "$1"; fi; }
46
+
47
+ # --- helpers ------------------------------------------------------------
48
+
49
+ # Write a fake ACStack CLI at $1/.claude/acstack/bin/acstack that answers
50
+ # EVERY invocation the same way: printing $2 (verbatim, plus a trailing
51
+ # newline) and exiting with code $3 (default 0). For the cases where the tool
52
+ # itself is the subject, so what it was asked does not matter.
53
+ mk_cli() {
54
+ local home_dir="$1" output="$2" code="${3:-0}"
55
+ local bin_dir="$home_dir/.claude/acstack/bin"
56
+ mkdir -p "$bin_dir"
57
+ printf '%s' "$output" > "$bin_dir/.output"
58
+ printf '%s' "$code" > "$bin_dir/.exitcode"
59
+ cat > "$bin_dir/acstack" <<'CLIEOF'
60
+ #!/usr/bin/env bash
61
+ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
62
+ cat "$DIR/.output"
63
+ exit "$(cat "$DIR/.exitcode")"
64
+ CLIEOF
65
+ chmod +x "$bin_dir/acstack"
66
+ }
67
+
68
+ # Write a fake ACStack CLI that answers the two orchestrator axes separately:
69
+ # `config get host.orchestrators.herdr` prints $2 and exits $3, and
70
+ # `config get host.orchestrators.orca` prints $4 and exits $5.
71
+ #
72
+ # Any other key exits 1 without printing, so a script that still asks the
73
+ # retired `host.orchestrator` key reads as a tool failure rather than
74
+ # silently passing these cases.
75
+ mk_cli_axes() {
76
+ local home_dir="$1"
77
+ local bin_dir="$home_dir/.claude/acstack/bin"
78
+ mkdir -p "$bin_dir"
79
+ printf '%s' "$2" > "$bin_dir/.herdr.output"
80
+ printf '%s' "$3" > "$bin_dir/.herdr.exitcode"
81
+ printf '%s' "$4" > "$bin_dir/.orca.output"
82
+ printf '%s' "$5" > "$bin_dir/.orca.exitcode"
83
+ cat > "$bin_dir/acstack" <<'CLIEOF'
84
+ #!/usr/bin/env bash
85
+ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
86
+ case "${3:-}" in
87
+ host.orchestrators.herdr) axis=herdr ;;
88
+ host.orchestrators.orca) axis=orca ;;
89
+ *) exit 1 ;;
90
+ esac
91
+ cat "$DIR/.$axis.output"
92
+ exit "$(cat "$DIR/.$axis.exitcode")"
93
+ CLIEOF
94
+ chmod +x "$bin_dir/acstack"
95
+ }
96
+
97
+ # The readable form of mk_cli_axes: each of $2 (herdr) and $3 (orca) is one
98
+ # of true, false or unset, and is turned into the answer a real CLI would
99
+ # give – the JSON boolean on exit 0, or exit 2 with no output for a key that
100
+ # was never set.
101
+ mk_cli_declared() {
102
+ local home_dir="$1" herdr="$2" orca="$3"
103
+ local h_out h_code o_out o_code
104
+ case "$herdr" in
105
+ true) h_out="true"; h_code=0 ;;
106
+ false) h_out="false"; h_code=0 ;;
107
+ unset) h_out=""; h_code=2 ;;
108
+ *) echo "mk_cli_declared: bad herdr spec: $herdr" >&2; exit 1 ;;
109
+ esac
110
+ case "$orca" in
111
+ true) o_out="true"; o_code=0 ;;
112
+ false) o_out="false"; o_code=0 ;;
113
+ unset) o_out=""; o_code=2 ;;
114
+ *) echo "mk_cli_declared: bad orca spec: $orca" >&2; exit 1 ;;
115
+ esac
116
+ mk_cli_axes "$home_dir" "$h_out" "$h_code" "$o_out" "$o_code"
117
+ }
118
+
119
+ # Run the SUT with HOME=$1 and the given env-setup snippet ($2, eval'd in a
120
+ # subshell before invoking the script) applied on top of a clean slate
121
+ # where HERDR_ENV, HERDR_PANE_ID and ORCA_PANE_KEY start unset. Echoes
122
+ # stdout only (stderr discarded); caller reads $? for the exit code.
123
+ run_case() {
124
+ local home_dir="$1" env_setup="$2"
125
+ (
126
+ unset HERDR_ENV HERDR_PANE_ID ORCA_PANE_KEY
127
+ export HOME="$home_dir"
128
+ eval "$env_setup"
129
+ bash "$SUT" 2>/dev/null
130
+ )
131
+ }
132
+
133
+ # Extract the value for key $2 out of full output $1.
134
+ get_val() {
135
+ printf '%s\n' "$1" | grep "^${2}=" | head -n1 | sed "s/^${2}=//"
136
+ }
137
+
138
+ new_home() { mktemp -d -p "$WORK"; }
139
+
140
+ # run_case, but from a given working directory ($2), so the case controls which
141
+ # checkout the script is looking at. cd is left logical on purpose: a case that
142
+ # hands over a symlinked path is testing that the script resolves it itself.
143
+ run_case_in() {
144
+ local home_dir="$1" cwd_dir="$2" env_setup="$3"
145
+ (
146
+ unset HERDR_ENV HERDR_PANE_ID ORCA_PANE_KEY
147
+ export HOME="$home_dir"
148
+ cd "$cwd_dir" || exit 1
149
+ eval "$env_setup"
150
+ bash "$SUT" 2>/dev/null
151
+ )
152
+ }
153
+
154
+ # run_case, but passing $2 as the script's first argument, so the case names the
155
+ # checkout to examine rather than standing in it.
156
+ run_case_arg() {
157
+ local home_dir="$1" path_arg="$2" env_setup="$3"
158
+ (
159
+ unset HERDR_ENV HERDR_PANE_ID ORCA_PANE_KEY
160
+ export HOME="$home_dir"
161
+ eval "$env_setup"
162
+ bash "$SUT" "$path_arg" 2>/dev/null
163
+ )
164
+ }
165
+
166
+ # Create the checkout $2/$3 under home $1's default herdr worktree root and echo
167
+ # its path.
168
+ mk_herdr_checkout() {
169
+ local path="$1/.herdr/worktrees/$2/$3"
170
+ mkdir -p "$path"
171
+ printf '%s\n' "$path"
172
+ }
173
+
174
+ # Create the orca workspace $2 under home $1 and echo its path.
175
+ mk_orca_checkout() {
176
+ local path="$1/orca/workspaces/$2"
177
+ mkdir -p "$path"
178
+ printf '%s\n' "$path"
179
+ }
180
+
181
+ # Write herdr's config file under home $1 with a [worktrees] section naming $2
182
+ # as the worktree root. $2 is written verbatim, so a case can pass either a
183
+ # "~/..." path or an absolute one.
184
+ mk_herdr_config() {
185
+ local home_dir="$1" dir="$2"
186
+ mkdir -p "$home_dir/.config/herdr"
187
+ cat > "$home_dir/.config/herdr/config.toml" <<EOF
188
+ [worktrees]
189
+ directory = "$dir"
190
+ EOF
191
+ }
192
+
193
+ # Write a herdr config file under home $1 that exists but says nothing about
194
+ # worktrees, so the default root still applies.
195
+ mk_herdr_config_without_worktrees() {
196
+ local home_dir="$1"
197
+ mkdir -p "$home_dir/.config/herdr"
198
+ cat > "$home_dir/.config/herdr/config.toml" <<'EOF'
199
+ [general]
200
+ theme = "dark"
201
+ EOF
202
+ }
203
+
204
+ echo "Testing: $SUT"
205
+ echo "Workdir: $WORK"
206
+ echo ""
207
+
208
+ # =========================================================================
209
+ # R1 – output shape: one line per declared orchestrator, then the rest
210
+ # =========================================================================
211
+ echo "Case 1: basic run – output shape (R1)"
212
+ H1="$(new_home)"
213
+ mk_cli_declared "$H1" false false
214
+ OUT="$(run_case "$H1" '')"
215
+ CODE=$?
216
+ LINES="$(printf '%s\n' "$OUT" | wc -l | tr -d ' ')"
217
+ check "exit code is 0" '[ "$CODE" -eq 0 ]'
218
+ check "prints exactly 6 lines" '[ "$LINES" -eq 6 ]'
219
+ check "line 1 is declared_herdr=" 'printf "%s\n" "$OUT" | sed -n "1p" | grep -q "^declared_herdr="'
220
+ check "line 2 is declared_orca=" 'printf "%s\n" "$OUT" | sed -n "2p" | grep -q "^declared_orca="'
221
+ check "line 3 is detected=" 'printf "%s\n" "$OUT" | sed -n "3p" | grep -q "^detected="'
222
+ check "line 4 is active=" 'printf "%s\n" "$OUT" | sed -n "4p" | grep -q "^active="'
223
+ check "line 5 is owner=" 'printf "%s\n" "$OUT" | sed -n "5p" | grep -q "^owner="'
224
+ check "line 6 is warning=" 'printf "%s\n" "$OUT" | sed -n "6p" | grep -q "^warning="'
225
+ check "no bare declared= line remains" '! printf "%s\n" "$OUT" | grep -q "^declared="'
226
+ check "declared_herdr=false" '[ "$(get_val "$OUT" declared_herdr)" = "false" ]'
227
+ check "declared_orca=false" '[ "$(get_val "$OUT" declared_orca)" = "false" ]'
228
+ check "detected=none" '[ "$(get_val "$OUT" detected)" = "none" ]'
229
+ check "active=none" '[ "$(get_val "$OUT" active)" = "none" ]'
230
+ check "owner=none (a plain git checkout)" '[ "$(get_val "$OUT" owner)" = "none" ]'
231
+ check "warning is empty" '[ -z "$(get_val "$OUT" warning)" ]'
232
+
233
+ # =========================================================================
234
+ # R2 – declared: two independent booleans
235
+ # =========================================================================
236
+ echo "Case 2: CLI prints true with surrounding whitespace -> trimmed (R2)"
237
+ H2="$(new_home)"
238
+ mk_cli_axes "$H2" " true " 0 " false " 0
239
+ OUT="$(run_case "$H2" '')"
240
+ check "declared_herdr=true (trimmed)" '[ "$(get_val "$OUT" declared_herdr)" = "true" ]'
241
+ check "declared_orca=false (trimmed)" '[ "$(get_val "$OUT" declared_orca)" = "false" ]'
242
+
243
+ echo "Case 3: the two booleans are read independently (R2)"
244
+ H3="$(new_home)"
245
+ mk_cli_declared "$H3" false true
246
+ OUT="$(run_case "$H3" '')"
247
+ check "declared_herdr=false" '[ "$(get_val "$OUT" declared_herdr)" = "false" ]'
248
+ check "declared_orca=true" '[ "$(get_val "$OUT" declared_orca)" = "true" ]'
249
+
250
+ echo "Case 4: both are declared true (R2)"
251
+ H4="$(new_home)"
252
+ mk_cli_declared "$H4" true true
253
+ OUT="$(run_case "$H4" '')"
254
+ check "declared_herdr=true" '[ "$(get_val "$OUT" declared_herdr)" = "true" ]'
255
+ check "declared_orca=true" '[ "$(get_val "$OUT" declared_orca)" = "true" ]'
256
+
257
+ echo "Case 5: CLI exits 2 for both keys (never declared) -> unset, no warning (R2)"
258
+ H5="$(new_home)"
259
+ mk_cli_declared "$H5" unset unset
260
+ OUT="$(run_case "$H5" '')"
261
+ check "declared_herdr=unset" '[ "$(get_val "$OUT" declared_herdr)" = "unset" ]'
262
+ check "declared_orca=unset" '[ "$(get_val "$OUT" declared_orca)" = "unset" ]'
263
+ check "no warning (nothing is hosting either)" '[ -z "$(get_val "$OUT" warning)" ]'
264
+
265
+ echo "Case 6: one key unset, the other declared -> only that one is unset (R2)"
266
+ H6="$(new_home)"
267
+ mk_cli_axes "$H6" "true" 0 "" 2
268
+ OUT="$(run_case "$H6" '')"
269
+ check "declared_herdr=true" '[ "$(get_val "$OUT" declared_herdr)" = "true" ]'
270
+ check "declared_orca=unset" '[ "$(get_val "$OUT" declared_orca)" = "unset" ]'
271
+
272
+ echo "Case 7: CLI exits 0 with an unrecognised value -> that key is unset, the other still read (R2)"
273
+ H7="$(new_home)"
274
+ mk_cli_axes "$H7" "tmux" 0 "true" 0
275
+ OUT="$(run_case "$H7" '')"
276
+ check "declared_herdr=unset" '[ "$(get_val "$OUT" declared_herdr)" = "unset" ]'
277
+ check "declared_orca=true (unaffected)" '[ "$(get_val "$OUT" declared_orca)" = "true" ]'
278
+ check "warning mentions the unrecognised value" 'printf "%s" "$(get_val "$OUT" warning)" | grep -q "tmux"'
279
+
280
+ echo "Case 8: CLI path does not exist -> both declarations unreadable (R2)"
281
+ H8="$(new_home)"
282
+ mkdir -p "$H8"
283
+ OUT="$(run_case "$H8" '')"
284
+ check "declared_herdr=unset" '[ "$(get_val "$OUT" declared_herdr)" = "unset" ]'
285
+ check "declared_orca=unset" '[ "$(get_val "$OUT" declared_orca)" = "unset" ]'
286
+ WARN8="$(get_val "$OUT" warning)"
287
+ check "warning says CLI could not be run" 'printf "%s" "$WARN8" | grep -qi "acstack"'
288
+ check "warning mentions restarting Claude Code" 'printf "%s" "$WARN8" | grep -qi "restart"'
289
+
290
+ echo "Case 9: CLI exists but is not executable (R2)"
291
+ H9="$(new_home)"
292
+ mk_cli_declared "$H9" true false
293
+ chmod -x "$H9/.claude/acstack/bin/acstack"
294
+ OUT="$(run_case "$H9" '')"
295
+ check "declared_herdr=unset" '[ "$(get_val "$OUT" declared_herdr)" = "unset" ]'
296
+ check "declared_orca=unset" '[ "$(get_val "$OUT" declared_orca)" = "unset" ]'
297
+ WARN9="$(get_val "$OUT" warning)"
298
+ check "warning says CLI could not be run" 'printf "%s" "$WARN9" | grep -qi "acstack"'
299
+ check "warning mentions restarting Claude Code" 'printf "%s" "$WARN9" | grep -qi "restart"'
300
+
301
+ echo "Case 10: CLI exits with an unrelated non-zero code (R2)"
302
+ H10="$(new_home)"
303
+ mk_cli "$H10" "boom" 1
304
+ OUT="$(run_case "$H10" '')"
305
+ check "declared_herdr=unset" '[ "$(get_val "$OUT" declared_herdr)" = "unset" ]'
306
+ check "declared_orca=unset" '[ "$(get_val "$OUT" declared_orca)" = "unset" ]'
307
+ check "warning mentions the CLI failed" 'printf "%s" "$(get_val "$OUT" warning)" | grep -qi "fail"'
308
+
309
+ # =========================================================================
310
+ # R3 – detected: environment markers, read without help from the CLI
311
+ # =========================================================================
312
+ echo "Case 11: HERDR_ENV set, herdr declared true -> detected=herdr, no warning (R3)"
313
+ H11="$(new_home)"
314
+ mk_cli_declared "$H11" true false
315
+ OUT="$(run_case "$H11" 'export HERDR_ENV=1')"
316
+ check "detected=herdr" '[ "$(get_val "$OUT" detected)" = "herdr" ]'
317
+ check "active=herdr" '[ "$(get_val "$OUT" active)" = "herdr" ]'
318
+ check "warning is empty" '[ -z "$(get_val "$OUT" warning)" ]'
319
+
320
+ echo "Case 12: HERDR_PANE_ID set, herdr declared true -> detected=herdr (R3)"
321
+ H12="$(new_home)"
322
+ mk_cli_declared "$H12" true false
323
+ OUT="$(run_case "$H12" 'export HERDR_PANE_ID=pane-1')"
324
+ check "detected=herdr" '[ "$(get_val "$OUT" detected)" = "herdr" ]'
325
+ check "warning is empty" '[ -z "$(get_val "$OUT" warning)" ]'
326
+
327
+ echo "Case 13: ORCA_PANE_KEY set, orca declared true -> detected=orca, no warning (R3)"
328
+ H13="$(new_home)"
329
+ mk_cli_declared "$H13" false true
330
+ OUT="$(run_case "$H13" 'export ORCA_PANE_KEY=key-1')"
331
+ check "detected=orca" '[ "$(get_val "$OUT" detected)" = "orca" ]'
332
+ check "active=orca" '[ "$(get_val "$OUT" active)" = "orca" ]'
333
+ check "warning is empty" '[ -z "$(get_val "$OUT" warning)" ]'
334
+
335
+ echo "Case 14: HERDR_ENV set but empty -> detected=none (R3)"
336
+ H14="$(new_home)"
337
+ mk_cli_declared "$H14" true false
338
+ OUT="$(run_case "$H14" 'export HERDR_ENV=""')"
339
+ check "detected=none" '[ "$(get_val "$OUT" detected)" = "none" ]'
340
+
341
+ echo "Case 15: all markers set but empty -> detected=none (R3)"
342
+ H15="$(new_home)"
343
+ mk_cli_declared "$H15" true true
344
+ OUT="$(run_case "$H15" 'export HERDR_ENV="" HERDR_PANE_ID="" ORCA_PANE_KEY=""')"
345
+ check "detected=none" '[ "$(get_val "$OUT" detected)" = "none" ]'
346
+
347
+ echo "Case 16: markers are read even when the CLI cannot be run (R3)"
348
+ H16="$(new_home)"
349
+ mkdir -p "$H16"
350
+ OUT="$(run_case "$H16" 'export HERDR_ENV=1')"
351
+ check "detected=herdr despite no readable CLI" '[ "$(get_val "$OUT" detected)" = "herdr" ]'
352
+ check "active=herdr" '[ "$(get_val "$OUT" active)" = "herdr" ]'
353
+ WARN16="$(get_val "$OUT" warning)"
354
+ check "warning is the actionable CLI one (restart)" 'printf "%s" "$WARN16" | grep -qi "restart"'
355
+
356
+ # =========================================================================
357
+ # R4 – hosting without a declaration is a warning, not an error
358
+ # =========================================================================
359
+ echo "Case 17: herdr markers, herdr declared false -> active=herdr, warned (R4)"
360
+ H17="$(new_home)"
361
+ mk_cli_declared "$H17" false false
362
+ OUT="$(run_case "$H17" 'export HERDR_ENV=1')"
363
+ check "detected=herdr" '[ "$(get_val "$OUT" detected)" = "herdr" ]'
364
+ check "active=herdr (hosting beats declaring)" '[ "$(get_val "$OUT" active)" = "herdr" ]'
365
+ WARN17="$(get_val "$OUT" warning)"
366
+ check "warning is non-empty" '[ -n "$WARN17" ]'
367
+ check "warning names herdr" 'printf "%s" "$WARN17" | grep -qi "herdr"'
368
+ check "warning mentions declaring" 'printf "%s" "$WARN17" | grep -qi "declare"'
369
+
370
+ echo "Case 18: herdr markers, herdr declaration unset -> active=herdr, warned (R4)"
371
+ H18="$(new_home)"
372
+ mk_cli_declared "$H18" unset unset
373
+ OUT="$(run_case "$H18" 'export HERDR_ENV=1')"
374
+ check "declared_herdr=unset" '[ "$(get_val "$OUT" declared_herdr)" = "unset" ]'
375
+ check "active=herdr" '[ "$(get_val "$OUT" active)" = "herdr" ]'
376
+ WARN18="$(get_val "$OUT" warning)"
377
+ check "warning names herdr" 'printf "%s" "$WARN18" | grep -qi "herdr"'
378
+ check "warning mentions declaring" 'printf "%s" "$WARN18" | grep -qi "declare"'
379
+
380
+ echo "Case 19: orca markers, orca declared false -> active=orca, warned (R4)"
381
+ H19="$(new_home)"
382
+ mk_cli_declared "$H19" true false
383
+ OUT="$(run_case "$H19" 'export ORCA_PANE_KEY=key-1')"
384
+ check "detected=orca" '[ "$(get_val "$OUT" detected)" = "orca" ]'
385
+ check "active=orca" '[ "$(get_val "$OUT" active)" = "orca" ]'
386
+ WARN19="$(get_val "$OUT" warning)"
387
+ check "warning names orca" 'printf "%s" "$WARN19" | grep -qi "orca"'
388
+ check "warning mentions declaring" 'printf "%s" "$WARN19" | grep -qi "declare"'
389
+
390
+ # =========================================================================
391
+ # R5 – no markers: nothing is hosting, and that is not worth a warning
392
+ # =========================================================================
393
+ echo "Case 20: no markers, herdr declared true -> active=none, no warning (R5)"
394
+ H20="$(new_home)"
395
+ mk_cli_declared "$H20" true false
396
+ OUT="$(run_case "$H20" '')"
397
+ check "detected=none" '[ "$(get_val "$OUT" detected)" = "none" ]'
398
+ check "active=none (not herdr)" '[ "$(get_val "$OUT" active)" = "none" ]'
399
+ check "warning is empty (installed is not hosting)" '[ -z "$(get_val "$OUT" warning)" ]'
400
+
401
+ echo "Case 21: no markers, both declared true -> active=none, no warning (R5)"
402
+ H21="$(new_home)"
403
+ mk_cli_declared "$H21" true true
404
+ OUT="$(run_case "$H21" '')"
405
+ check "active=none" '[ "$(get_val "$OUT" active)" = "none" ]'
406
+ check "warning is empty" '[ -z "$(get_val "$OUT" warning)" ]'
407
+
408
+ echo "Case 22: no markers, both declared false -> active=none, no warning (R5)"
409
+ H22="$(new_home)"
410
+ mk_cli_declared "$H22" false false
411
+ OUT="$(run_case "$H22" '')"
412
+ check "active=none" '[ "$(get_val "$OUT" active)" = "none" ]'
413
+ check "warning is empty" '[ -z "$(get_val "$OUT" warning)" ]'
414
+
415
+ # =========================================================================
416
+ # R6 – both marker families at once, in a checkout nobody owns
417
+ #
418
+ # The checkout's owner settles this first: when the checkout belongs to herdr
419
+ # or to orca, that owner is the one hosting the session and there is nothing
420
+ # ambiguous to say. Those cases live under R16.
421
+ #
422
+ # Every case below runs from a plain git checkout, so the owner is none and the
423
+ # older reconciliation still applies: the declared booleans break the tie when
424
+ # exactly one orchestrator is declared true, and when both or neither are,
425
+ # herdr is picked and the reader is told why.
426
+ # =========================================================================
427
+ echo "Case 23: both markers, only herdr declared true -> active=herdr (R6)"
428
+ H23="$(new_home)"
429
+ mk_cli_declared "$H23" true false
430
+ OUT="$(run_case "$H23" 'export HERDR_ENV=1 ORCA_PANE_KEY=key-1')"
431
+ check "detected=herdr" '[ "$(get_val "$OUT" detected)" = "herdr" ]'
432
+ check "active=herdr" '[ "$(get_val "$OUT" active)" = "herdr" ]'
433
+
434
+ echo "Case 24: both markers, only orca declared true -> active=orca (R6)"
435
+ H24="$(new_home)"
436
+ mk_cli_declared "$H24" false true
437
+ OUT="$(run_case "$H24" 'export HERDR_ENV=1 ORCA_PANE_KEY=key-1')"
438
+ check "detected=orca" '[ "$(get_val "$OUT" detected)" = "orca" ]'
439
+ check "active=orca" '[ "$(get_val "$OUT" active)" = "orca" ]'
440
+
441
+ echo "Case 25: both markers, both declared true -> active=herdr, warning names both (R6)"
442
+ H25="$(new_home)"
443
+ mk_cli_declared "$H25" true true
444
+ OUT="$(run_case "$H25" 'export HERDR_ENV=1 ORCA_PANE_KEY=key-1')"
445
+ check "active=herdr (the tie-break default)" '[ "$(get_val "$OUT" active)" = "herdr" ]'
446
+ check "owner=none (a plain git checkout)" '[ "$(get_val "$OUT" owner)" = "none" ]'
447
+ WARN25="$(get_val "$OUT" warning)"
448
+ check "warning is non-empty" '[ -n "$WARN25" ]'
449
+ check "warning names herdr" 'printf "%s" "$WARN25" | grep -qi "herdr"'
450
+ check "warning names orca" 'printf "%s" "$WARN25" | grep -qi "orca"'
451
+
452
+ echo "Case 26: both markers, neither declared true -> active=herdr, warning names both (R6)"
453
+ H26="$(new_home)"
454
+ mk_cli_declared "$H26" false false
455
+ OUT="$(run_case "$H26" 'export HERDR_ENV=1 ORCA_PANE_KEY=key-1')"
456
+ check "active=herdr (the tie-break default)" '[ "$(get_val "$OUT" active)" = "herdr" ]'
457
+ check "owner=none (a plain git checkout)" '[ "$(get_val "$OUT" owner)" = "none" ]'
458
+ WARN26="$(get_val "$OUT" warning)"
459
+ check "warning names herdr" 'printf "%s" "$WARN26" | grep -qi "herdr"'
460
+ check "warning names orca" 'printf "%s" "$WARN26" | grep -qi "orca"'
461
+
462
+ echo "Case 27: both markers, both declarations unset -> active=herdr, warning names both (R6)"
463
+ H27="$(new_home)"
464
+ mk_cli_declared "$H27" unset unset
465
+ OUT="$(run_case "$H27" 'export HERDR_ENV=1 ORCA_PANE_KEY=key-1')"
466
+ check "active=herdr (the tie-break default)" '[ "$(get_val "$OUT" active)" = "herdr" ]'
467
+ WARN27="$(get_val "$OUT" warning)"
468
+ check "warning names herdr" 'printf "%s" "$WARN27" | grep -qi "herdr"'
469
+ check "warning names orca" 'printf "%s" "$WARN27" | grep -qi "orca"'
470
+
471
+ # =========================================================================
472
+ # R7 – purity: no filesystem writes, anywhere
473
+ # =========================================================================
474
+ echo "Case 28: purity – a run leaves the scratch dir untouched (R7)"
475
+ H28="$(new_home)"
476
+ mk_cli_declared "$H28" true false
477
+ SCRATCH="$WORK/scratch28"
478
+ mkdir -p "$SCRATCH"
479
+ cd "$SCRATCH" || exit 1
480
+ BEFORE="$(find . | sort)"
481
+ (
482
+ unset HERDR_ENV HERDR_PANE_ID ORCA_PANE_KEY
483
+ export HOME="$H28"
484
+ export HERDR_ENV=1
485
+ bash "$SUT" >/dev/null 2>/dev/null
486
+ )
487
+ AFTER="$(find . | sort)"
488
+ cd "$WORK" || exit 1
489
+ check "scratch dir listing unchanged by the run" '[ "$BEFORE" = "$AFTER" ]'
490
+
491
+ echo "Case 29: purity – a run touches nothing under HOME and adds no global git config (R7)"
492
+ H29="$(new_home)"
493
+ mk_cli_declared "$H29" true false
494
+ BEFORE_TREE29="$(find "$H29" | sort)"
495
+ BEFORE_GITCFG29="$(HOME="$H29" git config --global --list 2>/dev/null || true)"
496
+ (
497
+ unset HERDR_ENV HERDR_PANE_ID ORCA_PANE_KEY
498
+ export HOME="$H29"
499
+ export HERDR_ENV=1
500
+ bash "$SUT" >/dev/null 2>/dev/null
501
+ )
502
+ AFTER_TREE29="$(find "$H29" | sort)"
503
+ AFTER_GITCFG29="$(HOME="$H29" git config --global --list 2>/dev/null || true)"
504
+ check "whole HOME tree unchanged by the run" '[ "$BEFORE_TREE29" = "$AFTER_TREE29" ]'
505
+ check "no global git config added" '[ "$BEFORE_GITCFG29" = "$AFTER_GITCFG29" ]'
506
+
507
+ # =========================================================================
508
+ # R8 – a malformed CLI answer must not break the six-line contract
509
+ # =========================================================================
510
+ echo "Case 30: CLI exits 0 and prints two lines -> six-line contract still holds (R8)"
511
+ H30="$(new_home)"
512
+ mk_cli "$H30" $'true\nfalse' 0
513
+ OUT="$(run_case "$H30" '')"
514
+ CODE=$?
515
+ LINES="$(printf '%s\n' "$OUT" | wc -l | tr -d ' ')"
516
+ check "exit code is 0" '[ "$CODE" -eq 0 ]'
517
+ check "prints exactly 6 lines" '[ "$LINES" -eq 6 ]'
518
+ check "line 1 is declared_herdr=" 'printf "%s\n" "$OUT" | sed -n "1p" | grep -q "^declared_herdr="'
519
+ check "line 2 is declared_orca=" 'printf "%s\n" "$OUT" | sed -n "2p" | grep -q "^declared_orca="'
520
+ check "line 3 is detected=" 'printf "%s\n" "$OUT" | sed -n "3p" | grep -q "^detected="'
521
+ check "line 4 is active=" 'printf "%s\n" "$OUT" | sed -n "4p" | grep -q "^active="'
522
+ check "line 5 is owner=" 'printf "%s\n" "$OUT" | sed -n "5p" | grep -q "^owner="'
523
+ check "line 6 is warning=" 'printf "%s\n" "$OUT" | sed -n "6p" | grep -q "^warning="'
524
+ check "declared_herdr=unset (a two-line answer is not a boolean)" '[ "$(get_val "$OUT" declared_herdr)" = "unset" ]'
525
+ check "warning is non-empty" '[ -n "$(get_val "$OUT" warning)" ]'
526
+
527
+ # =========================================================================
528
+ # R9 – a directory at the CLI path is not runnable
529
+ # =========================================================================
530
+ echo "Case 31: a directory sits at the CLI path -> warning names the fix (restart) (R9)"
531
+ H31="$(new_home)"
532
+ mkdir -p "$H31/.claude/acstack/bin/acstack"
533
+ OUT="$(run_case "$H31" '')"
534
+ CODE=$?
535
+ check "exit code is 0" '[ "$CODE" -eq 0 ]'
536
+ check "declared_herdr=unset" '[ "$(get_val "$OUT" declared_herdr)" = "unset" ]'
537
+ check "declared_orca=unset" '[ "$(get_val "$OUT" declared_orca)" = "unset" ]'
538
+ WARN31="$(get_val "$OUT" warning)"
539
+ check "warning mentions restarting Claude Code (the fix), not the generic CLI-failed message" 'printf "%s" "$WARN31" | grep -qi "restart"'
540
+
541
+ # =========================================================================
542
+ # R10 – an empty answer with exit 0 is not the same as an unset key
543
+ # =========================================================================
544
+ echo "Case 32: CLI exits 0 and prints nothing -> not an unset key, and not silent (R10)"
545
+ H32="$(new_home)"
546
+ mk_cli "$H32" "" 0
547
+ OUT="$(run_case "$H32" '')"
548
+ CODE=$?
549
+ check "exit code is 0" '[ "$CODE" -eq 0 ]'
550
+ check "declared_herdr=unset" '[ "$(get_val "$OUT" declared_herdr)" = "unset" ]'
551
+ check "declared_orca=unset" '[ "$(get_val "$OUT" declared_orca)" = "unset" ]'
552
+ WARN32="$(get_val "$OUT" warning)"
553
+ check "warning is non-empty (not silent)" '[ -n "$WARN32" ]'
554
+ check "warning says the tool gave an empty answer" 'printf "%s" "$WARN32" | grep -qi "empty"'
555
+ DQ32='""'
556
+ check "warning does not quote an empty value back at the user" '! printf "%s" "$WARN32" | grep -qF "$DQ32"'
557
+
558
+ # =========================================================================
559
+ # R11 – HOME is not set at all
560
+ # =========================================================================
561
+ echo "Case 33: HOME is not set at all -> same contract, same fix as the missing-tool case (R11)"
562
+ OUT="$(
563
+ env -u HOME -u HERDR_ENV -u HERDR_PANE_ID -u ORCA_PANE_KEY \
564
+ bash "$SUT" 2>/dev/null
565
+ )"
566
+ CODE=$?
567
+ LINES="$(printf '%s\n' "$OUT" | wc -l | tr -d ' ')"
568
+ check "exit code is 0" '[ "$CODE" -eq 0 ]'
569
+ check "prints exactly 6 lines" '[ "$LINES" -eq 6 ]'
570
+ check "line 1 is declared_herdr=" 'printf "%s\n" "$OUT" | sed -n "1p" | grep -q "^declared_herdr="'
571
+ check "line 2 is declared_orca=" 'printf "%s\n" "$OUT" | sed -n "2p" | grep -q "^declared_orca="'
572
+ check "line 3 is detected=" 'printf "%s\n" "$OUT" | sed -n "3p" | grep -q "^detected="'
573
+ check "line 4 is active=" 'printf "%s\n" "$OUT" | sed -n "4p" | grep -q "^active="'
574
+ check "line 5 is owner=" 'printf "%s\n" "$OUT" | sed -n "5p" | grep -q "^owner="'
575
+ check "line 6 is warning=" 'printf "%s\n" "$OUT" | sed -n "6p" | grep -q "^warning="'
576
+ check "declared_herdr=unset" '[ "$(get_val "$OUT" declared_herdr)" = "unset" ]'
577
+ check "declared_orca=unset" '[ "$(get_val "$OUT" declared_orca)" = "unset" ]'
578
+ check "owner=none (no HOME, so neither root resolves)" '[ "$(get_val "$OUT" owner)" = "none" ]'
579
+ WARN33="$(get_val "$OUT" warning)"
580
+ check "warning is non-empty" '[ -n "$WARN33" ]'
581
+ check "warning mentions restarting Claude Code (the same fix as the missing-tool case)" 'printf "%s" "$WARN33" | grep -qi "restart"'
582
+
583
+ # =========================================================================
584
+ # R12 – owner: which orchestrator owns the checkout being looked at
585
+ #
586
+ # A checkout is one git worktree directory, and its path root names the
587
+ # owner: a path under $HOME/orca/workspaces/ belongs to orca, a path under
588
+ # herdr's worktree root belongs to herdr, and anything else belongs to plain
589
+ # git and has no owner. The owner comes from the path alone – no markers, no
590
+ # declarations.
591
+ # =========================================================================
592
+ echo "Case 34: inside a herdr checkout, no markers -> owner=herdr (R12)"
593
+ H34="$(new_home)"
594
+ mk_cli_declared "$H34" false false
595
+ WT34="$(mk_herdr_checkout "$H34" repo-a branch-a)"
596
+ OUT="$(run_case_in "$H34" "$WT34" '')"
597
+ CODE=$?
598
+ LINES="$(printf '%s\n' "$OUT" | wc -l | tr -d ' ')"
599
+ check "exit code is 0" '[ "$CODE" -eq 0 ]'
600
+ check "prints exactly 6 lines" '[ "$LINES" -eq 6 ]'
601
+ check "line 5 is owner=" 'printf "%s\n" "$OUT" | sed -n "5p" | grep -q "^owner="'
602
+ check "line 6 is warning=" 'printf "%s\n" "$OUT" | sed -n "6p" | grep -q "^warning="'
603
+ check "owner=herdr" '[ "$(get_val "$OUT" owner)" = "herdr" ]'
604
+ check "detected=none (owning is not hosting)" '[ "$(get_val "$OUT" detected)" = "none" ]'
605
+ check "active=none" '[ "$(get_val "$OUT" active)" = "none" ]'
606
+ check "warning is empty" '[ -z "$(get_val "$OUT" warning)" ]'
607
+
608
+ echo "Case 35: inside an orca workspace, no markers -> owner=orca (R12)"
609
+ H35="$(new_home)"
610
+ mk_cli_declared "$H35" false false
611
+ WS35="$(mk_orca_checkout "$H35" project-a)"
612
+ OUT="$(run_case_in "$H35" "$WS35" '')"
613
+ check "owner=orca" '[ "$(get_val "$OUT" owner)" = "orca" ]'
614
+ check "active=none" '[ "$(get_val "$OUT" active)" = "none" ]'
615
+ check "warning is empty" '[ -z "$(get_val "$OUT" warning)" ]'
616
+
617
+ echo "Case 36: inside a checkout under neither root -> owner=none (R12)"
618
+ H36="$(new_home)"
619
+ mk_cli_declared "$H36" true true
620
+ PLAIN36="$WORK/plain36/repo"
621
+ mkdir -p "$PLAIN36"
622
+ OUT="$(run_case_in "$H36" "$PLAIN36" '')"
623
+ check "owner=none" '[ "$(get_val "$OUT" owner)" = "none" ]'
624
+
625
+ echo "Case 37: the owner is reported even when the CLI cannot be run (R12)"
626
+ H37="$(new_home)"
627
+ mkdir -p "$H37"
628
+ WT37="$(mk_herdr_checkout "$H37" repo-b branch-b)"
629
+ OUT="$(run_case_in "$H37" "$WT37" '')"
630
+ check "owner=herdr despite no readable CLI" '[ "$(get_val "$OUT" owner)" = "herdr" ]'
631
+ check "declared_herdr=unset" '[ "$(get_val "$OUT" declared_herdr)" = "unset" ]'
632
+ check "declared_orca=unset" '[ "$(get_val "$OUT" declared_orca)" = "unset" ]'
633
+ check "warning is still the actionable CLI one" 'printf "%s" "$(get_val "$OUT" warning)" | grep -qi "restart"'
634
+
635
+ echo "Case 38: the declared booleans do not move the owner (R12)"
636
+ H38A="$(new_home)"
637
+ mk_cli_declared "$H38A" true true
638
+ WS38="$(mk_orca_checkout "$H38A" project-b)"
639
+ OUT="$(run_case_in "$H38A" "$WS38" '')"
640
+ check "owner=orca with both declared true" '[ "$(get_val "$OUT" owner)" = "orca" ]'
641
+ H38B="$(new_home)"
642
+ mk_cli_declared "$H38B" false false
643
+ WT38="$(mk_herdr_checkout "$H38B" repo-c branch-c)"
644
+ OUT="$(run_case_in "$H38B" "$WT38" '')"
645
+ check "owner=herdr with both declared false" '[ "$(get_val "$OUT" owner)" = "herdr" ]'
646
+
647
+ # =========================================================================
648
+ # R13 – the optional path argument names the checkout to examine
649
+ # =========================================================================
650
+ echo "Case 39: a path argument replaces the working directory (R13)"
651
+ H39="$(new_home)"
652
+ mk_cli_declared "$H39" false false
653
+ WS39="$(mk_orca_checkout "$H39" project-c)"
654
+ WT39="$(mk_herdr_checkout "$H39" repo-d branch-d)"
655
+ PLAIN39="$WORK/plain39/repo"
656
+ mkdir -p "$PLAIN39"
657
+ OUT="$(run_case_arg "$H39" "$WS39" '')"
658
+ CODE=$?
659
+ LINES="$(printf '%s\n' "$OUT" | wc -l | tr -d ' ')"
660
+ check "exit code is 0 with an argument" '[ "$CODE" -eq 0 ]'
661
+ check "still prints exactly 6 lines" '[ "$LINES" -eq 6 ]'
662
+ check "owner=orca from the named path" '[ "$(get_val "$OUT" owner)" = "orca" ]'
663
+ OUT="$(
664
+ (
665
+ unset HERDR_ENV HERDR_PANE_ID ORCA_PANE_KEY
666
+ export HOME="$H39"
667
+ cd "$WT39" || exit 1
668
+ bash "$SUT" "$PLAIN39" 2>/dev/null
669
+ )
670
+ )"
671
+ check "the argument wins over a herdr-owned cwd" '[ "$(get_val "$OUT" owner)" = "none" ]'
672
+
673
+ # =========================================================================
674
+ # R14 – the path is resolved before it is judged
675
+ #
676
+ # Symlinks and .. segments are resolved first, and the root directory itself
677
+ # is not a checkout: only what sits underneath it counts.
678
+ # =========================================================================
679
+ echo "Case 40: symlinks are resolved before the path is judged (R14)"
680
+ H40="$(new_home)"
681
+ mk_cli_declared "$H40" false false
682
+ WT40="$(mk_herdr_checkout "$H40" repo-e branch-e)"
683
+ SYM40="$WORK/sym40"
684
+ ln -s "$WT40" "$SYM40"
685
+ OUT="$(run_case_in "$H40" "$SYM40" '')"
686
+ check "a symlink into the herdr root -> owner=herdr" '[ "$(get_val "$OUT" owner)" = "herdr" ]'
687
+ OUTSIDE40="$WORK/outside40"
688
+ mkdir -p "$OUTSIDE40"
689
+ mkdir -p "$H40/.herdr/worktrees/repo-f"
690
+ ln -s "$OUTSIDE40" "$H40/.herdr/worktrees/repo-f/branch-f"
691
+ OUT="$(run_case_in "$H40" "$H40/.herdr/worktrees/repo-f/branch-f" '')"
692
+ check "a symlink out of the herdr root -> owner=none" '[ "$(get_val "$OUT" owner)" = "none" ]'
693
+
694
+ echo "Case 41: .. segments are resolved before the path is judged (R14)"
695
+ H41="$(new_home)"
696
+ mk_cli_declared "$H41" false false
697
+ WT41="$(mk_herdr_checkout "$H41" repo-g branch-g)"
698
+ mkdir -p "$H41/elsewhere"
699
+ OUT="$(run_case_arg "$H41" "$H41/.herdr/worktrees/repo-g/branch-g/../branch-g" '')"
700
+ check ".. that lands back inside -> owner=herdr" '[ "$(get_val "$OUT" owner)" = "herdr" ]'
701
+ OUT="$(run_case_arg "$H41" "$H41/.herdr/worktrees/../elsewhere" '')"
702
+ check ".. that climbs out -> owner=none" '[ "$(get_val "$OUT" owner)" = "none" ]'
703
+
704
+ echo "Case 42: the root itself is not a checkout (R14)"
705
+ H42="$(new_home)"
706
+ mk_cli_declared "$H42" false false
707
+ mkdir -p "$H42/.herdr/worktrees" "$H42/orca/workspaces"
708
+ OUT="$(run_case_arg "$H42" "$H42/.herdr/worktrees" '')"
709
+ check "the herdr worktree root itself -> owner=none" '[ "$(get_val "$OUT" owner)" = "none" ]'
710
+ OUT="$(run_case_arg "$H42" "$H42/orca/workspaces" '')"
711
+ check "the orca workspaces root itself -> owner=none" '[ "$(get_val "$OUT" owner)" = "none" ]'
712
+
713
+ echo "Case 43: a sibling whose name merely starts with a root's name is not owned (R14)"
714
+ H43="$(new_home)"
715
+ mk_cli_declared "$H43" false false
716
+ mkdir -p "$H43/.herdr/worktrees-old/repo-h/branch-h"
717
+ mkdir -p "$H43/orca/workspaces-archive/project-d"
718
+ OUT="$(run_case_arg "$H43" "$H43/.herdr/worktrees-old/repo-h/branch-h" '')"
719
+ check ".herdr/worktrees-old is not the herdr root" '[ "$(get_val "$OUT" owner)" = "none" ]'
720
+ OUT="$(run_case_arg "$H43" "$H43/orca/workspaces-archive/project-d" '')"
721
+ check "orca/workspaces-archive is not the orca root" '[ "$(get_val "$OUT" owner)" = "none" ]'
722
+
723
+ # =========================================================================
724
+ # R15 – herdr's config names its own worktree root
725
+ #
726
+ # $HOME/.config/herdr/config.toml may set [worktrees] directory. When it
727
+ # does, that is the herdr root and the default one is not. A leading ~/ is
728
+ # the home directory.
729
+ # =========================================================================
730
+ echo "Case 44: config.toml moves the herdr root, and the default stops counting (R15)"
731
+ H44="$(new_home)"
732
+ mk_cli_declared "$H44" false false
733
+ mk_herdr_config "$H44" '~/custom/wt'
734
+ mkdir -p "$H44/custom/wt/repo-i/branch-i"
735
+ mkdir -p "$H44/.herdr/worktrees/repo-i/branch-i"
736
+ OUT="$(run_case_arg "$H44" "$H44/custom/wt/repo-i/branch-i" '')"
737
+ check "the configured root owns its checkouts" '[ "$(get_val "$OUT" owner)" = "herdr" ]'
738
+ OUT="$(run_case_arg "$H44" "$H44/.herdr/worktrees/repo-i/branch-i" '')"
739
+ check "the default root no longer owns anything" '[ "$(get_val "$OUT" owner)" = "none" ]'
740
+
741
+ echo "Case 45: config.toml may name the root as an absolute path (R15)"
742
+ H45="$(new_home)"
743
+ mk_cli_declared "$H45" false false
744
+ mk_herdr_config "$H45" "$H45/abs/wt"
745
+ mkdir -p "$H45/abs/wt/repo-j/branch-j"
746
+ OUT="$(run_case_arg "$H45" "$H45/abs/wt/repo-j/branch-j" '')"
747
+ check "an absolute configured root owns its checkouts" '[ "$(get_val "$OUT" owner)" = "herdr" ]'
748
+
749
+ echo "Case 46: a config.toml without a [worktrees] section falls back to the default (R15)"
750
+ H46="$(new_home)"
751
+ mk_cli_declared "$H46" false false
752
+ mk_herdr_config_without_worktrees "$H46"
753
+ WT46="$(mk_herdr_checkout "$H46" repo-k branch-k)"
754
+ OUT="$(run_case_arg "$H46" "$WT46" '')"
755
+ check "the default root still owns its checkouts" '[ "$(get_val "$OUT" owner)" = "herdr" ]'
756
+
757
+ # =========================================================================
758
+ # R16 – both marker families at once, in a checkout somebody owns
759
+ #
760
+ # When both families are present the owner settles it outright, ahead of the
761
+ # declared booleans, and there is nothing ambiguous left to warn about.
762
+ # =========================================================================
763
+ echo "Case 47: both markers, orca owns the checkout, both declared true -> active=orca (R16)"
764
+ H47="$(new_home)"
765
+ mk_cli_declared "$H47" true true
766
+ WS47="$(mk_orca_checkout "$H47" project-e)"
767
+ OUT="$(run_case_in "$H47" "$WS47" 'export HERDR_ENV=1 ORCA_PANE_KEY=key-1')"
768
+ check "owner=orca" '[ "$(get_val "$OUT" owner)" = "orca" ]'
769
+ check "detected=orca" '[ "$(get_val "$OUT" detected)" = "orca" ]'
770
+ check "active=orca" '[ "$(get_val "$OUT" active)" = "orca" ]'
771
+ check "warning is empty (nothing ambiguous)" '[ -z "$(get_val "$OUT" warning)" ]'
772
+
773
+ echo "Case 48: both markers, herdr owns the checkout, both declared true -> active=herdr (R16)"
774
+ H48="$(new_home)"
775
+ mk_cli_declared "$H48" true true
776
+ WT48="$(mk_herdr_checkout "$H48" repo-l branch-l)"
777
+ OUT="$(run_case_in "$H48" "$WT48" 'export HERDR_ENV=1 ORCA_PANE_KEY=key-1')"
778
+ check "owner=herdr" '[ "$(get_val "$OUT" owner)" = "herdr" ]'
779
+ check "detected=herdr" '[ "$(get_val "$OUT" detected)" = "herdr" ]'
780
+ check "active=herdr" '[ "$(get_val "$OUT" active)" = "herdr" ]'
781
+ check "warning is empty (nothing ambiguous)" '[ -z "$(get_val "$OUT" warning)" ]'
782
+
783
+ echo "Case 49: both markers, orca owns the checkout, only herdr declared true -> active=orca (R16)"
784
+ H49="$(new_home)"
785
+ mk_cli_declared "$H49" true false
786
+ WS49="$(mk_orca_checkout "$H49" project-f)"
787
+ OUT="$(run_case_in "$H49" "$WS49" 'export HERDR_ENV=1 ORCA_PANE_KEY=key-1')"
788
+ check "active=orca (the owner outranks the declarations)" '[ "$(get_val "$OUT" active)" = "orca" ]'
789
+ WARN49="$(get_val "$OUT" warning)"
790
+ check "no ambiguity warning (it does not name both)" '! { printf "%s" "$WARN49" | grep -qi "herdr" && printf "%s" "$WARN49" | grep -qi "orca"; }'
791
+
792
+ echo "Case 50: both markers, herdr owns the checkout, only orca declared true -> active=herdr (R16)"
793
+ H50="$(new_home)"
794
+ mk_cli_declared "$H50" false true
795
+ WT50="$(mk_herdr_checkout "$H50" repo-m branch-m)"
796
+ OUT="$(run_case_in "$H50" "$WT50" 'export HERDR_ENV=1 ORCA_PANE_KEY=key-1')"
797
+ check "active=herdr (the owner outranks the declarations)" '[ "$(get_val "$OUT" active)" = "herdr" ]'
798
+ WARN50="$(get_val "$OUT" warning)"
799
+ check "no ambiguity warning (it does not name both)" '! { printf "%s" "$WARN50" | grep -qi "herdr" && printf "%s" "$WARN50" | grep -qi "orca"; }'
800
+
801
+ echo "Case 51: both markers, orca owns the checkout, neither declared true -> active=orca (R16)"
802
+ H51="$(new_home)"
803
+ mk_cli_declared "$H51" false false
804
+ WS51="$(mk_orca_checkout "$H51" project-g)"
805
+ OUT="$(run_case_in "$H51" "$WS51" 'export HERDR_ENV=1 ORCA_PANE_KEY=key-1')"
806
+ check "active=orca" '[ "$(get_val "$OUT" active)" = "orca" ]'
807
+ WARN51="$(get_val "$OUT" warning)"
808
+ check "no ambiguity warning (it does not name both)" '! { printf "%s" "$WARN51" | grep -qi "herdr" && printf "%s" "$WARN51" | grep -qi "orca"; }'
809
+
810
+ # =========================================================================
811
+ # R17 – one marker family, or none, is unaffected by the owner
812
+ #
813
+ # The owner only ever settles a tie. A single family of markers is already
814
+ # unambiguous, and no markers at all means nothing is hosting this session,
815
+ # however the checkout is owned.
816
+ # =========================================================================
817
+ echo "Case 52: herdr markers only, inside an orca workspace -> active=herdr (R17)"
818
+ H52="$(new_home)"
819
+ mk_cli_declared "$H52" true false
820
+ WS52="$(mk_orca_checkout "$H52" project-h)"
821
+ OUT="$(run_case_in "$H52" "$WS52" 'export HERDR_ENV=1')"
822
+ check "owner=orca" '[ "$(get_val "$OUT" owner)" = "orca" ]'
823
+ check "detected=herdr" '[ "$(get_val "$OUT" detected)" = "herdr" ]'
824
+ check "active=herdr (markers beat the owner)" '[ "$(get_val "$OUT" active)" = "herdr" ]'
825
+ check "warning is empty" '[ -z "$(get_val "$OUT" warning)" ]'
826
+
827
+ echo "Case 53: orca markers only, inside a herdr checkout -> active=orca (R17)"
828
+ H53="$(new_home)"
829
+ mk_cli_declared "$H53" false true
830
+ WT53="$(mk_herdr_checkout "$H53" repo-n branch-n)"
831
+ OUT="$(run_case_in "$H53" "$WT53" 'export ORCA_PANE_KEY=key-1')"
832
+ check "owner=herdr" '[ "$(get_val "$OUT" owner)" = "herdr" ]'
833
+ check "detected=orca" '[ "$(get_val "$OUT" detected)" = "orca" ]'
834
+ check "active=orca (markers beat the owner)" '[ "$(get_val "$OUT" active)" = "orca" ]'
835
+ check "warning is empty" '[ -z "$(get_val "$OUT" warning)" ]'
836
+
837
+ echo "Case 54: one family undeclared is still warned about, whoever owns the checkout (R17)"
838
+ H54="$(new_home)"
839
+ mk_cli_declared "$H54" false false
840
+ WS54="$(mk_orca_checkout "$H54" project-i)"
841
+ OUT="$(run_case_in "$H54" "$WS54" 'export HERDR_ENV=1')"
842
+ check "active=herdr" '[ "$(get_val "$OUT" active)" = "herdr" ]'
843
+ WARN54="$(get_val "$OUT" warning)"
844
+ check "warning names herdr" 'printf "%s" "$WARN54" | grep -qi "herdr"'
845
+ check "warning mentions declaring" 'printf "%s" "$WARN54" | grep -qi "declare"'
846
+
847
+ echo "Case 55: no markers at all, inside a herdr checkout -> active=none (R17)"
848
+ H55="$(new_home)"
849
+ mk_cli_declared "$H55" true true
850
+ WT55="$(mk_herdr_checkout "$H55" repo-o branch-o)"
851
+ OUT="$(run_case_in "$H55" "$WT55" '')"
852
+ check "owner=herdr" '[ "$(get_val "$OUT" owner)" = "herdr" ]'
853
+ check "detected=none" '[ "$(get_val "$OUT" detected)" = "none" ]'
854
+ check "active=none (owning is not hosting)" '[ "$(get_val "$OUT" active)" = "none" ]'
855
+ check "warning is empty" '[ -z "$(get_val "$OUT" warning)" ]'
856
+
857
+ # =========================================================================
858
+ # R18 – purity: working out the owner changes nothing on disk
859
+ # =========================================================================
860
+ echo "Case 56: a run from inside an owned git checkout writes nothing (R18)"
861
+ H56="$(new_home)"
862
+ mk_cli_declared "$H56" true true
863
+ WT56="$(mk_herdr_checkout "$H56" repo-p branch-p)"
864
+ git init -q "$WT56" 2>/dev/null
865
+ BEFORE_TREE56="$(find "$H56" | sort)"
866
+ BEFORE_GITCFG56="$(HOME="$H56" git config --global --list 2>/dev/null || true)"
867
+ (
868
+ unset HERDR_ENV HERDR_PANE_ID ORCA_PANE_KEY
869
+ export HOME="$H56"
870
+ cd "$WT56" || exit 1
871
+ export HERDR_ENV=1 ORCA_PANE_KEY=key-1
872
+ bash "$SUT" >/dev/null 2>/dev/null
873
+ )
874
+ AFTER_TREE56="$(find "$H56" | sort)"
875
+ AFTER_GITCFG56="$(HOME="$H56" git config --global --list 2>/dev/null || true)"
876
+ check "the whole HOME tree is unchanged" '[ "$BEFORE_TREE56" = "$AFTER_TREE56" ]'
877
+ check "no global git config added" '[ "$BEFORE_GITCFG56" = "$AFTER_GITCFG56" ]'
878
+
879
+ echo "Case 57: a run in a directory that is not a git repo creates no repository (R18)"
880
+ H57="$(new_home)"
881
+ mk_cli_declared "$H57" true false
882
+ WT57="$(mk_herdr_checkout "$H57" repo-q branch-q)"
883
+ (
884
+ unset HERDR_ENV HERDR_PANE_ID ORCA_PANE_KEY
885
+ export HOME="$H57"
886
+ cd "$WT57" || exit 1
887
+ bash "$SUT" >/dev/null 2>/dev/null
888
+ )
889
+ check "no .git directory was created" '[ ! -e "$WT57/.git" ]'
890
+
891
+ echo ""
892
+ echo "----------------------------------------"
893
+ printf 'Result: %d passed, %d failed\n' "$pass" "$fail"
894
+ [ "$fail" -eq 0 ] || exit 1