autonomous-sdlc-harness 0.4.2 → 0.6.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/dist/commands/init.js +113 -16
- package/dist/commands/init.js.map +1 -1
- package/dist/config/check.js +28 -6
- package/dist/config/check.js.map +1 -1
- package/dist/config/model.js +64 -5
- package/dist/config/model.js.map +1 -1
- package/dist/core/pluginIdentity.js +2 -0
- package/dist/core/pluginIdentity.js.map +1 -1
- package/dist/core/writer.js +10 -5
- package/dist/core/writer.js.map +1 -1
- package/dist/core/yamlScalar.js +14 -0
- package/dist/core/yamlScalar.js.map +1 -0
- package/dist/doctor/checks.js +454 -23
- package/dist/doctor/checks.js.map +1 -1
- package/dist/generators/githubWorkflows.js +125 -20
- package/dist/generators/githubWorkflows.js.map +1 -1
- package/dist/generators/repoRoot.js +17 -8
- package/dist/generators/repoRoot.js.map +1 -1
- package/dist/remote/githubActions.js +110 -7
- package/dist/remote/githubActions.js.map +1 -1
- package/dist/retrieval/pythonBackend.js +114 -0
- package/dist/retrieval/pythonBackend.js.map +1 -0
- package/dist/retrieval/setup.js +8 -0
- package/dist/retrieval/setup.js.map +1 -1
- package/package.json +1 -1
- package/templates/README.md +1 -1
- package/templates/github/workflows/harness-control.yml +184 -0
- package/templates/github/workflows/harness-resume.yml +9 -0
- package/templates/github/workflows/harness-run.yml +127 -12
- package/templates/github/workflows/harness-trigger.yml +144 -0
- package/templates/repo/gitignore +5 -0
- package/templates/scripts/README.md +1 -1
- package/templates/scripts/autonomous-watcher.sh +121 -249
- package/templates/scripts/create-worktree.sh +52 -10
- package/templates/scripts/docs-search-server.sh +88 -17
- package/templates/scripts/lib/harness-run-lib.sh +614 -14
- package/templates/scripts/remote-run.sh +3684 -144
- package/templates/scripts/scratch-run.sh +54 -73
- package/templates/state-dir/README-root.md +1 -1
- package/templates/state-dir/scratch/README.md +4 -2
- package/templates/state-dir/user_reviews/README.md +2 -2
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# harness-run-lib.sh — the one place every generated outer-loop script resolves
|
|
3
3
|
# the repository it is operating on, reads that repository's
|
|
4
|
-
# `harness.config.json` at run time, answers "is this branch protected?",
|
|
5
|
-
#
|
|
4
|
+
# `harness.config.json` at run time, answers "is this branch protected?",
|
|
5
|
+
# routes an inbox filename to its engine and branch (`hr_inbox_route_var`),
|
|
6
|
+
# derives a branch name from a title (`hr_derive_branch`), judges whether a
|
|
7
|
+
# path lies strictly inside the state directory's scratch directory
|
|
8
|
+
# (`hr_scratch_path_var`), places a dropped
|
|
9
|
+
# artifact in a working copy and commits and pushes it, and derives the
|
|
10
|
+
# anchors (main checkout, work root, worktree directory, repo slug,
|
|
6
11
|
# state-dir paths) the scripts would otherwise each re-derive slightly
|
|
7
12
|
# differently. It also implements the run registry's reads and writes for the
|
|
8
13
|
# scripts that share that registry, and states the remote state bundle's format
|
|
9
|
-
# with the one writer and restorer every remote-execution consumer shares
|
|
14
|
+
# with the one writer and restorer every remote-execution consumer shares, and
|
|
15
|
+
# the GitHub route a job-side notification names beside its local command.
|
|
10
16
|
#
|
|
11
17
|
# WHO SOURCES THIS, AND HOW. Every script in the configured `scriptsDir` that
|
|
12
18
|
# needs this library sources it by a path computed from `${BASH_SOURCE[0]}` —
|
|
@@ -62,8 +68,9 @@
|
|
|
62
68
|
# `.stale.*` move-aside and `.break` mutex while a stale one is broken)
|
|
63
69
|
# and the temp files
|
|
64
70
|
# `.registry.*` in the registry's own directory. Written only by
|
|
65
|
-
# `hr_registry_init`, `hr_registry_set
|
|
66
|
-
# `
|
|
71
|
+
# `hr_registry_init`, `hr_registry_set`, `hr_remote_record_init` (through
|
|
72
|
+
# `hr_registry_set`) and the `hr_registry_lock` / `hr_registry_unlock`
|
|
73
|
+
# pair `hr_registry_set` calls.
|
|
67
74
|
# 3. THE REMOTE STATE BUNDLE writes the files its format lists. Fence: inside
|
|
68
75
|
# `<root>/<state_dir>/` (resolved through `hr_state_dir`), only
|
|
69
76
|
# `autonomous_logs/remote_status.json`, `clarifications/<branch>/`,
|
|
@@ -75,10 +82,21 @@
|
|
|
75
82
|
# of `hr_remote_status_write`. Written only by `hr_remote_status_write`,
|
|
76
83
|
# `hr_remote_bundle_write` and `hr_remote_bundle_restore`, and nothing
|
|
77
84
|
# there but a writer's own failed temp file is ever removed.
|
|
85
|
+
# 4. THE ARTIFACT PLACEMENT writes one artifact into a working copy. Fence:
|
|
86
|
+
# the caller-named `<worktree>/<rel>`, its parent directories and that
|
|
87
|
+
# path's index entry, plus whatever the two caller-named wrappers do.
|
|
88
|
+
# Written only by `hr_place_artifact`, `hr_commit_placed` and
|
|
89
|
+
# `hr_push_landed`.
|
|
90
|
+
#
|
|
91
|
+
# MIRRORS OF `cli/src/remote/githubActions.ts`, which owns these names; a
|
|
92
|
+
# rename there is an edit here, byte for byte:
|
|
93
|
+
# HR_REMOTE_WORKFLOW_RUN_FILE mirrors WORKFLOW_RUN_FILE
|
|
94
|
+
# HR_REMOTE_STATE_ARTIFACT mirrors STATE_ARTIFACT_NAME
|
|
78
95
|
#
|
|
79
96
|
# A caller that calls no `hr_lane_*`, `hr_registry_init`, `hr_registry_set`,
|
|
80
|
-
# `hr_registry_lock`, `hr_registry_unlock`, `hr_remote_status_write`,
|
|
81
|
-
# `hr_remote_bundle_write
|
|
97
|
+
# `hr_remote_record_init`, `hr_registry_lock`, `hr_registry_unlock`, `hr_remote_status_write`,
|
|
98
|
+
# `hr_remote_bundle_write`, `hr_remote_bundle_restore`, `hr_place_artifact`,
|
|
99
|
+
# `hr_commit_placed` or `hr_push_landed` function still gets a library that only reads. The
|
|
82
100
|
# lane's ceilings are the only environment values here that carry policy, because
|
|
83
101
|
# the lane is machine-scoped and has no configuration key to carry them; each is
|
|
84
102
|
# named where it is used. `XDG_STATE_HOME`, `XDG_CONFIG_HOME`, `XDG_CACHE_HOME`,
|
|
@@ -173,7 +191,10 @@
|
|
|
173
191
|
# `mktemp`'s and `jq`'s own stderr on a failed write to the caller, as the
|
|
174
192
|
# watcher's bodies they replaced did — that stream is the watcher's log — and
|
|
175
193
|
# add one line of their own, naming the lock, when `hr_registry_set` cannot
|
|
176
|
-
# take the registry lock.
|
|
194
|
+
# take the registry lock. The artifact placement's three writers likewise leave
|
|
195
|
+
# `mkdir`'s, `cp`'s, `git add`'s and both wrappers' own output on their stdout
|
|
196
|
+
# and stderr for the caller to redirect into its log.
|
|
197
|
+
# That silence is why the lane reports a lock it BROKE through a
|
|
177
198
|
# variable instead of a log line — the caller owns the log.
|
|
178
199
|
#
|
|
179
200
|
# NAMING. Every function is prefixed `hr_`; every variable this file touches
|
|
@@ -181,7 +202,10 @@
|
|
|
181
202
|
# value WITHOUT a command substitution — a `$(…)` forks a subshell, and the
|
|
182
203
|
# watcher calls these on every tick: `HR_CFG_PID`, `HR_CFG_ROOT`,
|
|
183
204
|
# `HR_CFG_STATE`, `HR_CFG_FILE`, `HR_CFG_SCALARS`, `HR_CFG_LISTS`,
|
|
184
|
-
# `HR_CFG_VALUE`, `HR_CFG_COMMAND_KEYS`, `HR_PROTECTED_DEFAULT`,
|
|
205
|
+
# `HR_CFG_VALUE`, `HR_CFG_COMMAND_KEYS`, `HR_PROTECTED_DEFAULT`,
|
|
206
|
+
# `HR_INBOX_KIND`, `HR_INBOX_BRANCH`, the branch derivation's
|
|
207
|
+
# `HR_BRANCH_SLUG_MAX`, `HR_BRANCH_SUFFIX_MAX`, `HR_TAKEN_REMOTE`,
|
|
208
|
+
# `HR_TAKEN_ARTIFACTS` and `HR_TAKEN_WHY`, and the lane's
|
|
185
209
|
# `HR_LANE_RANK`, `HR_LANE_STATE`, `HR_LANE_RESUME_AT`, `HR_LANE_OBSERVED_AT`,
|
|
186
210
|
# `HR_LANE_OBSERVED_REPO`, `HR_LANE_OWNER_SLUG`, `HR_LANE_OWNER_PID`,
|
|
187
211
|
# `HR_LANE_OWNER_AT` and `HR_LANE_BROKEN_OWNER`, and the remote state bundle's
|
|
@@ -572,9 +596,10 @@ hr_config_load() {
|
|
|
572
596
|
# which is what lets an explicitly EMPTY list read as a configured set rather
|
|
573
597
|
# than as an absent one.
|
|
574
598
|
#
|
|
575
|
-
# A `phases.*` value that is not a boolean is emitted as
|
|
576
|
-
# string `"true"` — which `tostring` would otherwise make
|
|
577
|
-
# from `true` — reaches `hr_phase_enabled`
|
|
599
|
+
# A `phases.*` or `docs.retrieval` value that is not a boolean is emitted as
|
|
600
|
+
# `invalid`, so the string `"true"` — which `tostring` would otherwise make
|
|
601
|
+
# indistinguishable from `true` — reaches `hr_phase_enabled` or
|
|
602
|
+
# `hr_docs_retrieval_applies` as a value it refuses (2).
|
|
578
603
|
out=$(jq -n -r '
|
|
579
604
|
def s($k; $v):
|
|
580
605
|
if $v == null then empty
|
|
@@ -609,6 +634,9 @@ hr_config_load() {
|
|
|
609
634
|
s("phases.qa"; try (.phases.qa | if type == "boolean" or . == null then . else "invalid" end) catch null),
|
|
610
635
|
s("phases.docs"; try (.phases.docs | if type == "boolean" or . == null then . else "invalid" end) catch null),
|
|
611
636
|
s("execution.target"; try .execution.target catch null),
|
|
637
|
+
s("docs.retrievalBackend"; try .docs.retrievalBackend catch null),
|
|
638
|
+
s("docs.retrieval"; try (.docs.retrieval | if type == "boolean" or . == null then . else "invalid" end) catch null),
|
|
639
|
+
s("forge"; try .forge catch null),
|
|
612
640
|
s("protectedBranches.present";
|
|
613
641
|
try (if (.protectedBranches | type) == "array" then "1" else null end) catch null),
|
|
614
642
|
l("protectedBranches"; try .protectedBranches catch null)
|
|
@@ -875,6 +903,115 @@ hr_execution_target() {
|
|
|
875
903
|
return 2
|
|
876
904
|
}
|
|
877
905
|
|
|
906
|
+
# `forge` — which code-hosting platform the flow integrates with: `github`,
|
|
907
|
+
# `gitlab` or `none`. THE ONE READER OF THE KEY IN THIS FAMILY, and the shell
|
|
908
|
+
# mirror of `cli/src/config/model.ts` → `FORGE_KINDS`: change the enum there and
|
|
909
|
+
# here together. 1 — printing nothing — when the key is absent: "not yet
|
|
910
|
+
# decided", which is not `none`, and the schema withholds a default on purpose.
|
|
911
|
+
# 2 — printing nothing — when the configuration is unresolvable or the value is
|
|
912
|
+
# outside the enum, a refusal rather than a guess.
|
|
913
|
+
hr_forge() {
|
|
914
|
+
local root="${1-}"
|
|
915
|
+
hr_config_load "$root" || return 2
|
|
916
|
+
hr_cfg_scalar_var "forge" || return 1
|
|
917
|
+
case "$HR_CFG_VALUE" in
|
|
918
|
+
github|gitlab|none)
|
|
919
|
+
printf '%s\n' "$HR_CFG_VALUE"
|
|
920
|
+
return 0
|
|
921
|
+
;;
|
|
922
|
+
esac
|
|
923
|
+
return 2
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
# Whether the docs phase's retrieval step runs: `phases.docs` and
|
|
927
|
+
# `docs.retrieval` both `true`. The shell mirror of `cli/src/config/model.ts` →
|
|
928
|
+
# `retrievalApplies`: change the predicate there and here together. PRINTS
|
|
929
|
+
# NOTHING: the answer is the status, as `hr_phase_enabled`'s is. 0 = both
|
|
930
|
+
# `true`; 1 = either `false` or unset; 2 = the configuration is unresolvable or
|
|
931
|
+
# either value is not a boolean — refused rather than guessed about.
|
|
932
|
+
hr_docs_retrieval_applies() {
|
|
933
|
+
local root="${1-}" status
|
|
934
|
+
hr_phase_enabled "$root" docs
|
|
935
|
+
status=$?
|
|
936
|
+
[ "$status" -eq 0 ] || return "$status"
|
|
937
|
+
hr_cfg_scalar_var "docs.retrieval" || return 1
|
|
938
|
+
case "$HR_CFG_VALUE" in
|
|
939
|
+
true) return 0 ;;
|
|
940
|
+
false) return 1 ;;
|
|
941
|
+
esac
|
|
942
|
+
return 2
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
# `docs.retrievalBackend` — which runtime the docs-retrieval index uses:
|
|
946
|
+
# `typescript` or `python`. THE ONE READER OF THE KEY IN THIS FAMILY, and the
|
|
947
|
+
# shell mirror of `cli/src/config/model.ts` → `RETRIEVAL_BACKENDS` /
|
|
948
|
+
# `DEFAULT_RETRIEVAL_BACKEND`: change the enum there and here together. Schema
|
|
949
|
+
# default `typescript`, so an absent key prints `typescript` and 1 is never
|
|
950
|
+
# returned; a `docs` parent of the wrong type (`"docs": "x"`) reads as absent
|
|
951
|
+
# too, because `hr_config_load`'s `try … catch` nulls that key alone — refusing
|
|
952
|
+
# that shape is the schema's job. 2 — printing nothing — when the configuration
|
|
953
|
+
# is unresolvable or the value is outside the enum, a refusal rather than a
|
|
954
|
+
# guess. It does NOT consult `phases.docs` / `docs.retrieval`: the key is read
|
|
955
|
+
# only inside the gate, so a caller asks `hr_docs_retrieval_applies` first and
|
|
956
|
+
# calls this only on its status 0.
|
|
957
|
+
hr_docs_retrieval_backend() {
|
|
958
|
+
local root="${1-}"
|
|
959
|
+
hr_config_load "$root" || return 2
|
|
960
|
+
if ! hr_cfg_scalar_var "docs.retrievalBackend"; then
|
|
961
|
+
printf 'typescript\n'
|
|
962
|
+
return 0
|
|
963
|
+
fi
|
|
964
|
+
case "$HR_CFG_VALUE" in
|
|
965
|
+
typescript|python)
|
|
966
|
+
printf '%s\n' "$HR_CFG_VALUE"
|
|
967
|
+
return 0
|
|
968
|
+
;;
|
|
969
|
+
esac
|
|
970
|
+
return 2
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
# ---------------------------------------------------------------------------
|
|
974
|
+
# Inbox routing — the one owner of the drop filename patterns.
|
|
975
|
+
# ---------------------------------------------------------------------------
|
|
976
|
+
|
|
977
|
+
# Route one inbox filename: set `HR_INBOX_KIND` (`task` | `user_review` |
|
|
978
|
+
# `docs`) and `HR_INBOX_BRANCH`, and return 0; on no match return 1 with both
|
|
979
|
+
# empty. Takes a basename, not a path.
|
|
980
|
+
#
|
|
981
|
+
# The task-prompt pattern is tested FIRST (the more specific suffix), but the
|
|
982
|
+
# anchored SUFFIX regexes are mutually exclusive by construction: a filename
|
|
983
|
+
# cannot end in more than one of `_task_prompt.md` / `_review[_<n>].md` /
|
|
984
|
+
# `_docs.md`, so a branch whose own name contains `review` or `task_prompt`
|
|
985
|
+
# cannot be mis-routed — `foo_review_task_prompt.md` is the task engine on branch
|
|
986
|
+
# `foo_review`, and `foo_task_prompt_review.md` is the review engine on branch
|
|
987
|
+
# `foo_task_prompt`. POSIX leftmost-longest matching of the greedy `(.+)` derives
|
|
988
|
+
# the right branch from a round-suffixed name: `foo_review_2.md` -> branch `foo`
|
|
989
|
+
# (the `_2` is consumed by the optional `(_[0-9]+)?`), while
|
|
990
|
+
# `foo_review_2_review.md` -> branch `foo_review_2`. THE WATCHER DERIVES ONLY THE
|
|
991
|
+
# BRANCH, never the round: the engine resolves the latest round itself, inside
|
|
992
|
+
# the working copy, which is why nothing here has to remember one.
|
|
993
|
+
hr_inbox_route_var() {
|
|
994
|
+
local fname="${1-}" branch
|
|
995
|
+
HR_INBOX_KIND=""
|
|
996
|
+
HR_INBOX_BRANCH=""
|
|
997
|
+
[ -n "$fname" ] || return 1
|
|
998
|
+
branch="$(printf '%s' "$fname" | sed -nE 's/^(.+)_task_prompt\.md$/\1/p')"
|
|
999
|
+
if [ -n "$branch" ]; then
|
|
1000
|
+
HR_INBOX_KIND="task"
|
|
1001
|
+
else
|
|
1002
|
+
branch="$(printf '%s' "$fname" | sed -nE 's/^(.+)_review(_[0-9]+)?\.md$/\1/p')"
|
|
1003
|
+
if [ -n "$branch" ]; then
|
|
1004
|
+
HR_INBOX_KIND="user_review"
|
|
1005
|
+
else
|
|
1006
|
+
branch="$(printf '%s' "$fname" | sed -nE 's/^(.+)_docs\.md$/\1/p')"
|
|
1007
|
+
[ -n "$branch" ] || return 1
|
|
1008
|
+
HR_INBOX_KIND="docs"
|
|
1009
|
+
fi
|
|
1010
|
+
fi
|
|
1011
|
+
HR_INBOX_BRANCH="$branch"
|
|
1012
|
+
return 0
|
|
1013
|
+
}
|
|
1014
|
+
|
|
878
1015
|
# ---------------------------------------------------------------------------
|
|
879
1016
|
# The protected-branch trichotomy.
|
|
880
1017
|
# ---------------------------------------------------------------------------
|
|
@@ -1055,6 +1192,92 @@ hr_state_path() {
|
|
|
1055
1192
|
fi
|
|
1056
1193
|
}
|
|
1057
1194
|
|
|
1195
|
+
# Judge whether <path> resolves STRICTLY INSIDE `<root>/<state_dir>/scratch/`.
|
|
1196
|
+
# Usage: hr_scratch_path_var <root> <path> [<base>]
|
|
1197
|
+
#
|
|
1198
|
+
# Two callers, differing only in what they do with an accepted path:
|
|
1199
|
+
# `scratch-run.sh` executes a file there and `remote-run.sh discard` removes a
|
|
1200
|
+
# directory there. So this reports a keyword in `HR_SCRATCH_WHY` and leaves every
|
|
1201
|
+
# message and exit code to its caller.
|
|
1202
|
+
#
|
|
1203
|
+
# The rule, stated once:
|
|
1204
|
+
# - `..` is refused ANYWHERE in the argument, not only as a whole segment —
|
|
1205
|
+
# strictly stronger, and free, because nothing in that directory depends on
|
|
1206
|
+
# a file's name. Then any character outside `A-Za-z0-9._/-` is refused. Both
|
|
1207
|
+
# tests run on <path> as the caller received it, before anything resolves.
|
|
1208
|
+
# - A relative <path> resolves against <base> when given, else <root>; an
|
|
1209
|
+
# absolute one is taken as given.
|
|
1210
|
+
# - Both sides of the comparison are resolved PHYSICALLY (`cd … && pwd -P`), so
|
|
1211
|
+
# a symlinked directory planted inside the scratch tree cannot widen the
|
|
1212
|
+
# fence. The target's parent must be the scratch directory or beneath it, and
|
|
1213
|
+
# a basename of `.` (the scratch directory itself) is refused.
|
|
1214
|
+
# - A target that is itself a symlink is refused rather than followed: its
|
|
1215
|
+
# destination is outside this judgement. The target need not exist.
|
|
1216
|
+
#
|
|
1217
|
+
# Clears the other four, then sets: `HR_SCRATCH_SUBDIR` (`scratch`, mirroring the `scratch` row
|
|
1218
|
+
# of `STATE_DIR_ENTRIES` in `cli/src/generators/stateDir.ts`), `HR_SCRATCH_DIR`
|
|
1219
|
+
# (physical), `HR_SCRATCH_PARENT` (physical), `HR_SCRATCH_TARGET`
|
|
1220
|
+
# (`<parent>/<basename>`, the path a caller acts on) and `HR_SCRATCH_WHY`.
|
|
1221
|
+
# Status / `HR_SCRATCH_WHY`:
|
|
1222
|
+
# 0 accepted (WHY empty)
|
|
1223
|
+
# 1 refused — `dotdot`, `charset`, `outside`, `itself`, `symlink`
|
|
1224
|
+
# 2 the target's parent does not resolve — `no-parent`
|
|
1225
|
+
# 3 the scratch directory cannot be located — `no-config`, `no-scratch`
|
|
1226
|
+
# 4 <root> or <path> is empty — `usage`
|
|
1227
|
+
hr_scratch_path_var() {
|
|
1228
|
+
local root="${1-}" path="${2-}" base="${3-}" scratch candidate name
|
|
1229
|
+
HR_SCRATCH_DIR=""
|
|
1230
|
+
HR_SCRATCH_PARENT=""
|
|
1231
|
+
HR_SCRATCH_TARGET=""
|
|
1232
|
+
HR_SCRATCH_WHY=""
|
|
1233
|
+
HR_SCRATCH_SUBDIR='scratch'
|
|
1234
|
+
if [ -z "$root" ] || [ -z "$path" ]; then
|
|
1235
|
+
HR_SCRATCH_WHY="usage"
|
|
1236
|
+
return 4
|
|
1237
|
+
fi
|
|
1238
|
+
case "$path" in
|
|
1239
|
+
*..*) HR_SCRATCH_WHY="dotdot"; return 1 ;;
|
|
1240
|
+
esac
|
|
1241
|
+
case "$path" in
|
|
1242
|
+
*[!A-Za-z0-9._/-]*) HR_SCRATCH_WHY="charset"; return 1 ;;
|
|
1243
|
+
esac
|
|
1244
|
+
scratch=$(hr_state_path "$root" "$HR_SCRATCH_SUBDIR")
|
|
1245
|
+
if [ "$?" -ne 0 ] || [ -z "$scratch" ]; then
|
|
1246
|
+
HR_SCRATCH_WHY="no-config"
|
|
1247
|
+
return 3
|
|
1248
|
+
fi
|
|
1249
|
+
HR_SCRATCH_DIR=$(cd "$scratch" 2>/dev/null && pwd -P)
|
|
1250
|
+
if [ -z "$HR_SCRATCH_DIR" ]; then
|
|
1251
|
+
HR_SCRATCH_WHY="no-scratch"
|
|
1252
|
+
return 3
|
|
1253
|
+
fi
|
|
1254
|
+
[ -n "$base" ] || base="$root"
|
|
1255
|
+
case "$path" in
|
|
1256
|
+
/*) candidate="$path" ;;
|
|
1257
|
+
*) candidate="${base%/}/$path" ;;
|
|
1258
|
+
esac
|
|
1259
|
+
HR_SCRATCH_PARENT=$(cd "$(dirname "$candidate")" 2>/dev/null && pwd -P)
|
|
1260
|
+
if [ -z "$HR_SCRATCH_PARENT" ]; then
|
|
1261
|
+
HR_SCRATCH_WHY="no-parent"
|
|
1262
|
+
return 2
|
|
1263
|
+
fi
|
|
1264
|
+
case "$HR_SCRATCH_PARENT" in
|
|
1265
|
+
"$HR_SCRATCH_DIR" | "$HR_SCRATCH_DIR"/*) ;;
|
|
1266
|
+
*) HR_SCRATCH_WHY="outside"; return 1 ;;
|
|
1267
|
+
esac
|
|
1268
|
+
name=$(basename "$candidate")
|
|
1269
|
+
if [ "$name" = "." ]; then
|
|
1270
|
+
HR_SCRATCH_WHY="itself"
|
|
1271
|
+
return 1
|
|
1272
|
+
fi
|
|
1273
|
+
HR_SCRATCH_TARGET="$HR_SCRATCH_PARENT/$name"
|
|
1274
|
+
if [ -L "$HR_SCRATCH_TARGET" ]; then
|
|
1275
|
+
HR_SCRATCH_WHY="symlink"
|
|
1276
|
+
return 1
|
|
1277
|
+
fi
|
|
1278
|
+
return 0
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1058
1281
|
# The machine-local settings directory — one place an operator keeps values that
|
|
1059
1282
|
# vary per machine rather than per repository, which is why it is not a config
|
|
1060
1283
|
# key and not a repository file. Return 1 when there is no home to anchor it to.
|
|
@@ -1319,12 +1542,349 @@ hr_registry_branches() {
|
|
|
1319
1542
|
jq -r '.runs | keys[]' "$file" 2>/dev/null
|
|
1320
1543
|
}
|
|
1321
1544
|
|
|
1545
|
+
# hr_remote_record_init <file> <branch> <worktree> <log_path> <engine>
|
|
1546
|
+
# The fields a remote run's record starts with — the one list, written by the
|
|
1547
|
+
# watcher's `launch_remote_run` — in one write, so no reader sees half of them. `status` is left to the caller, which writes it only
|
|
1548
|
+
# once its run exists. 1 when the write failed.
|
|
1549
|
+
hr_remote_record_init() {
|
|
1550
|
+
local file="${1-}" branch="${2-}"
|
|
1551
|
+
hr_registry_set "$file" "$branch" \
|
|
1552
|
+
worktree "${3-}" log_path "${4-}" engine "${5-}" \
|
|
1553
|
+
execution github-actions \
|
|
1554
|
+
started_at "$(date '+%Y-%m-%dT%H:%M:%S')" \
|
|
1555
|
+
pid "" remote_dispatched_at "" \
|
|
1556
|
+
stall_restarts 0 stall_warned "" stall_killing "" \
|
|
1557
|
+
paused_by "" usage_resume_at "" \
|
|
1558
|
+
resume_kind "" park_loop_cycles 0
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
# ---------------------------------------------------------------------------
|
|
1562
|
+
# DERIVING A BRANCH NAME FROM A TITLE.
|
|
1563
|
+
#
|
|
1564
|
+
# THE RULE. A title becomes a branch name by a fixed fold, with no model and no
|
|
1565
|
+
# confirmation step: lowercase A–Z, turn every run of characters outside
|
|
1566
|
+
# `[a-z0-9]` into one `_`, trim `_` from both ends, cut to `HR_BRANCH_SLUG_MAX`
|
|
1567
|
+
# and trim a trailing `_` the cut exposed. An empty result takes the caller's
|
|
1568
|
+
# <fallback> (`issue_<number>` for an issue, `task_<run id>` for a dispatch). A
|
|
1569
|
+
# taken name takes the lowest free `<name>_<n>`, `_2` through
|
|
1570
|
+
# `HR_BRANCH_SUFFIX_MAX`; the first branch carries no suffix (`Version bump` →
|
|
1571
|
+
# `version_bump`), and `_2` reads as "the second".
|
|
1572
|
+
#
|
|
1573
|
+
# - The fold is ASCII-only under `LC_ALL=C`, so `é` is a separator, never a
|
|
1574
|
+
# letter — `hr_repo_slug`'s precedent. A locale-dependent fold would derive
|
|
1575
|
+
# different names on different runners; a lowercase ASCII name passes every
|
|
1576
|
+
# `git check-ref-format` rule and cannot collide by case on macOS or Windows.
|
|
1577
|
+
# - The cap is 60, cut before the suffix. The name becomes a working-copy
|
|
1578
|
+
# directory component (`<projectName>-<branch>`) and prefixes artifact names
|
|
1579
|
+
# (`<branch>_task_prompt.md`); 60 keeps each far under a 255-byte file-name
|
|
1580
|
+
# limit and readable in the Actions run list. GitHub documents no ref limit.
|
|
1581
|
+
#
|
|
1582
|
+
# THIS SECTION ONLY READS. It fetches nothing and creates no branch or file; a
|
|
1583
|
+
# caller that wants `origin` fresh fetches first. Given a <gh_cli>, it also asks
|
|
1584
|
+
# GitHub one read per candidate — the run workflow's runs under that name — and
|
|
1585
|
+
# still writes nothing. A registry is read only when it already exists, because
|
|
1586
|
+
# `hr_registry_get` creates an absent one.
|
|
1587
|
+
# ---------------------------------------------------------------------------
|
|
1588
|
+
|
|
1589
|
+
# The two limits the rule above names.
|
|
1590
|
+
hr_branch_limits_var() {
|
|
1591
|
+
HR_BRANCH_SLUG_MAX=60
|
|
1592
|
+
HR_BRANCH_SUFFIX_MAX=99
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
# hr_branch_slug <text> — print the slug and return 0; print nothing and return
|
|
1596
|
+
# 1 when the fold leaves nothing (`🚀🚀`).
|
|
1597
|
+
hr_branch_slug() {
|
|
1598
|
+
# `[!a-z0-9]` is a collation range: under a UTF-8 locale it would keep `é`.
|
|
1599
|
+
local LC_ALL=C
|
|
1600
|
+
local text="${1-}" slug
|
|
1601
|
+
hr_branch_limits_var
|
|
1602
|
+
slug=$(printf '%s' "$text" | LC_ALL=C tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
|
|
1603
|
+
slug=${slug//[!a-z0-9]/_}
|
|
1604
|
+
while :; do
|
|
1605
|
+
case "$slug" in
|
|
1606
|
+
*__*) slug=${slug//__/_} ;;
|
|
1607
|
+
*) break ;;
|
|
1608
|
+
esac
|
|
1609
|
+
done
|
|
1610
|
+
slug=${slug#_}
|
|
1611
|
+
slug=${slug%_}
|
|
1612
|
+
if [ "${#slug}" -gt "$HR_BRANCH_SLUG_MAX" ]; then
|
|
1613
|
+
slug=${slug:0:$HR_BRANCH_SLUG_MAX}
|
|
1614
|
+
slug=${slug%_}
|
|
1615
|
+
fi
|
|
1616
|
+
[ -n "$slug" ] || return 1
|
|
1617
|
+
printf '%s\n' "$slug"
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
# Read, once, the two listings every `taken` judgement compares against:
|
|
1621
|
+
# `HR_TAKEN_REMOTE` — each branch on `origin`, lowercased — and
|
|
1622
|
+
# `HR_TAKEN_ARTIFACTS` — the basename of every file and directory under
|
|
1623
|
+
# `<state_dir>` on `origin/<defaultBranch>`; one per line in both. 0 when both
|
|
1624
|
+
# were read; 2, with `HR_TAKEN_WHY` naming which, when either could not be.
|
|
1625
|
+
hr_branch_taken_lists_var() {
|
|
1626
|
+
local LC_ALL=C
|
|
1627
|
+
local root="${1-}" heads state default tree line tab
|
|
1628
|
+
tab=$(printf '\t')
|
|
1629
|
+
HR_TAKEN_REMOTE=""
|
|
1630
|
+
HR_TAKEN_ARTIFACTS=""
|
|
1631
|
+
HR_TAKEN_WHY=""
|
|
1632
|
+
if ! heads=$(git -C "$root" ls-remote --heads origin 2>/dev/null); then
|
|
1633
|
+
HR_TAKEN_WHY="the branches on origin could not be listed"
|
|
1634
|
+
return 2
|
|
1635
|
+
fi
|
|
1636
|
+
while IFS= read -r line; do
|
|
1637
|
+
line=${line#*"$tab"refs/heads/}
|
|
1638
|
+
[ -n "$line" ] || continue
|
|
1639
|
+
HR_TAKEN_REMOTE="$HR_TAKEN_REMOTE$line
|
|
1640
|
+
"
|
|
1641
|
+
done <<EOF
|
|
1642
|
+
$heads
|
|
1643
|
+
EOF
|
|
1644
|
+
HR_TAKEN_REMOTE=$(printf '%s' "$HR_TAKEN_REMOTE" | LC_ALL=C tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
|
|
1645
|
+
|
|
1646
|
+
if ! state=$(hr_state_dir "$root") || ! default=$(hr_default_branch "$root"); then
|
|
1647
|
+
HR_TAKEN_WHY="the configuration could not be read"
|
|
1648
|
+
return 2
|
|
1649
|
+
fi
|
|
1650
|
+
if ! git -C "$root" rev-parse --verify --quiet "refs/remotes/origin/$default^{commit}" >/dev/null 2>&1; then
|
|
1651
|
+
HR_TAKEN_WHY="origin/$default is not present"
|
|
1652
|
+
return 2
|
|
1653
|
+
fi
|
|
1654
|
+
if ! tree=$(git -C "$root" ls-tree -r -t --name-only "refs/remotes/origin/$default" -- "$state/" 2>/dev/null); then
|
|
1655
|
+
HR_TAKEN_WHY="the tree of origin/$default could not be read"
|
|
1656
|
+
return 2
|
|
1657
|
+
fi
|
|
1658
|
+
while IFS= read -r line; do
|
|
1659
|
+
[ -n "$line" ] || continue
|
|
1660
|
+
HR_TAKEN_ARTIFACTS="$HR_TAKEN_ARTIFACTS${line##*/}
|
|
1661
|
+
"
|
|
1662
|
+
done <<EOF
|
|
1663
|
+
$tree
|
|
1664
|
+
EOF
|
|
1665
|
+
return 0
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
# hr_branch_run_history <root> <gh_cli> <name> — 0 when the run workflow
|
|
1669
|
+
# (`HR_REMOTE_WORKFLOW_RUN_FILE`) lists a run under <name>, 1 when it lists
|
|
1670
|
+
# none, 2 when <gh_cli> cannot run, exits non-zero, or answers anything but a
|
|
1671
|
+
# JSON array. Sets no `HR_` variable: the names are assigned inside the
|
|
1672
|
+
# subshell that `cd`s to <root>.
|
|
1673
|
+
#
|
|
1674
|
+
# GitHub keeps a deleted branch's runs listed under its name, and a later run
|
|
1675
|
+
# of that name inherits them: Gate 12 round 5 found run `36569531374` of the
|
|
1676
|
+
# deleted `feat_invoices` (`docs/development.md` → Gate 12 → Round 5, finding
|
|
1677
|
+
# 1). One bounded read per candidate, not one listing of every branch, because
|
|
1678
|
+
# a bounded all-branch listing could miss an old name. Bash 3.2 and jq 1.5.
|
|
1679
|
+
hr_branch_run_history() {
|
|
1680
|
+
local root="${1-}" gh_cli="${2-}" name="${3-}" answer verdict
|
|
1681
|
+
[ -n "$root" ] && [ -n "$gh_cli" ] && [ -n "$name" ] || return 2
|
|
1682
|
+
answer=$(cd "$root" 2>/dev/null && hr_remote_names_var &&
|
|
1683
|
+
"$gh_cli" run list --workflow "$HR_REMOTE_WORKFLOW_RUN_FILE" --branch "$name" --limit 1 --json databaseId 2>/dev/null) || return 2
|
|
1684
|
+
verdict=$(printf '%s' "$answer" | jq -r 'if type == "array" then (if length > 0 then "history" else "none" end) else "other" end' 2>/dev/null) || return 2
|
|
1685
|
+
case "$verdict" in
|
|
1686
|
+
history) return 0 ;;
|
|
1687
|
+
none) return 1 ;;
|
|
1688
|
+
esac
|
|
1689
|
+
return 2
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
# Judge <name> against the listings `hr_branch_taken_lists_var` last read, then,
|
|
1693
|
+
# given a <gh_cli>, against the run workflow's history — last, so the free local
|
|
1694
|
+
# checks answer first. The answers and `HR_TAKEN_WHY` are
|
|
1695
|
+
# `hr_branch_name_taken`'s.
|
|
1696
|
+
hr_branch_taken_judge() {
|
|
1697
|
+
local LC_ALL=C
|
|
1698
|
+
local root="${1-}" name="${2-}" registry="${3-}" gh_cli="${4-}" lower status nl
|
|
1699
|
+
nl='
|
|
1700
|
+
'
|
|
1701
|
+
HR_TAKEN_WHY=""
|
|
1702
|
+
status=0
|
|
1703
|
+
hr_branch_is_protected "$root" "$name" || status=$?
|
|
1704
|
+
case "$status" in
|
|
1705
|
+
0) HR_TAKEN_WHY="a protected branch"; return 0 ;;
|
|
1706
|
+
2) HR_TAKEN_WHY="the protected branches could not be resolved"; return 2 ;;
|
|
1707
|
+
esac
|
|
1708
|
+
lower=$(printf '%s' "$name" | LC_ALL=C tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
|
|
1709
|
+
case "$nl$HR_TAKEN_REMOTE$nl" in
|
|
1710
|
+
*"$nl$lower$nl"*) HR_TAKEN_WHY="a branch on origin"; return 0 ;;
|
|
1711
|
+
esac
|
|
1712
|
+
if git -C "$root" show-ref --verify --quiet "refs/heads/$name" 2>/dev/null; then
|
|
1713
|
+
HR_TAKEN_WHY="a local branch"
|
|
1714
|
+
return 0
|
|
1715
|
+
fi
|
|
1716
|
+
case "$nl$HR_TAKEN_ARTIFACTS" in
|
|
1717
|
+
*"$nl$name$nl"* | *"$nl${name}_task_prompt.md$nl"* | *"$nl${name}_story_plan.md$nl"* | *"$nl${name}_docs.md$nl"*)
|
|
1718
|
+
HR_TAKEN_WHY="a run's artifacts on the default branch"
|
|
1719
|
+
return 0
|
|
1720
|
+
;;
|
|
1721
|
+
esac
|
|
1722
|
+
if [ -n "$registry" ] && [ -f "$registry" ] && [ -n "$(hr_registry_get "$registry" "$name" branch)" ]; then
|
|
1723
|
+
HR_TAKEN_WHY="a run registry record"
|
|
1724
|
+
return 0
|
|
1725
|
+
fi
|
|
1726
|
+
if [ -n "$gh_cli" ]; then
|
|
1727
|
+
status=0
|
|
1728
|
+
hr_branch_run_history "$root" "$gh_cli" "$name" || status=$?
|
|
1729
|
+
case "$status" in
|
|
1730
|
+
0) HR_TAKEN_WHY="a run of the run workflow listed under that name"; return 0 ;;
|
|
1731
|
+
1) ;;
|
|
1732
|
+
*) HR_TAKEN_WHY="the run history of $name could not be listed"; return 2 ;;
|
|
1733
|
+
esac
|
|
1734
|
+
fi
|
|
1735
|
+
return 1
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
# hr_branch_name_taken <root> <name> [<registry>] [<gh_cli>] — 0 taken, 1 free,
|
|
1739
|
+
# 2 cannot tell. Sets `HR_TAKEN_WHY` to a short phrase naming the collision or
|
|
1740
|
+
# the failure. A caller with no registry passes `""` before <gh_cli>. Taken: a
|
|
1741
|
+
# protected name; a branch on `origin`, compared case-insensitively; a local
|
|
1742
|
+
# branch; under `<state_dir>` on `origin/<defaultBranch>`, a directory named
|
|
1743
|
+
# <name> or a file `<name>_task_prompt.md`, `<name>_story_plan.md` or
|
|
1744
|
+
# `<name>_docs.md` — which a merged and deleted branch still leaves; a record in
|
|
1745
|
+
# an existing <registry>; given a <gh_cli>, any run of the run workflow listed
|
|
1746
|
+
# under <name> (`hr_branch_run_history`), whose listing failing is a 2.
|
|
1747
|
+
hr_branch_name_taken() {
|
|
1748
|
+
local root="${1-}" name="${2-}" registry="${3-}" gh_cli="${4-}"
|
|
1749
|
+
HR_TAKEN_WHY=""
|
|
1750
|
+
if [ -z "$root" ] || [ -z "$name" ]; then
|
|
1751
|
+
HR_TAKEN_WHY="no branch name to judge"
|
|
1752
|
+
return 2
|
|
1753
|
+
fi
|
|
1754
|
+
hr_config_load "$root" || :
|
|
1755
|
+
hr_branch_taken_lists_var "$root" || return 2
|
|
1756
|
+
hr_branch_taken_judge "$root" "$name" "$registry" "$gh_cli"
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
# hr_derive_branch <root> <text> <fallback> [<registry>] [<gh_cli>] — print the
|
|
1760
|
+
# derived name and return 0; return 2, printing nothing, when a `taken`
|
|
1761
|
+
# judgement could not tell or no base routes back to itself; return 3, printing
|
|
1762
|
+
# nothing, when every suffix through `HR_BRANCH_SUFFIX_MAX` is taken. Never a
|
|
1763
|
+
# guessed name. <gh_cli> reaches every judgement as `hr_branch_name_taken`'s.
|
|
1764
|
+
#
|
|
1765
|
+
# THE BASE MUST ROUTE BACK TO ITSELF: `hr_inbox_route_var` on each drop filename
|
|
1766
|
+
# a run of that name produces has to give the base as its branch, so no derived
|
|
1767
|
+
# name makes the inbox patterns ambiguous. A base that does not is replaced by
|
|
1768
|
+
# <fallback> once.
|
|
1769
|
+
hr_derive_branch() {
|
|
1770
|
+
local root="${1-}" text="${2-}" fallback="${3-}" registry="${4-}" gh_cli="${5-}"
|
|
1771
|
+
local base="" candidate suffix routes n status
|
|
1772
|
+
hr_branch_limits_var
|
|
1773
|
+
candidate=$(hr_branch_slug "$text") || candidate="$fallback"
|
|
1774
|
+
for candidate in "$candidate" "$fallback"; do
|
|
1775
|
+
[ -n "$candidate" ] || continue
|
|
1776
|
+
routes=0
|
|
1777
|
+
for suffix in _task_prompt.md _review.md _review_2.md _docs.md; do
|
|
1778
|
+
if ! hr_inbox_route_var "$candidate$suffix" || [ "$HR_INBOX_BRANCH" != "$candidate" ]; then
|
|
1779
|
+
routes=1
|
|
1780
|
+
break
|
|
1781
|
+
fi
|
|
1782
|
+
done
|
|
1783
|
+
if [ "$routes" -eq 0 ]; then
|
|
1784
|
+
base="$candidate"
|
|
1785
|
+
break
|
|
1786
|
+
fi
|
|
1787
|
+
done
|
|
1788
|
+
[ -n "$base" ] || return 2
|
|
1789
|
+
|
|
1790
|
+
hr_config_load "$root" || :
|
|
1791
|
+
hr_branch_taken_lists_var "$root" || return 2
|
|
1792
|
+
candidate="$base"
|
|
1793
|
+
n=1
|
|
1794
|
+
while :; do
|
|
1795
|
+
status=0
|
|
1796
|
+
hr_branch_taken_judge "$root" "$candidate" "$registry" "$gh_cli" || status=$?
|
|
1797
|
+
case "$status" in
|
|
1798
|
+
1) printf '%s\n' "$candidate"; return 0 ;;
|
|
1799
|
+
2) return 2 ;;
|
|
1800
|
+
esac
|
|
1801
|
+
n=$((n + 1))
|
|
1802
|
+
[ "$n" -le "$HR_BRANCH_SUFFIX_MAX" ] || return 3
|
|
1803
|
+
candidate="${base}_$n"
|
|
1804
|
+
done
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
# ---------------------------------------------------------------------------
|
|
1808
|
+
# THE ARTIFACT PLACEMENT.
|
|
1809
|
+
#
|
|
1810
|
+
# THE CONTRACT. The one placement the watcher's inbox pass and a job starting a
|
|
1811
|
+
# run both perform: copy a dropped artifact into a working copy, stage exactly
|
|
1812
|
+
# that path, skip an identical re-drop, commit through the caller-named commit
|
|
1813
|
+
# wrapper, push through the caller-named push wrapper, and read "landed" as
|
|
1814
|
+
# `origin/<branch>` equal to `HEAD`. Every step reports by exit status only;
|
|
1815
|
+
# what a failure means — log and launch anyway, or block the dispatch — is the
|
|
1816
|
+
# caller's decision. The wrapper paths are arguments because this library
|
|
1817
|
+
# resolves no sibling script. Write exception 4 in the header is this section's.
|
|
1818
|
+
# ---------------------------------------------------------------------------
|
|
1819
|
+
|
|
1820
|
+
# hr_task_prompt_rel <state_rel> <branch> — print the task prompt's
|
|
1821
|
+
# repo-relative path, with <state_rel>'s trailing `/` dropped.
|
|
1822
|
+
hr_task_prompt_rel() {
|
|
1823
|
+
local state_rel="${1-}" branch="${2-}"
|
|
1824
|
+
printf '%s/task_prompts/%s_task_prompt.md\n' "${state_rel%/}" "$branch"
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
# hr_task_prompt_subject <branch> — print the task prompt's commit subject. The
|
|
1828
|
+
# one producer of it; `.claude/context/conventions.md` → `## Commit-message
|
|
1829
|
+
# policy` lists it byte for byte.
|
|
1830
|
+
hr_task_prompt_subject() {
|
|
1831
|
+
printf 'chore: add task prompt for %s\n' "${1-}"
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
# hr_user_review_subject <branch> — print a user review round's commit subject.
|
|
1835
|
+
# The one producer of it, for the watcher's remote inbox pass and
|
|
1836
|
+
# `remote-run.sh review`.
|
|
1837
|
+
hr_user_review_subject() {
|
|
1838
|
+
printf 'chore: add user review for %s\n' "${1-}"
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
# hr_place_artifact <worktree> <src_file> <rel> — copy <src_file> to
|
|
1842
|
+
# <worktree>/<rel>, creating its parent. 0, or 1 on a failure.
|
|
1843
|
+
hr_place_artifact() {
|
|
1844
|
+
local worktree="${1-}" src="${2-}" rel="${3-}" dest
|
|
1845
|
+
[ -n "$worktree" ] && [ -n "$src" ] && [ -n "$rel" ] || return 1
|
|
1846
|
+
dest="$worktree/$rel"
|
|
1847
|
+
mkdir -p "${dest%/*}" || return 1
|
|
1848
|
+
cp "$src" "$dest" || return 1
|
|
1849
|
+
return 0
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
# hr_commit_placed <commit_wrapper> <worktree> <rel> <subject> — stage <rel> and
|
|
1853
|
+
# commit it through <commit_wrapper>. 0 committed; 3 nothing staged for <rel> (an
|
|
1854
|
+
# identical re-drop — nothing committed); 1 staging or the wrapper failed.
|
|
1855
|
+
hr_commit_placed() {
|
|
1856
|
+
local wrapper="${1-}" worktree="${2-}" rel="${3-}" subject="${4-}"
|
|
1857
|
+
[ -n "$wrapper" ] && [ -n "$worktree" ] && [ -n "$rel" ] && [ -n "$subject" ] || return 1
|
|
1858
|
+
git -C "$worktree" add -- "$rel" || return 1
|
|
1859
|
+
git -C "$worktree" diff --cached --quiet -- "$rel" && return 3
|
|
1860
|
+
"$wrapper" --repo "$worktree" "$rel" -- "$subject" || return 1
|
|
1861
|
+
return 0
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
# hr_push_landed <push_wrapper> <worktree> <branch> — run <push_wrapper>, then 0
|
|
1865
|
+
# only when `HEAD` and `refs/remotes/origin/<branch>` both resolve and are
|
|
1866
|
+
# equal; 1 otherwise. `push-branch.sh` exits 0 on every path, so its status is
|
|
1867
|
+
# never the answer.
|
|
1868
|
+
hr_push_landed() {
|
|
1869
|
+
local wrapper="${1-}" worktree="${2-}" branch="${3-}" head upstream
|
|
1870
|
+
[ -n "$wrapper" ] && [ -n "$worktree" ] && [ -n "$branch" ] || return 1
|
|
1871
|
+
"$wrapper" "$worktree"
|
|
1872
|
+
head=$(git -C "$worktree" rev-parse --verify --quiet HEAD) || return 1
|
|
1873
|
+
upstream=$(git -C "$worktree" rev-parse --verify --quiet "refs/remotes/origin/$branch") || return 1
|
|
1874
|
+
[ -n "$head" ] && [ "$head" = "$upstream" ]
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1322
1877
|
# ---------------------------------------------------------------------------
|
|
1323
1878
|
# THE REMOTE STATE BUNDLE.
|
|
1324
1879
|
#
|
|
1325
1880
|
# THE FORMAT OF RECORD. What a remote job carries across a job boundary and
|
|
1326
|
-
# reports back, uploaded as the Actions artifact `
|
|
1327
|
-
# below is a variable `hr_remote_names_var` assigns
|
|
1881
|
+
# reports back, uploaded as the Actions artifact `HR_REMOTE_STATE_ARTIFACT`
|
|
1882
|
+
# names. Every name below is a variable `hr_remote_names_var` assigns, and it
|
|
1883
|
+
# also assigns `HR_REMOTE_WORKFLOW_RUN_FILE`, the run workflow's file name, for
|
|
1884
|
+
# the GitHub-route producers `hr_github_answer_route` and
|
|
1885
|
+
# `hr_github_resume_route` and for the run-history probe `hr_branch_run_history`.
|
|
1886
|
+
# Those two are mirrors of the header's table; no function spells either one,
|
|
1887
|
+
# or any other name here.
|
|
1328
1888
|
#
|
|
1329
1889
|
# <bundle>/status.json the job's record, fixed schema below
|
|
1330
1890
|
# <bundle>/clarifications/<branch>/ the whole branch directory, answered/ included
|
|
@@ -1402,6 +1962,46 @@ hr_remote_names_var() {
|
|
|
1402
1962
|
HR_REMOTE_STATUS_SOURCE="$HR_REMOTE_LOGS_DIR/remote_status.json"
|
|
1403
1963
|
HR_REMOTE_SUPERSEDED_DIR="$HR_REMOTE_LOGS_DIR/remote_superseded"
|
|
1404
1964
|
HR_REMOTE_PLANNING_DIR='planning'
|
|
1965
|
+
HR_REMOTE_WORKFLOW_RUN_FILE='harness-run.yml'
|
|
1966
|
+
HR_REMOTE_STATE_ARTIFACT='harness-state'
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
# hr_github_answer_route <branch> <engine> [park_loop_clear]
|
|
1970
|
+
# hr_github_resume_route <branch> <engine>
|
|
1971
|
+
#
|
|
1972
|
+
# Print the GitHub route for a remote-only reader of a job-side notification,
|
|
1973
|
+
# one clause with no trailing period, for the caller to join after its local
|
|
1974
|
+
# command. The route names the engine because `harness-run.yml`'s `engine`
|
|
1975
|
+
# input defaults to `task`: an empty <engine> prints where to read the run's
|
|
1976
|
+
# own instead. It names the branch twice, as the form's *Use workflow from*
|
|
1977
|
+
# ref and as the `branch` input: a run dispatched from the default branch is
|
|
1978
|
+
# listed under that branch, where every `gh run list --branch <branch>`
|
|
1979
|
+
# lookup (`sync`, `status`, `restore`) misses it. A non-empty third argument to the answer route adds the
|
|
1980
|
+
# park-loop clear. The section cited is `## 1. The lifecycle of a remote run`;
|
|
1981
|
+
# renumbering or retitling it is an edit here.
|
|
1982
|
+
hr_github_answer_route() {
|
|
1983
|
+
local branch="${1-}" engine="${2-}" clear='' eng
|
|
1984
|
+
hr_remote_names_var
|
|
1985
|
+
if [ -n "$engine" ]; then
|
|
1986
|
+
eng="engine \`$engine\`"
|
|
1987
|
+
else
|
|
1988
|
+
eng="engine the run's own (the \`engine\` field of \`$HR_REMOTE_STATUS_FILE\` in its \`$HR_REMOTE_STATE_ARTIFACT\` artifact)"
|
|
1989
|
+
fi
|
|
1990
|
+
[ -n "${3-}" ] && clear=', park_loop_clear true'
|
|
1991
|
+
printf 'or from GitHub: take the question from the run'"'"'s `%s` artifact, then Run workflow on %s from the branch `%s` (Use workflow from), with action run, branch `%s`, %s, resume answer%s and answers `{"<n>": "<your answer>"}` (docs/remote-execution.md, section 1)' \
|
|
1992
|
+
"$HR_REMOTE_STATE_ARTIFACT" "$HR_REMOTE_WORKFLOW_RUN_FILE" "$branch" "$branch" "$eng" "$clear"
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
hr_github_resume_route() {
|
|
1996
|
+
local branch="${1-}" engine="${2-}" eng
|
|
1997
|
+
hr_remote_names_var
|
|
1998
|
+
if [ -n "$engine" ]; then
|
|
1999
|
+
eng="engine \`$engine\`"
|
|
2000
|
+
else
|
|
2001
|
+
eng="engine the run's own (the \`engine\` field of \`$HR_REMOTE_STATUS_FILE\` in its \`$HR_REMOTE_STATE_ARTIFACT\` artifact)"
|
|
2002
|
+
fi
|
|
2003
|
+
printf 'or from GitHub: Run workflow on %s from the branch `%s` (Use workflow from), with action run, branch `%s`, %s and resume pause (docs/remote-execution.md, section 1)' \
|
|
2004
|
+
"$HR_REMOTE_WORKFLOW_RUN_FILE" "$branch" "$branch" "$eng"
|
|
1405
2005
|
}
|
|
1406
2006
|
|
|
1407
2007
|
# hr_remote_planning_paths <branch>
|