autonomous-sdlc-harness 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +7 -0
  3. package/README.md +24 -0
  4. package/dist/cli.js +194 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/config.js +561 -0
  7. package/dist/commands/config.js.map +1 -0
  8. package/dist/commands/daemon.js +791 -0
  9. package/dist/commands/daemon.js.map +1 -0
  10. package/dist/commands/doctor.js +336 -0
  11. package/dist/commands/doctor.js.map +1 -0
  12. package/dist/commands/init.js +2023 -0
  13. package/dist/commands/init.js.map +1 -0
  14. package/dist/commands/registry.js +42 -0
  15. package/dist/commands/registry.js.map +1 -0
  16. package/dist/config/check.js +505 -0
  17. package/dist/config/check.js.map +1 -0
  18. package/dist/config/io.js +177 -0
  19. package/dist/config/io.js.map +1 -0
  20. package/dist/config/model.js +406 -0
  21. package/dist/config/model.js.map +1 -0
  22. package/dist/core/errors.js +71 -0
  23. package/dist/core/errors.js.map +1 -0
  24. package/dist/core/git.js +537 -0
  25. package/dist/core/git.js.map +1 -0
  26. package/dist/core/json.js +125 -0
  27. package/dist/core/json.js.map +1 -0
  28. package/dist/core/layerCoverage.js +141 -0
  29. package/dist/core/layerCoverage.js.map +1 -0
  30. package/dist/core/layerGapRemedy.js +62 -0
  31. package/dist/core/layerGapRemedy.js.map +1 -0
  32. package/dist/core/nameList.js +23 -0
  33. package/dist/core/nameList.js.map +1 -0
  34. package/dist/core/paths.js +153 -0
  35. package/dist/core/paths.js.map +1 -0
  36. package/dist/core/prompt.js +206 -0
  37. package/dist/core/prompt.js.map +1 -0
  38. package/dist/core/repoPaths.js +55 -0
  39. package/dist/core/repoPaths.js.map +1 -0
  40. package/dist/core/report.js +150 -0
  41. package/dist/core/report.js.map +1 -0
  42. package/dist/core/templating.js +88 -0
  43. package/dist/core/templating.js.map +1 -0
  44. package/dist/core/writer.js +479 -0
  45. package/dist/core/writer.js.map +1 -0
  46. package/dist/daemon/backend.js +180 -0
  47. package/dist/daemon/backend.js.map +1 -0
  48. package/dist/daemon/units.js +380 -0
  49. package/dist/daemon/units.js.map +1 -0
  50. package/dist/detect/nestedApplication.js +79 -0
  51. package/dist/detect/nestedApplication.js.map +1 -0
  52. package/dist/detect/presets.js +2033 -0
  53. package/dist/detect/presets.js.map +1 -0
  54. package/dist/detect/signals.js +1368 -0
  55. package/dist/detect/signals.js.map +1 -0
  56. package/dist/doctor/checks.js +3530 -0
  57. package/dist/doctor/checks.js.map +1 -0
  58. package/dist/generators/claudeContext.js +588 -0
  59. package/dist/generators/claudeContext.js.map +1 -0
  60. package/dist/generators/githooks.js +446 -0
  61. package/dist/generators/githooks.js.map +1 -0
  62. package/dist/generators/harnessConfig.js +632 -0
  63. package/dist/generators/harnessConfig.js.map +1 -0
  64. package/dist/generators/notifications.js +191 -0
  65. package/dist/generators/notifications.js.map +1 -0
  66. package/dist/generators/outerLoopScripts.js +165 -0
  67. package/dist/generators/outerLoopScripts.js.map +1 -0
  68. package/dist/generators/permissionProfile.js +1172 -0
  69. package/dist/generators/permissionProfile.js.map +1 -0
  70. package/dist/generators/projectSettings.js +322 -0
  71. package/dist/generators/projectSettings.js.map +1 -0
  72. package/dist/generators/repoRoot.js +417 -0
  73. package/dist/generators/repoRoot.js.map +1 -0
  74. package/dist/generators/scripts.js +557 -0
  75. package/dist/generators/scripts.js.map +1 -0
  76. package/dist/generators/stateDir.js +221 -0
  77. package/dist/generators/stateDir.js.map +1 -0
  78. package/dist/machine/paths.js +111 -0
  79. package/dist/machine/paths.js.map +1 -0
  80. package/dist/machine/plugins.js +224 -0
  81. package/dist/machine/plugins.js.map +1 -0
  82. package/dist/machine/registry.js +330 -0
  83. package/dist/machine/registry.js.map +1 -0
  84. package/package.json +23 -0
  85. package/scripts/README.md +13 -0
  86. package/scripts/daemon/launchd.plist.template +59 -0
  87. package/scripts/daemon/systemd.service.template +58 -0
  88. package/templates/README.md +15 -0
  89. package/templates/claude/CLAUDE.md +54 -0
  90. package/templates/claude/README.md +5 -0
  91. package/templates/claude/context/api.md +29 -0
  92. package/templates/claude/context/conventions.md +23 -0
  93. package/templates/claude/context/data-layer.md +28 -0
  94. package/templates/claude/context/data-storage.md +29 -0
  95. package/templates/claude/context/docs-catalog.md +29 -0
  96. package/templates/claude/context/domain.md +28 -0
  97. package/templates/claude/context/layer.md +20 -0
  98. package/templates/claude/context/module.md +30 -0
  99. package/templates/claude/context/package.md +29 -0
  100. package/templates/claude/context/presentation.md +32 -0
  101. package/templates/claude/context/state-slices.md +28 -0
  102. package/templates/claude/context/tests.md +28 -0
  103. package/templates/claude/harness-task-offer.md +58 -0
  104. package/templates/claude/push-notify.env.example +21 -0
  105. package/templates/claude/qa-accounts.env.example +38 -0
  106. package/templates/claude/qa_test_scenarios.md +110 -0
  107. package/templates/claude/settings.autonomous.json +93 -0
  108. package/templates/claude/settings.autonomous.qa.json +36 -0
  109. package/templates/githooks/README.md +3 -0
  110. package/templates/githooks/pre-push +72 -0
  111. package/templates/repo/README.md +3 -0
  112. package/templates/repo/gitattributes +16 -0
  113. package/templates/repo/gitignore +61 -0
  114. package/templates/repo/gitignore.qa +25 -0
  115. package/templates/repo/mcp.json +17 -0
  116. package/templates/scripts/README.md +5 -0
  117. package/templates/scripts/autonomous-format-stream.sh +95 -0
  118. package/templates/scripts/autonomous-notify.sh +337 -0
  119. package/templates/scripts/autonomous-watcher.sh +3087 -0
  120. package/templates/scripts/cleanup-merged-worktrees.sh +327 -0
  121. package/templates/scripts/commit-on-branch.sh +288 -0
  122. package/templates/scripts/create-worktree.sh +360 -0
  123. package/templates/scripts/deploy.sh +47 -0
  124. package/templates/scripts/lib/harness-run-lib.sh +1481 -0
  125. package/templates/scripts/push-branch.sh +140 -0
  126. package/templates/scripts/refresh-branch.sh +244 -0
  127. package/templates/scripts/restart-watcher.sh +401 -0
  128. package/templates/scripts/scratch-run.sh +302 -0
  129. package/templates/scripts/setup-worktree.sh +262 -0
  130. package/templates/scripts/start-dev-server.sh +99 -0
  131. package/templates/scripts/test.sh +50 -0
  132. package/templates/scripts/typecheck.sh +50 -0
  133. package/templates/state-dir/README-root.md +13 -0
  134. package/templates/state-dir/README.md +9 -0
  135. package/templates/state-dir/architecture_branch_review_point_reviews/README.md +9 -0
  136. package/templates/state-dir/architecture_branch_reviews/README.md +9 -0
  137. package/templates/state-dir/architecture_reviews/README.md +9 -0
  138. package/templates/state-dir/architecture_user_review_reviews/README.md +9 -0
  139. package/templates/state-dir/autonomous_inbox/README.md +9 -0
  140. package/templates/state-dir/autonomous_logs/README.md +9 -0
  141. package/templates/state-dir/branch_statistics/README.md +9 -0
  142. package/templates/state-dir/business_parity_branch_review_point_reviews/README.md +9 -0
  143. package/templates/state-dir/business_parity_branch_reviews/README.md +9 -0
  144. package/templates/state-dir/business_parity_reviews/README.md +9 -0
  145. package/templates/state-dir/business_parity_user_review_reviews/README.md +9 -0
  146. package/templates/state-dir/clarification_digests/README.md +9 -0
  147. package/templates/state-dir/clarifications/README.md +9 -0
  148. package/templates/state-dir/code_reviews/README.md +9 -0
  149. package/templates/state-dir/dispatch_additions/README.md +19 -0
  150. package/templates/state-dir/docs_catalog/README.md +9 -0
  151. package/templates/state-dir/flow_progress/README.md +9 -0
  152. package/templates/state-dir/improvement_observations/README.md +19 -0
  153. package/templates/state-dir/improvement_suggestions.md +29 -0
  154. package/templates/state-dir/lessons.md +23 -0
  155. package/templates/state-dir/qa_review_point_reviews/README.md +9 -0
  156. package/templates/state-dir/qa_reviews/README.md +9 -0
  157. package/templates/state-dir/review_plan_point_reviews/README.md +9 -0
  158. package/templates/state-dir/review_plan_reviews/README.md +9 -0
  159. package/templates/state-dir/scratch/README.md +11 -0
  160. package/templates/state-dir/skeptic_review_plan_reviews/README.md +9 -0
  161. package/templates/state-dir/skeptic_review_point_reviews/README.md +9 -0
  162. package/templates/state-dir/skeptic_reviews/README.md +9 -0
  163. package/templates/state-dir/story_plans/README.md +9 -0
  164. package/templates/state-dir/task_plan_point_reviews/README.md +9 -0
  165. package/templates/state-dir/task_plan_reviews/README.md +9 -0
  166. package/templates/state-dir/task_plans/README.md +9 -0
  167. package/templates/state-dir/task_prompts/README.md +9 -0
  168. package/templates/state-dir/ui_test_plan_reviews/README.md +9 -0
  169. package/templates/state-dir/ui_test_plans/README.md +9 -0
  170. package/templates/state-dir/user_review_fix_plan_point_reviews/README.md +9 -0
  171. package/templates/state-dir/user_reviews/README.md +9 -0
