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,467 @@
1
+ #!/usr/bin/env bash
2
+ # ACStack host setup: tell herdr where to put its worktrees.
3
+ #
4
+ # herdr is the terminal workspace this project drives agents from. It makes a
5
+ # git worktree per unit of work, and where those worktrees land is one setting
6
+ # in ~/.config/herdr/config.toml. This script writes that one setting and
7
+ # nothing else, inside a fenced block it owns:
8
+ #
9
+ # # >>> acstack terminal-workspace >>>
10
+ # [worktrees]
11
+ # directory = "~/.herdr/worktrees"
12
+ # # <<< acstack terminal-workspace <<<
13
+ #
14
+ # The fence is what makes the script safe to run twice, and safe to run against
15
+ # a file somebody has been editing by hand. Every line outside the block
16
+ # survives, in its original order, with its content unchanged, and only the
17
+ # block's own lines are ever rewritten. Two edits outside the block are possible
18
+ # and both are deliberate: a blank separator line goes in ahead of a block being
19
+ # appended to a file that already had content, and a file that ended without a
20
+ # newline gains one.
21
+ #
22
+ # It refuses, and writes nothing, in three cases. Two of them are files that no
23
+ # edit leaves readable: a file holding more than one such block, and a file
24
+ # defining [worktrees] outside the block. TOML rejects a table defined twice,
25
+ # and guessing which copy the user meant to keep would be the wrong kind of
26
+ # helpful. The third is a block this script did not write.
27
+ # assets/terminal-workspace/setup.sh fences a far wider block between these same
28
+ # two markers, holding the keymap and the popup key bindings as well as
29
+ # [worktrees], and replacing that with these four lines would delete all of it.
30
+ # A wider block that already names the wanted directory is left alone and
31
+ # reported as nothing to do.
32
+ #
33
+ # There are two modes and no default. --plan prints what would happen and writes
34
+ # nothing at all; --apply does it. Plan mode cannot write by accident, because
35
+ # the one function in this script that writes is only ever called on the apply
36
+ # path.
37
+ #
38
+ # Every path derives from $HOME or from --config, and nothing is hardcoded to a
39
+ # real location. That is what makes it testable: tests/test-host-install.sh runs
40
+ # it under `env -i` with a PATH of fake executables and a temporary HOME.
41
+
42
+ set -u
43
+
44
+ PROG="install-herdr-block.sh"
45
+ TOTAL=1
46
+
47
+ # --- the managed block -----------------------------------------------------
48
+ #
49
+ # These four strings are the contract. The markers are matched on whole lines,
50
+ # exactly, so a line that merely mentions one of them is ordinary content.
51
+
52
+ BLOCK_START='# >>> acstack terminal-workspace >>>'
53
+ BLOCK_END='# <<< acstack terminal-workspace <<<'
54
+ BLOCK_TABLE='[worktrees]'
55
+ # shellcheck disable=SC2088 # the literal "~/" is what gets written into config.toml; herdr expands it
56
+ DEFAULT_WORKTREE_DIR='~/.herdr/worktrees'
57
+ # Where the kernel release string lives on Linux. An option overrides it, which
58
+ # is what lets a test hand the Windows Subsystem for Linux check its own file.
59
+ DEFAULT_OSRELEASE_FILE="/proc/sys/kernel/osrelease"
60
+
61
+ usage() {
62
+ cat <<'USAGE'
63
+ install-herdr-block.sh - point herdr's worktrees at one directory.
64
+
65
+ Usage:
66
+ install-herdr-block.sh --plan [options] Print what would happen. Writes nothing.
67
+ install-herdr-block.sh --apply [options] Do it. Running it twice is a no-op.
68
+ install-herdr-block.sh --help Print this help and exit.
69
+
70
+ Options:
71
+ --worktree-dir DIR Directory herdr puts worktrees in.
72
+ Default: ~/.herdr/worktrees, written literally, with the
73
+ tilde left for herdr to expand.
74
+ --config PATH Config file to edit.
75
+ Default: ~/.config/herdr/config.toml
76
+ --osrelease-file PATH
77
+ Kernel release file the Windows Subsystem for Linux
78
+ check reads. Default: /proc/sys/kernel/osrelease
79
+
80
+ Exactly one of --plan and --apply is required; neither is the default, because
81
+ this script edits a file you may have written by hand.
82
+
83
+ Only the fenced block between the markers
84
+
85
+ # >>> acstack terminal-workspace >>>
86
+ # <<< acstack terminal-workspace <<<
87
+
88
+ is ever written. Everything else in the file is preserved exactly, in order.
89
+
90
+ Exit codes: 0 all good, 1 the write failed, 3 this machine is not Linux,
91
+ 4 the file is not safe to edit, 64 the command line was wrong.
92
+ USAGE
93
+ }
94
+
95
+ # --- small helpers ---------------------------------------------------------
96
+
97
+ err() { printf '%s: %s\n' "$PROG" "$1" >&2; }
98
+
99
+ fail() { err "$1"; exit 1; }
100
+
101
+ die_usage() {
102
+ err "$1"
103
+ err "run with --help for usage."
104
+ exit 64
105
+ }
106
+
107
+ # The file cannot be edited without corrupting it. Say why, in full, and stop
108
+ # without having written anything.
109
+ refuse() {
110
+ err "$1"
111
+ exit 4
112
+ }
113
+
114
+ # --- argument parsing ------------------------------------------------------
115
+ #
116
+ # Parsed in full first, so --help and a bad command line cost nothing: no
117
+ # platform detection and no file so much as opened.
118
+
119
+ MODE=""
120
+ WORKTREE_DIR="$DEFAULT_WORKTREE_DIR"
121
+ CONFIG=""
122
+ OSRELEASE_FILE="$DEFAULT_OSRELEASE_FILE"
123
+
124
+ need_value() { # $1=option name, $2=arguments left including the option itself
125
+ [ "$2" -ge 2 ] || die_usage "$1 needs a value."
126
+ }
127
+
128
+ set_mode() {
129
+ [ -z "$MODE" ] || die_usage "choose exactly one of --plan and --apply."
130
+ MODE="$1"
131
+ }
132
+
133
+ while [ "$#" -gt 0 ]; do
134
+ case "$1" in
135
+ --plan) set_mode "plan" ;;
136
+ --apply) set_mode "apply" ;;
137
+ --worktree-dir) need_value "$1" "$#"; WORKTREE_DIR="$2"; shift ;;
138
+ --config) need_value "$1" "$#"; CONFIG="$2"; shift ;;
139
+ --osrelease-file) need_value "$1" "$#"; OSRELEASE_FILE="$2"; shift ;;
140
+ --help|-h) usage; exit 0 ;;
141
+ *) die_usage "unknown argument: $1" ;;
142
+ esac
143
+ shift
144
+ done
145
+
146
+ [ -n "$MODE" ] || die_usage "one of --plan and --apply is required."
147
+
148
+ [ -n "$WORKTREE_DIR" ] || die_usage "--worktree-dir needs a directory."
149
+
150
+ # The directory goes into a basic TOML string. A double quote would end that
151
+ # string early and a backslash starts an escape, so neither can be let through:
152
+ # the result would be a config file herdr cannot parse.
153
+ case "$WORKTREE_DIR" in
154
+ *'"'*|*'\'*) die_usage "--worktree-dir may not hold a double quote or a backslash, and this one does: $WORKTREE_DIR" ;;
155
+ esac
156
+
157
+ [ -n "${HOME:-}" ] || fail "HOME is not set, so there is no default config path."
158
+ [ -n "$CONFIG" ] || CONFIG="$HOME/.config/herdr/config.toml"
159
+
160
+ # --- platform gate ---------------------------------------------------------
161
+ #
162
+ # herdr is a Linux terminal workspace. Elsewhere there is no config file worth
163
+ # writing, so the script 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
+ # herdr is not supported there. Two markers give it away and either one on its
178
+ # 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 block -------------------------------------------------------------
200
+
201
+ DIR_LINE="directory = \"$WORKTREE_DIR\""
202
+
203
+ render_block() {
204
+ printf '%s\n' "$BLOCK_START"
205
+ printf '%s\n' "$BLOCK_TABLE"
206
+ printf '%s\n' "$DIR_LINE"
207
+ printf '%s\n' "$BLOCK_END"
208
+ }
209
+
210
+ BLOCK_WANTED="$(render_block)"
211
+
212
+ # True when $1 is a line that opens the [worktrees] table, allowing for the
213
+ # surrounding whitespace TOML permits.
214
+ is_worktrees_table() {
215
+ local s="$1"
216
+ s="${s#"${s%%[![:space:]]*}"}"
217
+ s="${s%"${s##*[![:space:]]}"}"
218
+ [ "$s" = "$BLOCK_TABLE" ]
219
+ }
220
+
221
+ # The block's own lines, markers stripped off.
222
+ block_body() {
223
+ printf '%s\n' "$BLOCK_FOUND" | sed '1d;$d'
224
+ }
225
+
226
+ # TOML tables the block defines besides [worktrees], space separated, and empty
227
+ # when it defines none.
228
+ #
229
+ # This script owns the [worktrees] table and nothing else, so a block holding a
230
+ # second table was written by somebody else. That somebody is
231
+ # assets/terminal-workspace/setup.sh, which fences a far wider block - [keys], a
232
+ # [[keys.command]] per binding, and [worktrees] - between these same two
233
+ # markers. Replacing that block with these four lines deletes a keymap, so the
234
+ # shape of the block is what tells the two writers apart.
235
+ foreign_tables() {
236
+ local line s out=""
237
+ while IFS= read -r line; do
238
+ s="${line#"${line%%[![:space:]]*}"}"
239
+ s="${s%"${s##*[![:space:]]}"}"
240
+ case "$s" in
241
+ \[*\]) ;;
242
+ *) continue ;;
243
+ esac
244
+ is_worktrees_table "$s" && continue
245
+ case " $out " in *" $s "*) continue ;; esac
246
+ out="${out:+$out }$s"
247
+ done <<BODY
248
+ $(block_body)
249
+ BODY
250
+ printf '%s' "$out"
251
+ }
252
+
253
+ # Whether the block already carries the exact directory line this script would
254
+ # write. A wider block that already says the right thing needs no edit at all.
255
+ block_sets_wanted_dir() {
256
+ printf '%s\n' "$BLOCK_FOUND" | grep -qxF -- "$DIR_LINE"
257
+ }
258
+
259
+ # Line numbers of every whole line equal to $1 in $2, space separated.
260
+ marker_lines() {
261
+ grep -nxF -- "$1" "$2" 2>/dev/null | cut -d: -f1 | tr '\n' ' '
262
+ }
263
+
264
+ count_words() {
265
+ printf '%s' "$1" | wc -w
266
+ }
267
+
268
+ # --- read the file and decide ----------------------------------------------
269
+ #
270
+ # ACTION is one of: create, append, replace, none. Working it out is entirely
271
+ # read-only, and both modes reach it the same way.
272
+
273
+ BLOCK_FOUND="" # the block exactly as the file holds it now
274
+ UNMANAGED_LINES="" # line numbers of [worktrees] tables outside the block
275
+ UNTERMINATED=0 # a start marker the file never closes
276
+
277
+ # One pass, tracking whether we are inside the block, because a [worktrees] line
278
+ # inside the block is ours and a [worktrees] line outside it is somebody else's.
279
+ scan_config() {
280
+ local n=0 inside=0 line
281
+ while IFS= read -r line || [ -n "$line" ]; do
282
+ n=$((n + 1))
283
+ if [ "$inside" -eq 0 ] && [ "$line" = "$BLOCK_START" ]; then
284
+ inside=1
285
+ BLOCK_FOUND="$line"
286
+ continue
287
+ fi
288
+ if [ "$inside" -eq 1 ]; then
289
+ BLOCK_FOUND="$BLOCK_FOUND
290
+ $line"
291
+ [ "$line" = "$BLOCK_END" ] && inside=0
292
+ continue
293
+ fi
294
+ if is_worktrees_table "$line"; then
295
+ UNMANAGED_LINES="${UNMANAGED_LINES:+$UNMANAGED_LINES }$n"
296
+ fi
297
+ done < "$1"
298
+ UNTERMINATED="$inside"
299
+ }
300
+
301
+ ACTION="create"
302
+ SKIP_REASON=""
303
+
304
+ if [ -e "$CONFIG" ] && [ ! -f "$CONFIG" ]; then
305
+ refuse "$CONFIG exists but is not a regular file, so it will not be edited."
306
+ fi
307
+
308
+ if [ -f "$CONFIG" ]; then
309
+ START_LINES="$(marker_lines "$BLOCK_START" "$CONFIG")"
310
+ END_LINES="$(marker_lines "$BLOCK_END" "$CONFIG")"
311
+ N_START="$(count_words "$START_LINES")"
312
+ N_END="$(count_words "$END_LINES")"
313
+
314
+ if [ "$N_START" -gt 1 ] || [ "$N_END" -gt 1 ]; then
315
+ err "$CONFIG holds more than one acstack terminal-workspace block."
316
+ err "start markers on line(s): ${START_LINES:-none}"
317
+ err "end markers on line(s): ${END_LINES:-none}"
318
+ refuse "leave exactly one block in place and run this again. Nothing has been changed."
319
+ fi
320
+ if [ "$N_START" -ne "$N_END" ]; then
321
+ err "$CONFIG has an acstack terminal-workspace fence that does not pair up:"
322
+ err "start markers on line(s): ${START_LINES:-none}"
323
+ err "end markers on line(s): ${END_LINES:-none}"
324
+ refuse "repair the fence by hand and run this again. Nothing has been changed."
325
+ fi
326
+
327
+ scan_config "$CONFIG"
328
+
329
+ if [ "$UNTERMINATED" -eq 1 ]; then
330
+ refuse "$CONFIG opens an acstack terminal-workspace block on line ${START_LINES:-?}and never closes it. Repair it by hand and run this again. Nothing has been changed."
331
+ fi
332
+ if [ -n "$UNMANAGED_LINES" ]; then
333
+ err "$CONFIG already defines a $BLOCK_TABLE table on line(s): $UNMANAGED_LINES"
334
+ err "That table is outside the managed block, so writing the block would define $BLOCK_TABLE twice and herdr would no longer be able to read the file."
335
+ refuse "move that table inside the block, or delete it, and run this again. Nothing has been changed."
336
+ fi
337
+
338
+ if [ "$N_START" -eq 1 ]; then
339
+ FOREIGN_TABLES="$(foreign_tables)"
340
+ if [ "$BLOCK_FOUND" = "$BLOCK_WANTED" ]; then
341
+ ACTION="none"
342
+ SKIP_REASON="the block is already exactly right"
343
+ elif [ -n "$FOREIGN_TABLES" ]; then
344
+ if block_sets_wanted_dir; then
345
+ ACTION="none"
346
+ SKIP_REASON="the terminal-workspace block already sets this directory"
347
+ else
348
+ err "$CONFIG holds a block between these markers that this script did not write."
349
+ err "Besides $BLOCK_TABLE that block defines: $FOREIGN_TABLES"
350
+ err "terminal-workspace setup.sh writes that block, and it carries the keymap and the popup key bindings. Replacing it with the four lines this script writes would delete all of it."
351
+ err "setup.sh writes $BLOCK_TABLE itself, from herdr.worktrees_directory in ~/.config/acstack/terminal-workspace.yaml."
352
+ refuse "set the directory there and run terminal-workspace setup.sh apply instead. Nothing has been changed."
353
+ fi
354
+ else
355
+ ACTION="replace"
356
+ fi
357
+ else
358
+ ACTION="append"
359
+ fi
360
+ fi
361
+
362
+ case "$ACTION" in
363
+ create) DETAIL="the file does not exist yet, so it is created holding just the block" ;;
364
+ append) DETAIL="the file exists without the block, so the block is added at the end" ;;
365
+ replace) DETAIL="the file holds an out-of-date block, so those lines are replaced in place" ;;
366
+ *) DETAIL="" ;;
367
+ esac
368
+
369
+ # --- the write -------------------------------------------------------------
370
+
371
+ # The whole new file, on stdout. Lines outside the block are reproduced exactly
372
+ # and in order; only the block's four lines come from us.
373
+ render_new_config() {
374
+ local line inside=0 had_content=0
375
+ case "$ACTION" in
376
+ create)
377
+ render_block
378
+ ;;
379
+ append)
380
+ while IFS= read -r line || [ -n "$line" ]; do
381
+ printf '%s\n' "$line"
382
+ had_content=1
383
+ done < "$CONFIG"
384
+ [ "$had_content" -eq 1 ] && printf '\n'
385
+ render_block
386
+ ;;
387
+ replace)
388
+ while IFS= read -r line || [ -n "$line" ]; do
389
+ if [ "$inside" -eq 0 ] && [ "$line" = "$BLOCK_START" ]; then
390
+ inside=1
391
+ render_block
392
+ continue
393
+ fi
394
+ if [ "$inside" -eq 1 ]; then
395
+ [ "$line" = "$BLOCK_END" ] && inside=0
396
+ continue
397
+ fi
398
+ printf '%s\n' "$line"
399
+ done < "$CONFIG"
400
+ ;;
401
+ esac
402
+ }
403
+
404
+ # The one function in this script that writes anything. It stages the new file
405
+ # beside the old one and moves it into place, so a config file is either the old
406
+ # one or the new one and never a half-written mixture.
407
+ write_config() {
408
+ local dir tmp mode=""
409
+
410
+ dir="$(dirname "$CONFIG")"
411
+ mkdir -p "$dir" || { err "could not create $dir"; return 1; }
412
+
413
+ tmp="$(mktemp "$dir/.herdr-config.XXXXXX")" || {
414
+ err "could not create a temporary file in $dir"; return 1; }
415
+
416
+ if ! render_new_config > "$tmp"; then
417
+ rm -f "$tmp"; err "could not write the new config to $tmp"; return 1
418
+ fi
419
+
420
+ [ -f "$CONFIG" ] && mode="$(stat -c '%a' "$CONFIG" 2>/dev/null || true)"
421
+
422
+ if ! mv -f "$tmp" "$CONFIG"; then
423
+ rm -f "$tmp"; err "could not move the new config into $CONFIG"; return 1
424
+ fi
425
+
426
+ # mktemp makes the staged file private; give the config back the permissions
427
+ # it had, or the ordinary ones if it is new.
428
+ chmod "${mode:-644}" "$CONFIG" || { err "could not restore the mode of $CONFIG"; return 1; }
429
+ }
430
+
431
+ # --- reporting -------------------------------------------------------------
432
+
433
+ step_line() { # $1=number $2=description $3=needed $4=reason
434
+ local status
435
+ if [ "$3" -eq 1 ]; then
436
+ if [ "$MODE" = "plan" ]; then status="[todo]"; else status="[done]"; fi
437
+ else
438
+ if [ "$MODE" = "plan" ]; then status="[skip: $4]"; else status="[skipped: $4]"; fi
439
+ fi
440
+ printf '[%s/%s] %s %s\n' "$1" "$TOTAL" "$2" "$status"
441
+ }
442
+
443
+ # --- run -------------------------------------------------------------------
444
+
445
+ if [ "$MODE" = "plan" ]; then
446
+ printf '%s: plan only. Nothing on this machine is changed.\n\n' "$PROG"
447
+ else
448
+ printf '%s: applying.\n\n' "$PROG"
449
+ fi
450
+
451
+ NEEDED=1
452
+ [ "$ACTION" = "none" ] && NEEDED=0
453
+
454
+ # The work comes first, so that a failure never gets to print [done].
455
+ if [ "$NEEDED" -eq 1 ] && [ "$MODE" = "apply" ]; then
456
+ write_config || fail "could not write the worktrees block into $CONFIG"
457
+ fi
458
+
459
+ step_line 1 "worktrees block in $CONFIG" "$NEEDED" "$SKIP_REASON"
460
+
461
+ if [ "$NEEDED" -eq 1 ]; then
462
+ printf ' %s:\n' "$DETAIL"
463
+ render_block | sed 's/^/ /'
464
+ fi
465
+
466
+ printf '\n'
467
+ printf 'Nothing here needs root: the only file touched is %s.\n' "$CONFIG"