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,337 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# autonomous-notify.sh — the one place a run's lifecycle event becomes a message
|
|
3
|
+
# an operator actually sees: a desktop banner on the machine the watcher runs on,
|
|
4
|
+
# plus a best-effort push to wherever that operator is instead.
|
|
5
|
+
#
|
|
6
|
+
# WHY IT EXISTS AT ALL. An unattended run is a background process nobody is
|
|
7
|
+
# looking at. Whatever "your chat finished" mechanism an interactive session has
|
|
8
|
+
# does not reach a detached one reliably, so the watcher's own notification on a
|
|
9
|
+
# lifecycle event is the PRIMARY mechanism rather than a nicety — it is how a
|
|
10
|
+
# completed branch, a parked question or a failed run is noticed at all.
|
|
11
|
+
#
|
|
12
|
+
# THE EVENT VOCABULARY IS A CONTRACT. The watcher's exit classifier and its
|
|
13
|
+
# launch, resume, park and stall paths all call this script with one of these six
|
|
14
|
+
# words, and nothing else may be added here without adding it there:
|
|
15
|
+
#
|
|
16
|
+
# completed the run reached "branch ready for review"
|
|
17
|
+
# parked the run wrote a clarification question and yielded — idle until
|
|
18
|
+
# the answer file lands
|
|
19
|
+
# paused the run honored a PAUSE request and yielded — idle until RESUME
|
|
20
|
+
# failed the run exited non-zero, or its process vanished
|
|
21
|
+
# launched a fresh inbox run was just started
|
|
22
|
+
# resumed the same run is being re-launched — a parked run's answer landed,
|
|
23
|
+
# or a paused run's RESUME landed (the detail argument says which)
|
|
24
|
+
#
|
|
25
|
+
# An unrecognized word is still delivered, under a generic title. A notification
|
|
26
|
+
# helper that refused an event it did not know would silently drop exactly the
|
|
27
|
+
# unusual event an operator most needs to hear about.
|
|
28
|
+
#
|
|
29
|
+
# IT NEVER FAILS ITS CALLER. Every delivery arm is best-effort and the script
|
|
30
|
+
# exits 0 whatever happened — a missing notifier, an unreachable endpoint, a
|
|
31
|
+
# push command that returns non-zero. The only non-zero exit is a usage error,
|
|
32
|
+
# which is a caller defect rather than a delivery failure. Each arm that fails
|
|
33
|
+
# prints ONE line to stderr, which lands in the run log next to the event.
|
|
34
|
+
#
|
|
35
|
+
# EVERY MESSAGE CARRIES THE REPOSITORY SLUG. The title is
|
|
36
|
+
# `[<slug>] <event title> — <branch>`, because one machine watches more than one
|
|
37
|
+
# repository and two of them may well have a branch of the same name. The slug is
|
|
38
|
+
# `HARNESS_REPO_SLUG` when the caller exported it (the watcher does, once, rather
|
|
39
|
+
# than re-deriving it per event), else the shared library's `hr_repo_slug` for
|
|
40
|
+
# this checkout — so a hand invocation from a shell still resolves one. It is the
|
|
41
|
+
# same slug the daemon label and the machine-level usage lane are keyed on.
|
|
42
|
+
#
|
|
43
|
+
# CREDENTIAL RESOLUTION ORDER, STATED ONCE AND IMPLEMENTED ONCE:
|
|
44
|
+
#
|
|
45
|
+
# 1. the machine-local file `${XDG_CONFIG_HOME:-$HOME/.config}/autonomous-sdlc-harness/push.env`
|
|
46
|
+
# 2. the repository's configured `pushEnvPath`, when it is set and the file is
|
|
47
|
+
# there
|
|
48
|
+
#
|
|
49
|
+
# The first that exists wins and the other is not read. Neither present is a
|
|
50
|
+
# GRACEFUL DEGRADE, not an error: desktop delivery still happens and the script
|
|
51
|
+
# still exits 0, with one line saying which paths were looked at. Machine-local
|
|
52
|
+
# comes first because a push token belongs to a person and a machine rather than
|
|
53
|
+
# to a repository — an operator keeps it in one place instead of once per
|
|
54
|
+
# checkout, and it cannot be committed by accident from there. The file is
|
|
55
|
+
# sourced under `set -a` so a value reaches a child process; NO VALUE IS EVER
|
|
56
|
+
# PRINTED, by this script or in a failure line — only paths and key names. A key
|
|
57
|
+
# ALREADY IN THE ENVIRONMENT WINS over the file's value for that key: an operator
|
|
58
|
+
# checking delivery with `HARNESS_PUSH_CMD=… autonomous-notify.sh …` means that
|
|
59
|
+
# command, and a file quietly overriding it would send the check somewhere else.
|
|
60
|
+
#
|
|
61
|
+
# DELIVERY IS TWO INDEPENDENT ARMS, NOT A PRECEDENCE CHAIN.
|
|
62
|
+
#
|
|
63
|
+
# HARNESS_PUSH_CMD a local command. It receives the MESSAGE ON STANDARD INPUT
|
|
64
|
+
# and the title in the environment as `HARNESS_PUSH_TITLE`.
|
|
65
|
+
# It is not a `{title}`/`{message}` template — nothing is
|
|
66
|
+
# substituted into it, which is also what keeps a message
|
|
67
|
+
# containing shell metacharacters from being re-parsed.
|
|
68
|
+
# HARNESS_PUSH_URL an endpoint that accepts a POST. The message is the body,
|
|
69
|
+
# the title rides in a `Title:` header, and a failure retries
|
|
70
|
+
# once with a JSON body so either shape of endpoint works.
|
|
71
|
+
#
|
|
72
|
+
# Both set means BOTH run, in that order, and a failing command must not suppress
|
|
73
|
+
# the POST. This is deliberately not the "command instead of URL" precedence the
|
|
74
|
+
# script was ported from: `cli/templates/claude/push-notify.env.example` is the
|
|
75
|
+
# contract — it is committed, it is what an adopter reads before this file, and
|
|
76
|
+
# it describes the command as running *in addition to* the POST.
|
|
77
|
+
#
|
|
78
|
+
# DESKTOP DELIVERY IS PLATFORM-GATED AND DEGRADES TO NOTHING. The two macOS
|
|
79
|
+
# mechanisms are tried only when `command -v` finds them, which is the platform
|
|
80
|
+
# test: on a machine without them (a Linux host running the systemd backend) both
|
|
81
|
+
# are skipped, no error is printed, and the push arms carry the notification
|
|
82
|
+
# alone. The richer notifier and the built-in one are both fired when both are
|
|
83
|
+
# present — the second is what guarantees a banner when the first is installed
|
|
84
|
+
# but not permitted to post one. Notifications are grouped per repository slug,
|
|
85
|
+
# so one repository's stream of events replaces its own and not another's.
|
|
86
|
+
#
|
|
87
|
+
# WHO RUNS IT. The watcher, on every lifecycle event, and a person by hand for a
|
|
88
|
+
# delivery check. Not a dispatched agent: it has no entry in the generated
|
|
89
|
+
# permission profile. That is a CALLING CONVENTION, NOT A GATE — its basename is
|
|
90
|
+
# not on the script-allowlist guard's `DENY_SCRIPT_BASENAMES`, and that guard
|
|
91
|
+
# grants independently of the profile, so an agent invoking this path under
|
|
92
|
+
# `scriptsDir` is auto-allowed today — but only in a PLAIN invocation: that
|
|
93
|
+
# guard's construct scan withholds the allow, silently, from any command
|
|
94
|
+
# carrying `$(…)`, a backtick, `|`, `<`, a braced form other than a bare
|
|
95
|
+
# `${IDENT}`, or a `>` that is neither a descriptor duplication (`2>&1`, `>&2`,
|
|
96
|
+
# `2>&-`) nor a redirection to the literal `/dev/null` — which includes a
|
|
97
|
+
# `<placeholder>` written into this script's own `detail` argument.
|
|
98
|
+
#
|
|
99
|
+
# Usage:
|
|
100
|
+
# autonomous-notify.sh <event> <branch> [log_path] [detail]
|
|
101
|
+
# event completed | parked | paused | failed | launched | resumed
|
|
102
|
+
# branch the run's branch name
|
|
103
|
+
# log_path optional path to the central run log, shown in the message
|
|
104
|
+
# detail optional one-line extra, e.g. the clarification question's file
|
|
105
|
+
#
|
|
106
|
+
# Exit map a caller can switch on:
|
|
107
|
+
#
|
|
108
|
+
# 0 the event was processed — delivered, partly delivered, or degraded to
|
|
109
|
+
# desktop-only. The stderr lines say which; the status deliberately does not
|
|
110
|
+
# 2 usage error: no event, or no branch. Nothing was delivered
|
|
111
|
+
#
|
|
112
|
+
# REPRO — reproduce any decision by hand, with a recorder standing in for a real
|
|
113
|
+
# push target and nothing real configured:
|
|
114
|
+
#
|
|
115
|
+
# w=$(mktemp -d); printf '#!/bin/sh\ncat > "%s/body"; printf %%s "$HARNESS_PUSH_TITLE" > "%s/title"\n' "$w" "$w" > "$w/rec"
|
|
116
|
+
# chmod +x "$w/rec"
|
|
117
|
+
#
|
|
118
|
+
# both arms HARNESS_PUSH_CMD="$w/rec" HARNESS_PUSH_URL=http://127.0.0.1:9/x \
|
|
119
|
+
# bash <repo>/<scripts_dir>/autonomous-notify.sh completed feat/x; echo $?
|
|
120
|
+
# -> "$w/title" begins `[<slug>] ` and names feat/x, "$w/body"
|
|
121
|
+
# holds the message, ONE stderr line for the failed POST, and 0
|
|
122
|
+
# url only HARNESS_PUSH_URL=http://127.0.0.1:9/x bash …/autonomous-notify.sh failed feat/x
|
|
123
|
+
# -> one stderr line, exit 0
|
|
124
|
+
# neither env -u HARNESS_PUSH_CMD -u HARNESS_PUSH_URL bash …/autonomous-notify.sh paused feat/x
|
|
125
|
+
# -> the degrade line naming the paths looked at, exit 0
|
|
126
|
+
# file order put `HARNESS_PUSH_CMD=<recorder A>` in
|
|
127
|
+
# "${XDG_CONFIG_HOME:-$HOME/.config}/autonomous-sdlc-harness/push.env"
|
|
128
|
+
# and `HARNESS_PUSH_CMD=<recorder B>` in the repository's `pushEnvPath`
|
|
129
|
+
# -> A records; remove that file and B records
|
|
130
|
+
# unknown bash …/autonomous-notify.sh sneezed feat/x; echo $? -> generic title, 0
|
|
131
|
+
# usage bash …/autonomous-notify.sh completed; echo $? -> usage on stderr, 2
|
|
132
|
+
|
|
133
|
+
set -u
|
|
134
|
+
|
|
135
|
+
self="autonomous-notify.sh"
|
|
136
|
+
|
|
137
|
+
EVENT="${1:-}"
|
|
138
|
+
BRANCH="${2:-}"
|
|
139
|
+
LOG_PATH="${3:-}"
|
|
140
|
+
DETAIL="${4:-}"
|
|
141
|
+
|
|
142
|
+
if [ -z "$EVENT" ] || [ -z "$BRANCH" ]; then
|
|
143
|
+
echo "usage: $self <completed|parked|paused|failed|launched|resumed> <branch> [log_path] [detail]" >&2
|
|
144
|
+
exit 2
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
# --- The shared library, reached by a path computed from this script's own
|
|
148
|
+
# location. Unlike every other script in this set, NOT finding it is not a
|
|
149
|
+
# refusal: delivering a notification mutates nothing, and the run whose
|
|
150
|
+
# configuration just became unreadable is exactly the run an operator needs to
|
|
151
|
+
# hear about. Without it the slug falls back to the caller's export, no
|
|
152
|
+
# credential file is looked for, and the pause message loses its state-directory
|
|
153
|
+
# name — the message still goes out.
|
|
154
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
155
|
+
hr_lib="$script_dir/lib/harness-run-lib.sh"
|
|
156
|
+
have_lib=0
|
|
157
|
+
if [ -r "$hr_lib" ]; then
|
|
158
|
+
# shellcheck source=lib/harness-run-lib.sh
|
|
159
|
+
. "$hr_lib"
|
|
160
|
+
have_lib=1
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
# Anchored on the script's own directory rather than on the caller's working
|
|
164
|
+
# directory: the watcher invokes this from wherever a run happens to be, and an
|
|
165
|
+
# anchor that moved with `$PWD` would key a notification on another repository.
|
|
166
|
+
root=""
|
|
167
|
+
if [ "$have_lib" -eq 1 ]; then
|
|
168
|
+
root="$(hr_repo_root "$script_dir")" || root=""
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
slug="${HARNESS_REPO_SLUG:-}"
|
|
172
|
+
if [ -z "$slug" ] && [ -n "$root" ]; then
|
|
173
|
+
slug="$(hr_repo_slug "$root")" || slug=""
|
|
174
|
+
fi
|
|
175
|
+
[ -n "$slug" ] || slug="unknown-repo"
|
|
176
|
+
|
|
177
|
+
# The resume sentinel lives at `<state_dir>/RESUME` inside the run's own working
|
|
178
|
+
# copy, and `<state_dir>` is configured — so the message names the resolved
|
|
179
|
+
# directory, never a remembered one. Unresolvable configuration drops to the
|
|
180
|
+
# generic wording rather than to a guess an operator would then look for in vain.
|
|
181
|
+
state_dir=""
|
|
182
|
+
if [ -n "$root" ]; then
|
|
183
|
+
state_dir="$(hr_state_dir "$root")" || state_dir=""
|
|
184
|
+
fi
|
|
185
|
+
if [ -n "$state_dir" ]; then
|
|
186
|
+
resume_hint="Drop '$state_dir/RESUME' in its working copy to continue."
|
|
187
|
+
else
|
|
188
|
+
resume_hint="Drop a RESUME file in its state directory to continue."
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
case "$EVENT" in
|
|
192
|
+
completed)
|
|
193
|
+
TITLE="[$slug] Run complete — $BRANCH"
|
|
194
|
+
MESSAGE="Branch '$BRANCH' is ready for review."
|
|
195
|
+
;;
|
|
196
|
+
parked)
|
|
197
|
+
TITLE="[$slug] Run parked — $BRANCH"
|
|
198
|
+
MESSAGE="Run '$BRANCH' is waiting for a clarification answer."
|
|
199
|
+
;;
|
|
200
|
+
paused)
|
|
201
|
+
TITLE="[$slug] Run paused — $BRANCH"
|
|
202
|
+
MESSAGE="Run '$BRANCH' honored a PAUSE request and yielded. $resume_hint"
|
|
203
|
+
;;
|
|
204
|
+
resumed)
|
|
205
|
+
TITLE="[$slug] Run resumed — $BRANCH"
|
|
206
|
+
MESSAGE="Run '$BRANCH' resumed."
|
|
207
|
+
;;
|
|
208
|
+
launched)
|
|
209
|
+
TITLE="[$slug] Run launched — $BRANCH"
|
|
210
|
+
MESSAGE="Run '$BRANCH' started."
|
|
211
|
+
;;
|
|
212
|
+
failed)
|
|
213
|
+
TITLE="[$slug] Run failed — $BRANCH"
|
|
214
|
+
MESSAGE="Run '$BRANCH' exited non-zero. Check the log."
|
|
215
|
+
;;
|
|
216
|
+
*)
|
|
217
|
+
TITLE="[$slug] Run ($EVENT) — $BRANCH"
|
|
218
|
+
MESSAGE="Run '$BRANCH': $EVENT."
|
|
219
|
+
;;
|
|
220
|
+
esac
|
|
221
|
+
|
|
222
|
+
[ -n "$DETAIL" ] && MESSAGE="$MESSAGE $DETAIL"
|
|
223
|
+
[ -n "$LOG_PATH" ] && MESSAGE="$MESSAGE Log: $LOG_PATH"
|
|
224
|
+
|
|
225
|
+
# --- Credentials. First candidate that exists wins; the rest are not read. The
|
|
226
|
+
# candidate list itself, and its order, belong to the library — this script must
|
|
227
|
+
# not re-derive either, or the two would disagree about where an operator's
|
|
228
|
+
# settings live.
|
|
229
|
+
push_env=""
|
|
230
|
+
checked=""
|
|
231
|
+
# Held across the source so an explicitly-invoked value survives it — see the
|
|
232
|
+
# header: the environment wins per key, and a file supplies only what the
|
|
233
|
+
# environment did not.
|
|
234
|
+
env_cmd="${HARNESS_PUSH_CMD:-}"
|
|
235
|
+
env_url="${HARNESS_PUSH_URL:-}"
|
|
236
|
+
if [ "$have_lib" -eq 1 ]; then
|
|
237
|
+
while IFS= read -r candidate; do
|
|
238
|
+
[ -n "$candidate" ] || continue
|
|
239
|
+
if [ -n "$checked" ]; then checked="$checked, $candidate"; else checked="$candidate"; fi
|
|
240
|
+
[ -n "$push_env" ] && continue
|
|
241
|
+
[ -f "$candidate" ] && [ -r "$candidate" ] || continue
|
|
242
|
+
# `set -a` so the keys reach the push command's child process; restored
|
|
243
|
+
# immediately, because exporting everything this script defines afterwards
|
|
244
|
+
# would leak its internals into that child too.
|
|
245
|
+
set -a
|
|
246
|
+
# shellcheck disable=SC1090
|
|
247
|
+
. "$candidate"
|
|
248
|
+
set +a
|
|
249
|
+
push_env="$candidate"
|
|
250
|
+
done <<EOF
|
|
251
|
+
$(hr_push_env_files "$root")
|
|
252
|
+
EOF
|
|
253
|
+
fi
|
|
254
|
+
[ -n "$env_cmd" ] && export HARNESS_PUSH_CMD="$env_cmd"
|
|
255
|
+
[ -n "$env_url" ] && export HARNESS_PUSH_URL="$env_url"
|
|
256
|
+
|
|
257
|
+
# --- Desktop notification. Platform-gated by `command -v` (see the header): both
|
|
258
|
+
# mechanisms are macOS-only, both are skipped silently elsewhere, and each is
|
|
259
|
+
# `|| true` so a notifier that is present but refused permission cannot end the
|
|
260
|
+
# script before the push arms run.
|
|
261
|
+
if command -v terminal-notifier >/dev/null 2>&1; then
|
|
262
|
+
terminal-notifier -title "$TITLE" -message "$MESSAGE" -group "autonomous-sdlc-harness.$slug" >/dev/null 2>&1 || true
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
if command -v osascript >/dev/null 2>&1; then
|
|
266
|
+
# Escape backslash and double quote for the AppleScript string literals, in
|
|
267
|
+
# that order: an AppleScript literal treats `\` as an escape too, so escaping
|
|
268
|
+
# the quotes first would re-escape the backslashes this step then adds.
|
|
269
|
+
esc_title="$(printf '%s' "$TITLE" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g')"
|
|
270
|
+
esc_message="$(printf '%s' "$MESSAGE" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g')"
|
|
271
|
+
osascript -e "display notification \"$esc_message\" with title \"$esc_title\"" >/dev/null 2>&1 || true
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
# Minimal JSON string escaping for the fallback body below. Two steps, in this
|
|
275
|
+
# order and no other:
|
|
276
|
+
#
|
|
277
|
+
# 1. translate the control characters JSON forbids unescaped inside a string
|
|
278
|
+
# (tab, CR, LF) to spaces. Translated rather than encoded, because a
|
|
279
|
+
# one-line notification body has no use for any of them and `\t`/`\r`/`\n`
|
|
280
|
+
# escapes would be a second thing to get right for no gain — and to a
|
|
281
|
+
# SPACE rather than deleted, because LF is the reachable one in practice
|
|
282
|
+
# (the detail argument interpolates command output) and deleting it would
|
|
283
|
+
# run the last word of one line into the first of the next.
|
|
284
|
+
# 2. BACKSLASH FIRST, THEN QUOTE. Doing it the other way round re-escapes the
|
|
285
|
+
# backslashes step 2 just added, and a value ending in a backslash would
|
|
286
|
+
# escape the closing quote and corrupt the document.
|
|
287
|
+
#
|
|
288
|
+
# This exists because the body is built with printf rather than jq: jq is not a
|
|
289
|
+
# dependency of this script, which must keep working when the library and
|
|
290
|
+
# everything under it is unavailable.
|
|
291
|
+
json_escape() {
|
|
292
|
+
printf '%s' "${1-}" | tr '\t\r\n' ' ' | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
# --- Push. Two independent arms; neither is conditional on the other's outcome.
|
|
296
|
+
pushed=0
|
|
297
|
+
|
|
298
|
+
if [ -n "${HARNESS_PUSH_CMD:-}" ]; then
|
|
299
|
+
pushed=1
|
|
300
|
+
# The message goes in on STDIN and the title in the environment — the command
|
|
301
|
+
# string is never rewritten, so a message carrying quotes, backticks or a `$`
|
|
302
|
+
# cannot be re-parsed as shell.
|
|
303
|
+
printf '%s\n' "$MESSAGE" | HARNESS_PUSH_TITLE="$TITLE" bash -c "$HARNESS_PUSH_CMD" >/dev/null 2>&1 ||
|
|
304
|
+
echo "$self: HARNESS_PUSH_CMD exited non-zero (the other delivery arms were unaffected)" >&2
|
|
305
|
+
fi
|
|
306
|
+
|
|
307
|
+
if [ -n "${HARNESS_PUSH_URL:-}" ]; then
|
|
308
|
+
pushed=1
|
|
309
|
+
if command -v curl >/dev/null 2>&1; then
|
|
310
|
+
# A plain body first, a JSON body second: the two shapes cover both the
|
|
311
|
+
# "post the text" endpoints and the "post an object" ones without asking the
|
|
312
|
+
# adopter to declare which they have.
|
|
313
|
+
curl -sf -m 10 \
|
|
314
|
+
-H "Title: $TITLE" \
|
|
315
|
+
-d "$MESSAGE" \
|
|
316
|
+
"$HARNESS_PUSH_URL" >/dev/null 2>&1 ||
|
|
317
|
+
curl -sf -m 10 \
|
|
318
|
+
-H "Content-Type: application/json" \
|
|
319
|
+
-d "{\"title\":\"$(json_escape "$TITLE")\",\"message\":\"$(json_escape "$MESSAGE")\"}" \
|
|
320
|
+
"$HARNESS_PUSH_URL" >/dev/null 2>&1 ||
|
|
321
|
+
echo "$self: HARNESS_PUSH_URL POST failed (the other delivery arms were unaffected)" >&2
|
|
322
|
+
else
|
|
323
|
+
echo "$self: curl is not on PATH, so HARNESS_PUSH_URL could not be posted to" >&2
|
|
324
|
+
fi
|
|
325
|
+
fi
|
|
326
|
+
|
|
327
|
+
if [ "$pushed" -eq 0 ]; then
|
|
328
|
+
# Not an error — see the header. The paths are named so an operator can see
|
|
329
|
+
# where the settings were expected; no value is printed.
|
|
330
|
+
if [ -n "$checked" ]; then
|
|
331
|
+
echo "$self: no HARNESS_PUSH_CMD/HARNESS_PUSH_URL set; push skipped (looked in: $checked)" >&2
|
|
332
|
+
else
|
|
333
|
+
echo "$self: no HARNESS_PUSH_CMD/HARNESS_PUSH_URL set and no credential file could be looked for; push skipped" >&2
|
|
334
|
+
fi
|
|
335
|
+
fi
|
|
336
|
+
|
|
337
|
+
exit 0
|