@@ -0,0 +1,401 @@
1
+ #!/usr/bin/env bash
2
+ # restart-watcher.sh — bounce this repository's run daemon so the watcher, and
3
+ # the scripts it calls (the stream formatter, the notifier, the cleanup sweep),
4
+ # are re-read from the checkout. Run it after editing any of them: the service
5
+ # manager starts the watcher once and the watcher parses its file once, so an
6
+ # edit is inert in the live daemon until it is restarted.
7
+ #
8
+ # WHAT IT DOES, AND ALL IT DOES. It decides whether restarting is safe right
9
+ # now, and then delegates the lifecycle to the harness CLI:
10
+ #
11
+ # <cli> daemon stop then <cli> daemon install
12
+ #
13
+ # both run FROM THE MAIN CHECKOUT. Nothing here composes a service label, a unit
14
+ # path or a service-manager command line of its own. The CLI derives the backend
15
+ # (launchd or systemd), the per-repository label and the unit path together, in
16
+ # one place, from the repository it is run in — a second derivation here is
17
+ # exactly how a daemon gets installed under one name and addressed by another,
18
+ # and on the day the two disagreed no `stop` would reach the running service.
19
+ # Running from the main checkout rather than from a sibling working copy is part
20
+ # of that: the identity is derived from the CLI's working directory, so a bounce
21
+ # started inside a worktree would otherwise stop nothing and install a SECOND
22
+ # daemon for the worktree's path. Both delegated commands print the label and
23
+ # the unit path they resolved; that output is the answer to "which daemon did
24
+ # this just bounce", and it is the only place that answer comes from.
25
+ #
26
+ # LAUNCHD IS BOUNCED; SYSTEMD IS INSTRUCTED, and that is the CLI's split rather
27
+ # than this script's (docs/cli.md §9). On macOS the two commands above drive
28
+ # launchctl, and the bounce is guaranteed by the PAIR exiting 0 — a `daemon
29
+ # stop` that failed leaves `daemon install` to surface launchctl's refusal
30
+ # (docs/cli.md §9 keeps that non-zero deliberately, and this script still
31
+ # attempts the install after a failed stop), so a non-zero `status` here is
32
+ # exactly how that case reaches you. On a systemd host the two commands write
33
+ # the unit and PRINT the `systemctl --user` lines for you to run, and both
34
+ # still exit 0 — so a successful run of this script on systemd means "the
35
+ # instructions are correct and printed", not "the daemon has been restarted".
36
+ # Until you run them the live watcher is still the process the service manager
37
+ # started, holding the copy of these files that bash parsed then.
38
+ #
39
+ # WHY IT EXISTS AT ALL, given the CLI already has both verbs: the one thing
40
+ # `daemon stop` + `daemon install` do not do is refuse while a run is in flight.
41
+ #
42
+ # SAFETY — A RESTART KILLS IN-FLIGHT RUNS. Stopping the service tears down the
43
+ # job's WHOLE process tree: the watcher, each run's subshell, and that run's
44
+ # agent child. A killed run does NOT auto-resume — the watcher's reconcile pass
45
+ # marks it `failed` on the next tick, and only cleanly PARKED OR PAUSED runs
46
+ # are ever re-launched. Committed work is safe on the branch; a killed run is
47
+ # continued by re-launching the engine BY HAND in its worktree, where it picks
48
+ # up from the last commit and the next unchecked task. So this REFUSES by
49
+ # default while a run is in flight, and `--force` is how you say you meant it.
50
+ #
51
+ # WHAT COUNTS AS IN FLIGHT, and what happens when that cannot be answered:
52
+ #
53
+ # * the run registry (`<state_dir>/autonomous_logs/registry.json`, shaped
54
+ # `{"runs": {"<branch>": {"status": …}}}`) is the SOURCE OF TRUTH: a record
55
+ # whose status is `running` or `parked` is a run in flight;
56
+ # * a process probe for the agent binary — `${HARNESS_AGENT_CLI:-claude}`, the
57
+ # same variable the watcher launches through — is a BACKSTOP, for a run OF
58
+ # THIS PROJECT whose record has not been written yet or was written by a
59
+ # watcher that was killed before it could update it. It is SCOPED to this
60
+ # project, because the probe searches every process on the machine while a
61
+ # daemon's identity is per repository: a match counts only if the process's
62
+ # arguments name this repository's MAIN CHECKOUT or its sibling-worktree
63
+ # prefix `<work_root>/<projectName>-`. The second spelling is what keeps an
64
+ # engine RE-LAUNCHED BY HAND in its worktree — the continuation route the
65
+ # SAFETY block above prescribes, which has no registry record either —
66
+ # visible to this guard; without it the refusal would become a silent
67
+ # `--force`. A process whose argument vector cannot be read COUNTS AS A
68
+ # MATCH, on the same footing as the unanswerable cases below;
69
+ # * a registry that is ABSENT is not an error and not a refusal: a repository
70
+ # the watcher has never run in has no registry and no run either, and the
71
+ # process probe still guards that case;
72
+ # * a registry that is THERE but cannot be read as a registry — unreadable,
73
+ # invalid JSON, or no `.runs` wrapper — and a configuration that is there
74
+ # and cannot be read are refusals, on the same footing as a run in flight.
75
+ # This script cannot prove nothing is running, and reading "cannot tell" as
76
+ # "nothing is running" is a killed run. A repository with no configuration
77
+ # at all is the separate case: it has no daemon to bounce, so it stops
78
+ # without a refusal to override.
79
+ #
80
+ # NEVER RUN BY A DISPATCHED AGENT. It has no entry in the generated permission
81
+ # profile, and the script-allowlist guard withholds the permit rather than
82
+ # granting one — its basename is on that guard's deny list, and being on it means
83
+ # the guard stays silent — so an agent that tries it gets a prompt it cannot
84
+ # answer, never a permit. The reason is indirection
85
+ # rather than this script's own blast radius: restarting a watcher whose sweep
86
+ # force-deletes merged branches has an agent trigger `git branch -D` at one
87
+ # remove, through a service manager, on branches nobody asked it about. This is
88
+ # an operator's command; it stays one.
89
+ #
90
+ # Usage:
91
+ # restart-watcher.sh restart only if no run is in flight
92
+ # restart-watcher.sh --force restart even then, killing the run
93
+ # restart-watcher.sh --help print this header
94
+ #
95
+ # Exit map a caller can switch on:
96
+ #
97
+ # 0 both delegated commands succeeded. On launchd that pair IS the bounce and
98
+ # the daemon is up on the current file; on systemd it means the unit is
99
+ # written and the `systemctl --user` lines were printed for you to run —
100
+ # the restart is complete only once you have run them
101
+ # 1 nothing was restarted for a reason that is not about runs: a usage
102
+ # error, a missing shared library, no repository here, no configuration at
103
+ # the main checkout, the CLI not on PATH, or a delegated command that
104
+ # exited non-zero
105
+ # 2 REFUSED: a run is in flight, or it could not be proven that none is, and
106
+ # `--force` was not given. Nothing was stopped and nothing was installed
107
+ #
108
+ # REPRO — reproduce every decision by hand, against a throwaway fixture and a
109
+ # recorder standing in for the CLI, with no daemon and no service manager:
110
+ #
111
+ # w=$(mktemp -d); d="$w/demo"; git init -q -b trunk "$d"
112
+ # printf '%s' '{"version":1,"projectName":"demo","defaultBranch":"trunk","stateDir":"sdlc-harness/","layers":[],"commands":{}}' > "$d/harness.config.json"
113
+ # mkdir -p "$d/scripts/lib" "$d/sdlc-harness/autonomous_logs" "$w/bin"
114
+ # # copy this script into "$d/scripts" and lib/harness-run-lib.sh beside it
115
+ # printf '#!/usr/bin/env bash\necho "$*" >> "%s/calls"\n' "$w" > "$w/bin/hcli"
116
+ # chmod +x "$w/bin/hcli"
117
+ # reg="$d/sdlc-harness/autonomous_logs/registry.json"
118
+ # run() { PATH="$w/bin:$PATH" HARNESS_CLI=hcli bash "$d/scripts/restart-watcher.sh" "$@"; echo "exit $?"; }
119
+ #
120
+ # in flight printf '%s' '{"runs":{"feat_x":{"status":"running"}}}' > "$reg"
121
+ # run -> the in-flight report, exit 2, and no
122
+ # "$w/calls" at all (same for "parked")
123
+ # forced run --force -> the same report plus the restarting line,
124
+ # and "$w/calls" holds `daemon stop` then
125
+ # `daemon install`, in that order
126
+ # idle printf '%s' '{"runs":{"feat_x":{"status":"completed"}}}' > "$reg"
127
+ # rm -f "$w/calls"; run
128
+ # -> those same two lines and exit 0
129
+ # never ran rm -f "$reg"; run -> the restart proceeds, exit 0
130
+ # unreadable printf 'x' > "$reg"; run
131
+ # -> "cannot prove no run is in flight", exit 2
132
+ # no CLI HARNESS_CLI=definitely-not-installed run
133
+ # -> one line naming it, exit 1, no registry read
134
+ # stop failed printf '#!/usr/bin/env bash\necho "$*" >> "%s/calls"\ncase "$*" in *stop*) exit 3;; esac\n' "$w" > "$w/bin/hcli"
135
+ # rm -f "$w/calls"; run
136
+ # -> `daemon stop` reported as exit 3, the
137
+ # install still attempted once, exit 1
138
+
139
+ set -u
140
+
141
+ # The header itself is the `--help` text, so the two cannot drift: printed from
142
+ # this file's own leading comment block, stopping at the first line that is not
143
+ # one. Read through `${BASH_SOURCE[0]}` rather than `$0` so it still answers when
144
+ # the script is invoked through a wrapper.
145
+ print_header() {
146
+ local line first=1
147
+ while IFS= read -r line; do
148
+ if [ "$first" -eq 1 ]; then
149
+ first=0
150
+ continue
151
+ fi
152
+ case "$line" in
153
+ '#'*)
154
+ line=${line#\#}
155
+ printf '%s\n' "${line# }"
156
+ ;;
157
+ *) return 0 ;;
158
+ esac
159
+ done < "${BASH_SOURCE[0]}"
160
+ }
161
+
162
+ # --- Arguments. One optional flag, and nothing else accepted: a misspelling is
163
+ # refused here rather than silently becoming a plain restart (or, worse, a
164
+ # forced one).
165
+ force=0
166
+ case "${1:-}" in
167
+ --force)
168
+ force=1
169
+ shift
170
+ ;;
171
+ -h | --help)
172
+ print_header
173
+ exit 0
174
+ ;;
175
+ '') ;;
176
+ *)
177
+ echo "restart-watcher.sh: unrecognized argument '$1'" >&2
178
+ echo " usage: restart-watcher.sh [--force] (or --help)" >&2
179
+ exit 1
180
+ ;;
181
+ esac
182
+ if [ "$#" -gt 0 ]; then
183
+ echo "restart-watcher.sh: unexpected extra arguments" >&2
184
+ echo " usage: restart-watcher.sh [--force] (or --help)" >&2
185
+ exit 1
186
+ fi
187
+
188
+ # The library is reached by a path computed from this script's own location — no
189
+ # session root and no runtime-substituted token is assumed. Without it there is
190
+ # no way to find this repository's main checkout or its registry, so there is
191
+ # nothing to restart and nothing to check first.
192
+ hr_lib="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/harness-run-lib.sh"
193
+ if [ ! -r "$hr_lib" ]; then
194
+ echo "restart-watcher.sh: cannot read '$hr_lib' — nothing was restarted" >&2
195
+ exit 1
196
+ fi
197
+ # shellcheck source=lib/harness-run-lib.sh
198
+ . "$hr_lib"
199
+
200
+ # --- Anchors. Derived, never remembered: this copy may be sitting in the main
201
+ # checkout or in a sibling working copy of the same repository.
202
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
203
+ top="$(hr_repo_root "$script_dir")" || top=""
204
+ if [ -z "$top" ]; then
205
+ echo "restart-watcher.sh: '$script_dir' is not inside a git repository — nothing was restarted" >&2
206
+ exit 1
207
+ fi
208
+
209
+ # The daemon belongs to the MAIN checkout — that is where the watcher runs, and
210
+ # where the CLI derived its label from when it installed the unit. Unlike the
211
+ # configuration readers this does NOT fall back to the checkout it was started
212
+ # from: bouncing from a worktree would address an identity nothing installed.
213
+ main_repo="$(hr_main_repo "$top")" || main_repo=""
214
+ if [ -z "$main_repo" ]; then
215
+ echo "restart-watcher.sh: could not determine the main checkout of '$top' — nothing was restarted" >&2
216
+ echo " (the daemon's identity is derived from the checkout the CLI runs in, so guessing here would bounce the wrong one)" >&2
217
+ exit 1
218
+ fi
219
+
220
+ # --- The CLI that owns the lifecycle. Checked before anything is read, so a
221
+ # host without it stops here having done nothing — the analogue of asking
222
+ # whether the unit is installed before deciding whether it may be torn down.
223
+ cli="${HARNESS_CLI:-autonomous-sdlc-harness}"
224
+ if ! command -v "$cli" >/dev/null 2>&1; then
225
+ echo "restart-watcher.sh: '$cli' is not on PATH — nothing was restarted" >&2
226
+ echo " install the harness CLI, or set HARNESS_CLI to the command that runs it" >&2
227
+ exit 1
228
+ fi
229
+
230
+ # --- The safety gate. `unknown` is a non-empty REASON the in-flight question
231
+ # could not be answered, and it is treated exactly like an answered yes.
232
+ active=""
233
+ unknown=""
234
+ registry=""
235
+
236
+ # The library's 1/2 split is kept apart here, because the two mean different
237
+ # things to an operator: a repository with NO configuration has no harness
238
+ # daemon to bounce at all (nothing is in doubt, so this is not a `--force`
239
+ # case), while one whose configuration cannot be read is the repository this
240
+ # was meant to run in with the registry out of reach.
241
+ hr_config_load "$main_repo"
242
+ cfg_status=$?
243
+ if [ "$cfg_status" -eq 1 ]; then
244
+ echo "restart-watcher.sh: there is no '$main_repo/harness.config.json' — nothing was restarted" >&2
245
+ exit 1
246
+ elif [ "$cfg_status" -ne 0 ]; then
247
+ unknown="'$main_repo/harness.config.json' could not be resolved (unreadable, invalid JSON, more than one document, no defaultBranch, or jq missing/older than 1.5)"
248
+ else
249
+ registry="$(hr_state_path "$main_repo" autonomous_logs/registry.json)" || registry=""
250
+ if [ -z "$registry" ]; then
251
+ unknown="the run registry path could not be derived from the configuration"
252
+ elif [ ! -e "$registry" ]; then
253
+ : # No registry: the watcher has never run in this repository.
254
+ elif [ ! -r "$registry" ]; then
255
+ unknown="'$registry' is not readable"
256
+ # Enumerated THROUGH the `.runs` wrapper, deliberately without a `?`: a
257
+ # document that has no such wrapper is a registry this cannot enumerate, so
258
+ # `jq` fails and the refusal below fires. Reading such a file as "no active
259
+ # runs" is the one misreading that costs a run.
260
+ elif ! active="$(jq -r '.runs | to_entries[] | select(.value.status == "running" or .value.status == "parked") | "\(.value.status) \(.key)"' "$registry" 2>/dev/null)"; then
261
+ active=""
262
+ unknown="'$registry' could not be read as a run registry (invalid JSON, or no .runs wrapper)"
263
+ fi
264
+ fi
265
+
266
+ # The backstop probe. The basename is matched so a binary named by an absolute
267
+ # path still matches the command line it was spawned with, and the bracket in
268
+ # `<name>[ ]-p` keeps the pattern from matching the process running this probe.
269
+ #
270
+ # SCOPED TO THIS PROJECT, because `pgrep -f` searches the whole machine and a
271
+ # daemon's identity is per repository (docs/watcher.md §3): several armed
272
+ # repositories on one host is the designed configuration, and under the shipped
273
+ # defaults several of them RUNNING CONCURRENTLY is the steady state, because the
274
+ # advisory machine-level lock is opt-in and off (USAGE_LANE_LOCK_ENABLED, 0)
275
+ # (docs/watcher.md §5). An unscoped match refuses this repository's restart over
276
+ # a run it does not own, and an operator refused by pids that are visibly not
277
+ # theirs learns to reach for --force — which is the flag that kills this
278
+ # repository's runs.
279
+ #
280
+ # TWO SPELLINGS ARE ACCEPTED, and dropping either one is a fail-OPEN:
281
+ # * $main_repo/ — every run THIS WATCHER launched carries it (spawn_engine
282
+ # passes --settings "$MAIN_REPO/.claude/..." and --add-dir
283
+ # "<MAIN_REPO>/<state_dir>");
284
+ # * <work_root>/<projectName>- — a run an operator RE-LAUNCHED BY HAND in its
285
+ # worktree, which the SAFETY block above tells them to do. That process runs
286
+ # under a sibling checkout, carries no $main_repo path, and has no registry
287
+ # record either, so the main-checkout key alone would make this guard
288
+ # invisible to it and turn a refusal into a --force-equivalent teardown of a
289
+ # live run. The prefix is the same one hr_worktree_dir builds and the CLI's
290
+ # worktreeGlob() puts in the permission profile.
291
+ #
292
+ # FAIL SAFE ON AN UNREADABLE ARGUMENT VECTOR: a pid whose command line `ps`
293
+ # will not print is KEPT, not dropped. Losing a real in-flight run to a probe
294
+ # that could not read it is the expensive mistake here; an extra refusal is not.
295
+ # Same for an underivable worktree prefix: the prefix test is skipped, never
296
+ # treated as a non-match.
297
+ #
298
+ # `ps -ww -o args= -p <pid>` is accepted by both BSD `ps` (macOS) and procps
299
+ # `ps` (Linux), and `-ww` is what stops the argument vector being truncated to
300
+ # the terminal width — a truncated vector would drop the trailing --add-dir
301
+ # arguments and silently turn a match into a non-match.
302
+ headless=""
303
+ candidates=""
304
+ cand=""
305
+ args=""
306
+ mine=""
307
+ worktree_prefix=""
308
+ wt_work="$(hr_work_root "$main_repo" 2>/dev/null || true)"
309
+ wt_name="$(hr_project_name "$main_repo" 2>/dev/null || true)"
310
+ if [ -n "$wt_work" ] && [ -n "$wt_name" ]; then
311
+ worktree_prefix="${wt_work%/}/${wt_name}-"
312
+ fi
313
+ agent_probe="$(basename "${HARNESS_AGENT_CLI:-claude}")"
314
+ if [ -n "$agent_probe" ]; then
315
+ candidates="$(pgrep -f "${agent_probe}[ ]-p" 2>/dev/null || true)"
316
+ while IFS= read -r cand; do
317
+ [ -n "$cand" ] || continue
318
+ args="$(ps -ww -o args= -p "$cand" 2>/dev/null || true)"
319
+ if [ -n "$args" ]; then
320
+ mine=""
321
+ case "$args" in *"$main_repo"/*) mine="yes" ;; esac
322
+ if [ -z "$mine" ] && [ -n "$worktree_prefix" ]; then
323
+ case "$args" in *"$worktree_prefix"*) mine="yes" ;; esac
324
+ fi
325
+ [ -n "$mine" ] || continue
326
+ fi
327
+ if [ -n "$headless" ]; then headless="$headless
328
+ $cand"; else headless="$cand"; fi
329
+ done <<EOF
330
+ $candidates
331
+ EOF
332
+ fi
333
+
334
+ if [ -n "$active" ] || [ -n "$headless" ] || [ -n "$unknown" ]; then
335
+ if [ -n "$unknown" ]; then
336
+ echo "restart-watcher.sh: cannot prove no run is in flight — $unknown"
337
+ else
338
+ echo "restart-watcher.sh: a run appears to be IN FLIGHT —"
339
+ fi
340
+ if [ -n "$active" ]; then
341
+ while IFS= read -r line; do
342
+ [ -n "$line" ] || continue
343
+ printf ' registry: %s\n' "$line"
344
+ done <<EOF
345
+ $active
346
+ EOF
347
+ fi
348
+ if [ -n "$headless" ]; then
349
+ printf ' agent process id(s): %s\n' "$(printf '%s' "$headless" | tr '\n' ' ')"
350
+ fi
351
+ echo " A restart KILLS it: the whole process tree goes, killed runs reconcile to 'failed' and do NOT auto-resume."
352
+ echo " Committed work stays on the branch — re-launch the engine in that worktree to continue it."
353
+ if [ "$force" -eq 0 ]; then
354
+ echo " Aborting. Re-run with --force to restart anyway." >&2
355
+ exit 2
356
+ fi
357
+ echo " --force given: restarting despite the in-flight run."
358
+ fi
359
+
360
+ # --- The bounce. Two delegated commands, each run exactly once, each status
361
+ # surfaced as its own line. Run in a subshell so this script's own working
362
+ # directory is not what decides which repository the CLI acts on.
363
+ echo "restart-watcher.sh: $cli daemon stop, then $cli daemon install, in '$main_repo' ..."
364
+
365
+ status=0
366
+
367
+ (cd "$main_repo" && "$cli" daemon stop)
368
+ stop_status=$?
369
+ if [ "$stop_status" -ne 0 ]; then
370
+ echo "restart-watcher.sh: '$cli daemon stop' exited $stop_status" >&2
371
+ echo " (commonly: nothing was loaded under this repository's label, or no unit is installed yet)" >&2
372
+ # The install is still attempted, once: a stop that failed because there was
373
+ # nothing loaded leaves an install to do, and that is the case an operator
374
+ # runs this script in most often. It is not a retry of the stop, and the exit
375
+ # status below stays non-zero either way, so a failure is never reported as a
376
+ # clean bounce.
377
+ echo " attempting the install anyway; this run will still report a failure." >&2
378
+ status=1
379
+ fi
380
+
381
+ (cd "$main_repo" && "$cli" daemon install)
382
+ install_status=$?
383
+ if [ "$install_status" -ne 0 ]; then
384
+ echo "restart-watcher.sh: '$cli daemon install' exited $install_status" >&2
385
+ status=1
386
+ fi
387
+
388
+ if [ "$status" -eq 0 ]; then
389
+ # Deliberately not "restarted": this script does not drive the service manager
390
+ # and cannot observe whether one was driven. `daemon stop` and `daemon install`
391
+ # DRIVE launchd and only PRINT the `systemctl --user` lines on systemd (that
392
+ # asymmetry is the CLI's, and docs/cli.md §9 gives the reason) — so on a systemd
393
+ # host both exit 0 having changed nothing, and a "restarted" line here would be
394
+ # the exact failure docs/cli.md §9 designed against: a daemon still running the
395
+ # OLD parsed file while the command that replaced it reported success.
396
+ echo "restart-watcher.sh: '$cli daemon stop' and '$cli daemon install' both succeeded."
397
+ echo " On macOS/launchd that PAIR succeeding is the bounce: the daemon is running the current file."
398
+ echo " On systemd those two commands PRINT the systemctl lines above rather than running them:"
399
+ echo " the watcher is running the current file only once you have run them yourself."
400
+ fi
401
+ exit "$status"