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.
- package/LICENSE +201 -0
- package/NOTICE +7 -0
- package/README.md +24 -0
- package/dist/cli.js +194 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/config.js +561 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/daemon.js +791 -0
- package/dist/commands/daemon.js.map +1 -0
- package/dist/commands/doctor.js +336 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/init.js +2023 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/registry.js +42 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/config/check.js +505 -0
- package/dist/config/check.js.map +1 -0
- package/dist/config/io.js +177 -0
- package/dist/config/io.js.map +1 -0
- package/dist/config/model.js +406 -0
- package/dist/config/model.js.map +1 -0
- package/dist/core/errors.js +71 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/git.js +537 -0
- package/dist/core/git.js.map +1 -0
- package/dist/core/json.js +125 -0
- package/dist/core/json.js.map +1 -0
- package/dist/core/layerCoverage.js +141 -0
- package/dist/core/layerCoverage.js.map +1 -0
- package/dist/core/layerGapRemedy.js +62 -0
- package/dist/core/layerGapRemedy.js.map +1 -0
- package/dist/core/nameList.js +23 -0
- package/dist/core/nameList.js.map +1 -0
- package/dist/core/paths.js +153 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/prompt.js +206 -0
- package/dist/core/prompt.js.map +1 -0
- package/dist/core/repoPaths.js +55 -0
- package/dist/core/repoPaths.js.map +1 -0
- package/dist/core/report.js +150 -0
- package/dist/core/report.js.map +1 -0
- package/dist/core/templating.js +88 -0
- package/dist/core/templating.js.map +1 -0
- package/dist/core/writer.js +479 -0
- package/dist/core/writer.js.map +1 -0
- package/dist/daemon/backend.js +180 -0
- package/dist/daemon/backend.js.map +1 -0
- package/dist/daemon/units.js +380 -0
- package/dist/daemon/units.js.map +1 -0
- package/dist/detect/nestedApplication.js +79 -0
- package/dist/detect/nestedApplication.js.map +1 -0
- package/dist/detect/presets.js +2033 -0
- package/dist/detect/presets.js.map +1 -0
- package/dist/detect/signals.js +1368 -0
- package/dist/detect/signals.js.map +1 -0
- package/dist/doctor/checks.js +3530 -0
- package/dist/doctor/checks.js.map +1 -0
- package/dist/generators/claudeContext.js +588 -0
- package/dist/generators/claudeContext.js.map +1 -0
- package/dist/generators/githooks.js +446 -0
- package/dist/generators/githooks.js.map +1 -0
- package/dist/generators/harnessConfig.js +632 -0
- package/dist/generators/harnessConfig.js.map +1 -0
- package/dist/generators/notifications.js +191 -0
- package/dist/generators/notifications.js.map +1 -0
- package/dist/generators/outerLoopScripts.js +165 -0
- package/dist/generators/outerLoopScripts.js.map +1 -0
- package/dist/generators/permissionProfile.js +1172 -0
- package/dist/generators/permissionProfile.js.map +1 -0
- package/dist/generators/projectSettings.js +322 -0
- package/dist/generators/projectSettings.js.map +1 -0
- package/dist/generators/repoRoot.js +417 -0
- package/dist/generators/repoRoot.js.map +1 -0
- package/dist/generators/scripts.js +557 -0
- package/dist/generators/scripts.js.map +1 -0
- package/dist/generators/stateDir.js +221 -0
- package/dist/generators/stateDir.js.map +1 -0
- package/dist/machine/paths.js +111 -0
- package/dist/machine/paths.js.map +1 -0
- package/dist/machine/plugins.js +224 -0
- package/dist/machine/plugins.js.map +1 -0
- package/dist/machine/registry.js +330 -0
- package/dist/machine/registry.js.map +1 -0
- package/package.json +23 -0
- package/scripts/README.md +13 -0
- package/scripts/daemon/launchd.plist.template +59 -0
- package/scripts/daemon/systemd.service.template +58 -0
- package/templates/README.md +15 -0
- package/templates/claude/CLAUDE.md +54 -0
- package/templates/claude/README.md +5 -0
- package/templates/claude/context/api.md +29 -0
- package/templates/claude/context/conventions.md +23 -0
- package/templates/claude/context/data-layer.md +28 -0
- package/templates/claude/context/data-storage.md +29 -0
- package/templates/claude/context/docs-catalog.md +29 -0
- package/templates/claude/context/domain.md +28 -0
- package/templates/claude/context/layer.md +20 -0
- package/templates/claude/context/module.md +30 -0
- package/templates/claude/context/package.md +29 -0
- package/templates/claude/context/presentation.md +32 -0
- package/templates/claude/context/state-slices.md +28 -0
- package/templates/claude/context/tests.md +28 -0
- package/templates/claude/harness-task-offer.md +58 -0
- package/templates/claude/push-notify.env.example +21 -0
- package/templates/claude/qa-accounts.env.example +38 -0
- package/templates/claude/qa_test_scenarios.md +110 -0
- package/templates/claude/settings.autonomous.json +93 -0
- package/templates/claude/settings.autonomous.qa.json +36 -0
- package/templates/githooks/README.md +3 -0
- package/templates/githooks/pre-push +72 -0
- package/templates/repo/README.md +3 -0
- package/templates/repo/gitattributes +16 -0
- package/templates/repo/gitignore +61 -0
- package/templates/repo/gitignore.qa +25 -0
- package/templates/repo/mcp.json +17 -0
- package/templates/scripts/README.md +5 -0
- package/templates/scripts/autonomous-format-stream.sh +95 -0
- package/templates/scripts/autonomous-notify.sh +337 -0
- package/templates/scripts/autonomous-watcher.sh +3087 -0
- package/templates/scripts/cleanup-merged-worktrees.sh +327 -0
- package/templates/scripts/commit-on-branch.sh +288 -0
- package/templates/scripts/create-worktree.sh +360 -0
- package/templates/scripts/deploy.sh +47 -0
- package/templates/scripts/lib/harness-run-lib.sh +1481 -0
- package/templates/scripts/push-branch.sh +140 -0
- package/templates/scripts/refresh-branch.sh +244 -0
- package/templates/scripts/restart-watcher.sh +401 -0
- package/templates/scripts/scratch-run.sh +302 -0
- package/templates/scripts/setup-worktree.sh +262 -0
- package/templates/scripts/start-dev-server.sh +99 -0
- package/templates/scripts/test.sh +50 -0
- package/templates/scripts/typecheck.sh +50 -0
- package/templates/state-dir/README-root.md +13 -0
- package/templates/state-dir/README.md +9 -0
- package/templates/state-dir/architecture_branch_review_point_reviews/README.md +9 -0
- package/templates/state-dir/architecture_branch_reviews/README.md +9 -0
- package/templates/state-dir/architecture_reviews/README.md +9 -0
- package/templates/state-dir/architecture_user_review_reviews/README.md +9 -0
- package/templates/state-dir/autonomous_inbox/README.md +9 -0
- package/templates/state-dir/autonomous_logs/README.md +9 -0
- package/templates/state-dir/branch_statistics/README.md +9 -0
- package/templates/state-dir/business_parity_branch_review_point_reviews/README.md +9 -0
- package/templates/state-dir/business_parity_branch_reviews/README.md +9 -0
- package/templates/state-dir/business_parity_reviews/README.md +9 -0
- package/templates/state-dir/business_parity_user_review_reviews/README.md +9 -0
- package/templates/state-dir/clarification_digests/README.md +9 -0
- package/templates/state-dir/clarifications/README.md +9 -0
- package/templates/state-dir/code_reviews/README.md +9 -0
- package/templates/state-dir/dispatch_additions/README.md +19 -0
- package/templates/state-dir/docs_catalog/README.md +9 -0
- package/templates/state-dir/flow_progress/README.md +9 -0
- package/templates/state-dir/improvement_observations/README.md +19 -0
- package/templates/state-dir/improvement_suggestions.md +29 -0
- package/templates/state-dir/lessons.md +23 -0
- package/templates/state-dir/qa_review_point_reviews/README.md +9 -0
- package/templates/state-dir/qa_reviews/README.md +9 -0
- package/templates/state-dir/review_plan_point_reviews/README.md +9 -0
- package/templates/state-dir/review_plan_reviews/README.md +9 -0
- package/templates/state-dir/scratch/README.md +11 -0
- package/templates/state-dir/skeptic_review_plan_reviews/README.md +9 -0
- package/templates/state-dir/skeptic_review_point_reviews/README.md +9 -0
- package/templates/state-dir/skeptic_reviews/README.md +9 -0
- package/templates/state-dir/story_plans/README.md +9 -0
- package/templates/state-dir/task_plan_point_reviews/README.md +9 -0
- package/templates/state-dir/task_plan_reviews/README.md +9 -0
- package/templates/state-dir/task_plans/README.md +9 -0
- package/templates/state-dir/task_prompts/README.md +9 -0
- package/templates/state-dir/ui_test_plan_reviews/README.md +9 -0
- package/templates/state-dir/ui_test_plans/README.md +9 -0
- package/templates/state-dir/user_review_fix_plan_point_reviews/README.md +9 -0
- 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"
|