acstack 0.0.0-stage → 0.47.1

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 +334 -0
  27. package/assets/terminal-workspace/setup.sh +4404 -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 +1780 -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,512 @@
1
+ #!/usr/bin/env bash
2
+ # ACStack host setup: install Orca as a headless server for this user.
3
+ #
4
+ # Orca is an agent integrated development environment. This script installs it
5
+ # as a background service that serves its interface over the network, so a
6
+ # machine with no screen - a development virtual machine, say - can host it
7
+ # while you pair a browser or a phone with it from somewhere else.
8
+ #
9
+ # Everything it touches lives under $HOME and no step needs root:
10
+ #
11
+ # ~/.local/opt/orca/orca-linux.AppImage the binary
12
+ # ~/.local/bin/orca-ide, ~/.local/bin/orca symlinks onto PATH
13
+ # ~/.config/systemd/user/orca.service the systemd user unit
14
+ #
15
+ # There are two modes and no default. --plan prints the five steps with the
16
+ # status each one would have and writes nothing at all; --apply does the work.
17
+ # Every step is idempotent, so a second --apply reports every step as skipped
18
+ # and leaves the machine byte for byte as it was. Plan mode cannot write by
19
+ # accident, because every write in this script lives inside a step's doer
20
+ # function and a doer is only ever called on the apply path.
21
+ #
22
+ # A step that cannot finish says so on stderr and stops the run non-zero. It
23
+ # never carries on to the next step, because a half-configured service that
24
+ # reports success is worse than one that is missing.
25
+ #
26
+ # Every tool it needs is looked up on PATH and every path it writes derives from
27
+ # $HOME. That is what makes it testable: tests/test-host-install.sh runs it under
28
+ # `env -i` with a PATH of fake executables and a temporary HOME, so the result is
29
+ # entirely determined by what the test wired up. Reaching past PATH or $HOME to a
30
+ # real location would defeat that, so don't.
31
+
32
+ set -u
33
+
34
+ PROG="install-orca.sh"
35
+ TOTAL=5
36
+
37
+ # --- defaults --------------------------------------------------------------
38
+
39
+ # Upstream's own documented download link. The asset name carries no version, so
40
+ # the `latest` alias resolves to whatever the current release is.
41
+ DEFAULT_APPIMAGE_URL="https://github.com/stablyai/orca/releases/latest/download/orca-linux.AppImage"
42
+ DEFAULT_PORT="6768"
43
+ # Where the kernel release string lives on Linux. An option overrides it, which
44
+ # is what lets a test hand the Windows Subsystem for Linux check its own file.
45
+ DEFAULT_OSRELEASE_FILE="/proc/sys/kernel/osrelease"
46
+
47
+ usage() {
48
+ cat <<'USAGE'
49
+ install-orca.sh - install Orca as a headless systemd user service.
50
+
51
+ Usage:
52
+ install-orca.sh --plan [options] Print the steps and their status. Writes nothing.
53
+ install-orca.sh --apply [options] Do the work. Every step is idempotent.
54
+ install-orca.sh --help Print this help and exit.
55
+
56
+ Options:
57
+ --appimage-path PATH Install this local AppImage instead of downloading one.
58
+ --appimage-url URL Download the AppImage from here. Defaults to the latest
59
+ orca-linux.AppImage on the upstream releases page.
60
+ --port N Port the server listens on. Default: 6768.
61
+ --pairing-address ADDR Address to advertise in the pairing URL, a Tailscale
62
+ address for example. Left out of the unit when unset.
63
+ --mobile-pairing Also advertise the server to the Orca mobile app.
64
+ --osrelease-file PATH Kernel release file the Windows Subsystem for Linux
65
+ check reads. Default: /proc/sys/kernel/osrelease.
66
+
67
+ Exactly one of --plan and --apply is required; neither is the default, because
68
+ this script changes a machine and guessing which way is not a kindness.
69
+
70
+ Everything is installed under your home directory and no step needs root:
71
+ ~/.local/opt/orca/orca-linux.AppImage
72
+ ~/.local/bin/orca-ide, ~/.local/bin/orca
73
+ ~/.config/systemd/user/orca.service
74
+
75
+ Exit codes: 0 all good, 1 a step failed, 3 this machine is not Linux,
76
+ 64 the command line was wrong.
77
+ USAGE
78
+ }
79
+
80
+ # --- small helpers ---------------------------------------------------------
81
+
82
+ err() { printf '%s: %s\n' "$PROG" "$1" >&2; }
83
+
84
+ # A step could not finish. Say so and stop: nothing further is attempted.
85
+ fail() { err "$1"; exit 1; }
86
+
87
+ die_usage() {
88
+ err "$1"
89
+ err "run with --help for usage."
90
+ exit 64
91
+ }
92
+
93
+ # $1 wrapped in single quotes, safe to paste inside a /bin/sh command string.
94
+ # An embedded single quote is closed, escaped and reopened, which is the only
95
+ # form /bin/sh accepts. Home directories with a space in them are ordinary, so
96
+ # the AppImage path genuinely needs this.
97
+ shq() {
98
+ local s="$1"
99
+ printf "'%s'" "${s//\'/\'\\\'\'}"
100
+ }
101
+
102
+ # --- argument parsing ------------------------------------------------------
103
+ #
104
+ # Parsed in full before anything else happens, so --help and a bad command line
105
+ # cost nothing: no platform detection, no systemctl call, no file touched.
106
+
107
+ MODE=""
108
+ APPIMAGE_URL="$DEFAULT_APPIMAGE_URL"
109
+ APPIMAGE_PATH=""
110
+ PORT="$DEFAULT_PORT"
111
+ PAIRING_ADDRESS=""
112
+ MOBILE_PAIRING=0
113
+ OSRELEASE_FILE="$DEFAULT_OSRELEASE_FILE"
114
+
115
+ # $1=option name, $2=how many arguments are left including the option itself.
116
+ need_value() {
117
+ [ "$2" -ge 2 ] || die_usage "$1 needs a value."
118
+ }
119
+
120
+ set_mode() {
121
+ [ -z "$MODE" ] || die_usage "choose exactly one of --plan and --apply."
122
+ MODE="$1"
123
+ }
124
+
125
+ while [ "$#" -gt 0 ]; do
126
+ case "$1" in
127
+ --plan) set_mode "plan" ;;
128
+ --apply) set_mode "apply" ;;
129
+ --appimage-path) need_value "$1" "$#"; APPIMAGE_PATH="$2"; shift ;;
130
+ --appimage-url) need_value "$1" "$#"; APPIMAGE_URL="$2"; shift ;;
131
+ --port) need_value "$1" "$#"; PORT="$2"; shift ;;
132
+ --pairing-address) need_value "$1" "$#"; PAIRING_ADDRESS="$2"; shift ;;
133
+ --mobile-pairing) MOBILE_PAIRING=1 ;;
134
+ --osrelease-file) need_value "$1" "$#"; OSRELEASE_FILE="$2"; shift ;;
135
+ --help|-h) usage; exit 0 ;;
136
+ *) die_usage "unknown argument: $1" ;;
137
+ esac
138
+ shift
139
+ done
140
+
141
+ [ -n "$MODE" ] || die_usage "one of --plan and --apply is required."
142
+
143
+ case "$PORT" in
144
+ ''|*[!0-9]*) die_usage "--port takes a number, not: $PORT" ;;
145
+ esac
146
+ if [ "$PORT" -lt 1 ] || [ "$PORT" -gt 65535 ]; then
147
+ die_usage "--port must be between 1 and 65535, not: $PORT"
148
+ fi
149
+
150
+ # The pairing address is written into the unit unquoted, so that the unit reads
151
+ # the way the upstream examples do. Keep it to the characters a host name or an
152
+ # IP address can hold, and nothing that a shell would act on can get in.
153
+ case "$PAIRING_ADDRESS" in
154
+ '') ;;
155
+ *[!A-Za-z0-9._:-]*) die_usage "--pairing-address may hold only letters, digits, dot, colon, hyphen and underscore, not: $PAIRING_ADDRESS" ;;
156
+ esac
157
+
158
+ [ -n "${HOME:-}" ] || fail "HOME is not set, so there is nowhere to install to."
159
+
160
+ # --- platform gate ---------------------------------------------------------
161
+ #
162
+ # systemd user units and AppImages are Linux things. Anywhere else this script
163
+ # would produce a confident-looking mess, so it stops before touching anything.
164
+
165
+ PLATFORM="$(uname -s 2>/dev/null || true)"
166
+ if [ "$PLATFORM" != "Linux" ]; then
167
+ printf '%s: this installer supports Linux only. This machine reports: %s\n' \
168
+ "$PROG" "${PLATFORM:-unknown}"
169
+ printf '%s: nothing has been changed.\n' "$PROG"
170
+ exit 3
171
+ fi
172
+
173
+ # --- the Windows Subsystem for Linux is not supported ----------------------
174
+ #
175
+ # The Windows Subsystem for Linux is a Linux environment running on Windows. It
176
+ # answers `Linux` to `uname -s`, so the gate above lets it straight through, and
177
+ # neither a systemd user unit nor an AppImage behaves there the way this install
178
+ # assumes. Two markers give it away and either one on its own is enough.
179
+
180
+ WSL_EVIDENCE=""
181
+ if [ -n "${WSL_DISTRO_NAME:-}" ]; then
182
+ WSL_EVIDENCE="the WSL_DISTRO_NAME environment variable is set to: $WSL_DISTRO_NAME"
183
+ elif [ -f "$OSRELEASE_FILE" ] && [ -r "$OSRELEASE_FILE" ] &&
184
+ grep -qi microsoft "$OSRELEASE_FILE" 2>/dev/null; then
185
+ # Matched without regard to case: the kernel release reads `microsoft` on WSL2
186
+ # and `Microsoft` on WSL1.
187
+ WSL_EVIDENCE="the kernel release in $OSRELEASE_FILE names microsoft: $(head -n 1 "$OSRELEASE_FILE" 2>/dev/null)"
188
+ fi
189
+
190
+ # An osrelease file that is not there says nothing about the environment, so a
191
+ # machine without one carries on rather than being refused on a missing file.
192
+ if [ -n "$WSL_EVIDENCE" ]; then
193
+ printf '%s: this is the Windows Subsystem for Linux (WSL), which is not supported.\n' "$PROG"
194
+ printf '%s: %s\n' "$PROG" "$WSL_EVIDENCE"
195
+ printf '%s: nothing has been changed.\n' "$PROG"
196
+ exit 3
197
+ fi
198
+
199
+ # --- the paths this script owns --------------------------------------------
200
+
201
+ APPIMAGE_DIR="$HOME/.local/opt/orca"
202
+ APPIMAGE="$APPIMAGE_DIR/orca-linux.AppImage"
203
+ BIN_DIR="$HOME/.local/bin"
204
+ LINK_IDE="$BIN_DIR/orca-ide"
205
+ LINK_ORCA="$BIN_DIR/orca"
206
+ UNIT_DIR="$HOME/.config/systemd/user"
207
+ UNIT_NAME="orca.service"
208
+ UNIT="$UNIT_DIR/$UNIT_NAME"
209
+ JOURNAL_CMD="journalctl --user -u $UNIT_NAME -n 100 --no-pager"
210
+
211
+ HAVE_SYSTEMCTL=0
212
+ command -v systemctl >/dev/null 2>&1 && HAVE_SYSTEMCTL=1
213
+
214
+ if [ "$MODE" = "apply" ] && [ "$HAVE_SYSTEMCTL" -eq 0 ]; then
215
+ fail "systemctl is not on PATH, so the user service cannot be installed. Nothing has been changed."
216
+ fi
217
+
218
+ # --- the unit file ---------------------------------------------------------
219
+
220
+ # The server invocation, as one /bin/sh command string.
221
+ ORCA_CMD="$(shq "$APPIMAGE") serve --port $PORT"
222
+ [ -n "$PAIRING_ADDRESS" ] && ORCA_CMD="$ORCA_CMD --pairing-address $PAIRING_ADDRESS"
223
+ [ "$MOBILE_PAIRING" -eq 1 ] && ORCA_CMD="$ORCA_CMD --mobile-pairing"
224
+
225
+ # Built once, up front: step 3 compares it against what is on disk, and step 4
226
+ # needs to know whether step 3 will rewrite the file.
227
+ UNIT_TEXT="$(cat <<UNIT
228
+ [Unit]
229
+ Description=Orca headless server
230
+ Documentation=https://github.com/stablyai/orca
231
+ After=network-online.target
232
+ Wants=network-online.target
233
+
234
+ [Service]
235
+ Type=simple
236
+ # Orca prints its readiness block - the pairing URL among it - only when it
237
+ # believes it is talking to a terminal. Started plainly, the server runs fine
238
+ # but the journal holds nothing, and there is then no way to retrieve the
239
+ # pairing URL. /usr/bin/script gives the server a pseudo-terminal and copies
240
+ # everything written to it onto stdout, which systemd captures into the journal.
241
+ # -q drops script's own start and done banners, -e makes script exit with the
242
+ # server's status so Restart= behaves, -c is the command to run, and the
243
+ # trailing /dev/null is where script's transcript file goes, because we want the
244
+ # journal copy and not a second one on disk.
245
+ ExecStart=/usr/bin/script -qec "$ORCA_CMD" /dev/null
246
+ Environment=ORCA_TELEMETRY_DISABLED=1
247
+ Restart=on-failure
248
+ RestartSec=5
249
+
250
+ [Install]
251
+ WantedBy=default.target
252
+ UNIT
253
+ )"
254
+
255
+ # --- step state ------------------------------------------------------------
256
+ #
257
+ # Every step's status is worked out here, before any of them runs. Two reasons:
258
+ # plan mode and apply mode then report from exactly the same reasoning, and
259
+ # step 4 can see whether step 3 is about to rewrite the unit.
260
+
261
+ # Step 1: the AppImage. An existing file counts only when it is non-empty and
262
+ # executable - a truncated download or a lost permission bit is not an install.
263
+ STEP1_NEEDED=1
264
+ STEP1_REASON=""
265
+ if [ -f "$APPIMAGE" ] && [ -s "$APPIMAGE" ] && [ -x "$APPIMAGE" ]; then
266
+ STEP1_NEEDED=0
267
+ STEP1_REASON="already present and executable"
268
+ fi
269
+
270
+ # Step 2: the two symlinks. Anything that is not a symlink pointing at the
271
+ # AppImage - a stale link, a copied binary, nothing at all - is replaced.
272
+ points_at_appimage() {
273
+ [ -L "$1" ] && [ "$(readlink "$1")" = "$APPIMAGE" ]
274
+ }
275
+ STEP2_NEEDED=1
276
+ STEP2_REASON=""
277
+ if points_at_appimage "$LINK_IDE" && points_at_appimage "$LINK_ORCA"; then
278
+ STEP2_NEEDED=0
279
+ STEP2_REASON="both links already point at the AppImage"
280
+ fi
281
+
282
+ # Step 3: the unit file, compared whole. Any difference at all - a changed port,
283
+ # an added pairing address, an edit by hand - is a rewrite.
284
+ STEP3_NEEDED=1
285
+ STEP3_REASON=""
286
+ if [ -f "$UNIT" ] && [ "$(cat "$UNIT" 2>/dev/null)" = "$UNIT_TEXT" ]; then
287
+ STEP3_NEEDED=0
288
+ STEP3_REASON="already exactly this unit"
289
+ fi
290
+
291
+ # Step 4: reload, enable, start. The unit's current state comes from systemd
292
+ # itself; these are queries and change nothing.
293
+ UNIT_ENABLED=0
294
+ UNIT_ACTIVE=0
295
+ if [ "$HAVE_SYSTEMCTL" -eq 1 ]; then
296
+ systemctl --user is-enabled "$UNIT_NAME" >/dev/null 2>&1 && UNIT_ENABLED=1
297
+ systemctl --user is-active "$UNIT_NAME" >/dev/null 2>&1 && UNIT_ACTIVE=1
298
+ fi
299
+
300
+ # It skips only when the unit is already enabled and already running AND step 3
301
+ # is not rewriting the unit in this same run. When step 3 does rewrite it, the
302
+ # running server is still on the old unit until systemd is told otherwise.
303
+ STEP4_NEEDED=1
304
+ STEP4_REASON=""
305
+ if [ "$UNIT_ENABLED" -eq 1 ] && [ "$UNIT_ACTIVE" -eq 1 ] && [ "$STEP3_NEEDED" -eq 0 ]; then
306
+ STEP4_NEEDED=0
307
+ STEP4_REASON="already enabled and running on this unit"
308
+ fi
309
+
310
+ # Step 5: the pairing URL - the link a client uses to connect to this server.
311
+ # Orca prints it once, when the server starts, so it is worth reading back
312
+ # exactly when step 4 has just started or restarted the server. An untouched
313
+ # server's pairing URL has not changed, so there is nothing to re-read.
314
+ #
315
+ # This step only ever reads. It is a convenience on the end of an install that
316
+ # has already succeeded, so nothing it runs into can fail the run: a missing
317
+ # journalctl or a journal with no readiness line in it yet reports as skipped,
318
+ # says why, and prints the command to run by hand.
319
+ STEP5_NEEDED="$STEP4_NEEDED"
320
+ STEP5_REASON="the server was left running untouched, so its pairing URL has not changed"
321
+ PAIRING_URL=""
322
+ SHOW_JOURNAL_HINT=0
323
+
324
+ # --- the doers -------------------------------------------------------------
325
+ #
326
+ # Every write in this script is in one of these three functions, and they are
327
+ # only ever called from the apply path. That is what makes plan mode inert.
328
+
329
+ do_step1() {
330
+ mkdir -p "$APPIMAGE_DIR" || { err "could not create $APPIMAGE_DIR"; return 1; }
331
+
332
+ local tmp
333
+ # Staged next to its destination, so the move into place is atomic and a
334
+ # failed download never leaves a half-written AppImage where a working one
335
+ # is expected.
336
+ tmp="$(mktemp "$APPIMAGE_DIR/.orca-install.XXXXXX")" || {
337
+ err "could not create a temporary file in $APPIMAGE_DIR"; return 1; }
338
+
339
+ if [ -n "$APPIMAGE_PATH" ]; then
340
+ if [ ! -f "$APPIMAGE_PATH" ]; then
341
+ rm -f "$tmp"; err "no AppImage at $APPIMAGE_PATH"; return 1
342
+ fi
343
+ if [ ! -s "$APPIMAGE_PATH" ]; then
344
+ rm -f "$tmp"; err "the AppImage at $APPIMAGE_PATH is empty"; return 1
345
+ fi
346
+ if ! cp -f "$APPIMAGE_PATH" "$tmp"; then
347
+ rm -f "$tmp"; err "could not copy $APPIMAGE_PATH into $APPIMAGE_DIR"; return 1
348
+ fi
349
+ else
350
+ if ! command -v curl >/dev/null 2>&1; then
351
+ rm -f "$tmp"
352
+ err "curl is not on PATH, so the AppImage cannot be downloaded. Pass --appimage-path to install a local copy instead."
353
+ return 1
354
+ fi
355
+ if ! curl -fL --retry 2 --connect-timeout 20 --output "$tmp" "$APPIMAGE_URL"; then
356
+ rm -f "$tmp"; err "failed to download the Orca AppImage from $APPIMAGE_URL"; return 1
357
+ fi
358
+ if [ ! -s "$tmp" ]; then
359
+ rm -f "$tmp"; err "the download from $APPIMAGE_URL produced an empty file"; return 1
360
+ fi
361
+ fi
362
+
363
+ if ! chmod +x "$tmp"; then
364
+ rm -f "$tmp"; err "could not make $tmp executable"; return 1
365
+ fi
366
+ if ! mv -f "$tmp" "$APPIMAGE"; then
367
+ rm -f "$tmp"; err "could not move the AppImage into $APPIMAGE"; return 1
368
+ fi
369
+ }
370
+
371
+ do_step2() {
372
+ mkdir -p "$BIN_DIR" || { err "could not create $BIN_DIR"; return 1; }
373
+ # -n so that an existing symlink is replaced rather than followed, which is
374
+ # what makes a link pointing at a directory safe to overwrite.
375
+ ln -sfn "$APPIMAGE" "$LINK_IDE" || { err "could not link $LINK_IDE"; return 1; }
376
+ ln -sfn "$APPIMAGE" "$LINK_ORCA" || { err "could not link $LINK_ORCA"; return 1; }
377
+ }
378
+
379
+ do_step3() {
380
+ mkdir -p "$UNIT_DIR" || { err "could not create $UNIT_DIR"; return 1; }
381
+ printf '%s\n' "$UNIT_TEXT" > "$UNIT" || { err "could not write $UNIT"; return 1; }
382
+ }
383
+
384
+ do_step4() {
385
+ systemctl --user daemon-reload || { err "systemctl --user daemon-reload failed"; return 1; }
386
+ if [ "$UNIT_ENABLED" -eq 0 ] || [ "$UNIT_ACTIVE" -eq 0 ]; then
387
+ # Not enabled, or enabled but not running: --now does both jobs at once.
388
+ systemctl --user enable --now "$UNIT_NAME" || {
389
+ err "systemctl --user enable --now $UNIT_NAME failed"; return 1; }
390
+ else
391
+ # Already enabled and running, and we are here only because the unit was
392
+ # rewritten: pick the new unit up without touching the enablement.
393
+ systemctl --user restart "$UNIT_NAME" || {
394
+ err "systemctl --user restart $UNIT_NAME failed"; return 1; }
395
+ fi
396
+ }
397
+
398
+ # Orca announces itself with one line of JSON carrying an "orca_server_ready"
399
+ # event and a "pairing" value. jq is one of the things a bare machine may not
400
+ # have, and needing a JSON tool to finish installing the tools would be a poor
401
+ # joke, so the value is cut out with sed. The last matching line wins: the
402
+ # journal is chronological, so the newest readiness line is the one belonging to
403
+ # the server running now.
404
+ extract_pairing_url() { # $1 = journal text
405
+ printf '%s\n' "$1" \
406
+ | grep -F 'orca_server_ready' \
407
+ | tail -n 1 \
408
+ | sed -n 's/.*"pairing"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p'
409
+ }
410
+
411
+ # Reads the journal and sets PAIRING_URL. On any outcome but success it turns
412
+ # step 5 into a skip that explains itself, and never returns non-zero: see the
413
+ # note on STEP5_NEEDED above.
414
+ read_pairing_url() {
415
+ if ! command -v journalctl >/dev/null 2>&1; then
416
+ STEP5_NEEDED=0
417
+ STEP5_REASON="journalctl is not on PATH, so the pairing URL cannot be read here"
418
+ SHOW_JOURNAL_HINT=1
419
+ return 0
420
+ fi
421
+
422
+ # The server has only just been told to start, and Orca takes a moment to get
423
+ # as far as printing its readiness line. Look again a few times before giving
424
+ # up. `sleep` is a command like any other and not on every PATH; where it is
425
+ # missing we settle for a single look rather than spinning.
426
+ local attempts=1 n=1 journal=""
427
+ command -v sleep >/dev/null 2>&1 && attempts=10
428
+
429
+ while [ "$n" -le "$attempts" ]; do
430
+ journal="$(journalctl --user -u "$UNIT_NAME" -n 100 --no-pager 2>/dev/null || true)"
431
+ PAIRING_URL="$(extract_pairing_url "$journal")"
432
+ [ -n "$PAIRING_URL" ] && return 0
433
+ n=$((n + 1))
434
+ [ "$n" -le "$attempts" ] && sleep 1
435
+ done
436
+
437
+ STEP5_NEEDED=0
438
+ STEP5_REASON="journalctl showed no readiness line for $UNIT_NAME yet"
439
+ SHOW_JOURNAL_HINT=1
440
+ return 0
441
+ }
442
+
443
+ # --- reporting -------------------------------------------------------------
444
+
445
+ # One line per step: number, what it is, and a bracketed status. Plan mode says
446
+ # what would happen ([todo] / [skip: ...]), apply mode says what did
447
+ # ([done] / [skipped: ...]).
448
+ step_line() { # $1=number $2=description $3=needed $4=reason
449
+ local status
450
+ if [ "$3" -eq 1 ]; then
451
+ if [ "$MODE" = "plan" ]; then status="[todo]"; else status="[done]"; fi
452
+ else
453
+ if [ "$MODE" = "plan" ]; then status="[skip: $4]"; else status="[skipped: $4]"; fi
454
+ fi
455
+ printf '[%s/%s] %s %s\n' "$1" "$TOTAL" "$2" "$status"
456
+ }
457
+
458
+ # Does the step when there is work and we are applying, then reports it. The
459
+ # work comes first so that a failure never gets to print [done].
460
+ run_step() { # $1=number $2=description $3=needed $4=reason $5=doer
461
+ if [ "$3" -eq 1 ] && [ "$MODE" = "apply" ]; then
462
+ "$5" || fail "step $1 failed: $2"
463
+ fi
464
+ step_line "$1" "$2" "$3" "$4"
465
+ }
466
+
467
+ print_notes() {
468
+ cat <<'NOTES'
469
+
470
+ Notes:
471
+ Orca rewrites ~/.claude/settings.json on its first run, to add its own hooks,
472
+ and keeps the file it found as ~/.claude/settings.json.bak. That is Orca's
473
+ own doing and this script never touches either file, but it is worth knowing
474
+ before you start the server.
475
+ The pairing URL is printed once, when the server starts. The journalctl
476
+ command in step 5 is how you read it back afterwards.
477
+ Nothing here needs root: every path is under your home directory.
478
+ NOTES
479
+ }
480
+
481
+ # --- run -------------------------------------------------------------------
482
+
483
+ if [ "$MODE" = "plan" ]; then
484
+ printf '%s: plan only. Nothing on this machine is changed.\n\n' "$PROG"
485
+ else
486
+ printf '%s: applying.\n\n' "$PROG"
487
+ fi
488
+
489
+ run_step 1 "AppImage at $APPIMAGE" \
490
+ "$STEP1_NEEDED" "$STEP1_REASON" do_step1
491
+ run_step 2 "symlinks orca-ide and orca in $BIN_DIR" \
492
+ "$STEP2_NEEDED" "$STEP2_REASON" do_step2
493
+ run_step 3 "systemd user unit $UNIT" \
494
+ "$STEP3_NEEDED" "$STEP3_REASON" do_step3
495
+ run_step 4 "systemctl --user daemon-reload, then enable and start $UNIT_NAME" \
496
+ "$STEP4_NEEDED" "$STEP4_REASON" do_step4
497
+ # Step 5 is reported by hand rather than through run_step, because reading the
498
+ # journal can turn the step into a skip and run_step's status is fixed before
499
+ # its doer runs. Plan mode never gets here, so it never calls journalctl.
500
+ if [ "$MODE" = "apply" ] && [ "$STEP5_NEEDED" -eq 1 ]; then
501
+ read_pairing_url
502
+ fi
503
+ step_line 5 "pairing URL: $JOURNAL_CMD" "$STEP5_NEEDED" "$STEP5_REASON"
504
+ if [ -n "$PAIRING_URL" ]; then
505
+ printf ' %s\n' "$PAIRING_URL"
506
+ fi
507
+ if [ "$SHOW_JOURNAL_HINT" -eq 1 ]; then
508
+ printf ' read it yourself, once the server has settled, with:\n'
509
+ printf ' %s\n' "$JOURNAL_CMD"
510
+ fi
511
+
512
+ print_notes
@@ -0,0 +1,97 @@
1
+ # Host notes
2
+
3
+ Read this file when a `config set` call fails, or when the user asks one of three things:
4
+
5
+ - Where the answers live
6
+ - Why the skill finds the plugin root through a symlink
7
+ - What a declaration means
8
+
9
+ ## Why this is per machine, not per project
10
+
11
+ The same repository gets opened on several machines, and those machines differ.
12
+ A laptop with a screen can launch a visible browser; a headless build server
13
+ cannot. One machine has herdr installed, another has Orca, another has neither.
14
+
15
+ None of that is a property of the project, so none of it belongs in the
16
+ project's config file.
17
+
18
+ The answers therefore go to the global config of the ACStack command-line
19
+ interface. That is a JSON file at `~/.config/acstack/config.json`, or under
20
+ `$XDG_CONFIG_HOME` when you set that environment variable. Run
21
+ `~/.claude/acstack/bin/acstack config path` to print the real location.
22
+
23
+ They do not go in `acstack/project.yaml`.
24
+
25
+ Project concerns stay in `project.yaml`. In particular
26
+ `frontend.browser.mode` stays there as the *project's preference*, while
27
+ `host.browser.*` says what this *machine* can actually do. The two are
28
+ different questions, and they live in different files.
29
+
30
+ ## Why Step 1 resolves the plugin root from a symlink
31
+
32
+ The path resolution looks odd, so here is why. The plugin's own files live in a
33
+ versioned cache directory. Its path changes on every release, so nobody can
34
+ hardcode it.
35
+
36
+ The one invariant path is `~/.claude/acstack/bin/acstack`, a symlink to the CLI
37
+ inside the current cache. The SessionStart hook re-points that symlink at the
38
+ start of every agent session.
39
+
40
+ The command in Step 1 of `SKILL.md` therefore derives the plugin root from the
41
+ symlink rather than from a written-down path.
42
+
43
+ ## When a `config set` call fails (Step 4)
44
+
45
+ - **A rejected value.** The message names the allowed values for a key the tool
46
+ knows. Re-ask rather than guessing at a different spelling – enum values are
47
+ matched case-sensitively, so it is `stagewise`, never `Stagewise`.
48
+
49
+ - **An unknown key.** The message reads `Unknown config key "<key>"` and lists
50
+ the host keys the tool does know. That is version skew: the command line tool
51
+ in this plugin cache is older than these skills. `host.orchestrators.herdr`,
52
+ `host.orchestrators.orca` and `host.orca.topology` are the newest axes, so
53
+ they are the ones an old cached tool rejects.
54
+
55
+ - **Recovering from version skew.** Do not re-ask – the answer is not the
56
+ problem, and re-asking a boolean axis only loops. Tell the user to restart
57
+ their agent session, so the SessionStart hook re-points
58
+ `~/.claude/acstack/bin/acstack` at the current cache.
59
+
60
+ Skip the axis and carry on. Do not report it to the user as their mistake.
61
+
62
+ ## Notes
63
+
64
+ **Declared means installed.** Each of the two declarations says that this
65
+ machine has that orchestrator installed. They are independent, so both can be
66
+ true at once. Neither says that the orchestrator is hosting the current
67
+ session.
68
+
69
+ At worktree time, `/acstack:new-worktree` and `/acstack:teardown-worktree` run
70
+ the same detector. It reads the environment markers an orchestrator sets in the
71
+ terminals it opens, and those markers name the orchestrator hosting this
72
+ session. The markers are `HERDR_ENV` or `HERDR_PANE_ID` for herdr, and
73
+ `ORCA_PANE_KEY` for Orca.
74
+
75
+ When no marker holds a value, those skills use plain
76
+ git worktrees and say so. An environment variable for an orchestrator that was
77
+ never declared is worth a warning, not a refusal.
78
+
79
+ **The host declares, the project prefers.** `host.browser.*` says what this
80
+ machine can do. `frontend.browser.mode` in `acstack/project.yaml` says what the
81
+ project would like.
82
+
83
+ Today the only thing that compares the two is the ACStack command-line
84
+ interface's own health check, `~/.claude/acstack/bin/acstack config check`.
85
+
86
+ It passes when the host declares the project's preferred mode available. It
87
+ passes with a note when the host does not. The note names the first mode the
88
+ host does declare, in the order remote, headless, local, as the one that would
89
+ stand in.
90
+
91
+ It fails when the host declares none of the three. That is a report about the
92
+ setup, not a choice made on the verifier's behalf.
93
+
94
+ Nothing under `agents/` or `skills/browser-verify/` reads `host.browser.*` yet,
95
+ so the frontend verifier still takes `frontend.browser.mode` at its word.
96
+ Teaching it to resolve the project's preference against the host, in the order
97
+ the check already uses, is still to come.