autonomous-sdlc-harness 0.2.0 → 0.4.1
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/README.md +2 -2
- package/dist/cli.js +0 -0
- package/dist/commands/docs.js +2 -1
- package/dist/commands/docs.js.map +1 -1
- package/dist/commands/doctor.js +56 -6
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.js +107 -20
- package/dist/commands/init.js.map +1 -1
- package/dist/config/check.js +11 -4
- package/dist/config/check.js.map +1 -1
- package/dist/config/model.js +25 -0
- package/dist/config/model.js.map +1 -1
- package/dist/core/defaultBranchPush.js +26 -0
- package/dist/core/defaultBranchPush.js.map +1 -0
- package/dist/core/git.js +61 -0
- package/dist/core/git.js.map +1 -1
- package/dist/core/paths.js +22 -2
- package/dist/core/paths.js.map +1 -1
- package/dist/core/prompt.js +6 -2
- package/dist/core/prompt.js.map +1 -1
- package/dist/core/writer.js +2 -0
- package/dist/core/writer.js.map +1 -1
- package/dist/doctor/checks.js +482 -73
- package/dist/doctor/checks.js.map +1 -1
- package/dist/generators/githubWorkflows.js +66 -0
- package/dist/generators/githubWorkflows.js.map +1 -0
- package/dist/generators/notifications.js +105 -19
- package/dist/generators/notifications.js.map +1 -1
- package/dist/generators/outerLoopScripts.js +17 -3
- package/dist/generators/outerLoopScripts.js.map +1 -1
- package/dist/generators/permissionProfile.js +141 -21
- package/dist/generators/permissionProfile.js.map +1 -1
- package/dist/generators/projectSettings.js +2 -1
- package/dist/generators/projectSettings.js.map +1 -1
- package/dist/generators/repoRoot.js +23 -4
- package/dist/generators/repoRoot.js.map +1 -1
- package/dist/generators/stateDir.js +9 -3
- package/dist/generators/stateDir.js.map +1 -1
- package/dist/machine/paths.js +4 -1
- package/dist/machine/paths.js.map +1 -1
- package/dist/remote/githubActions.js +86 -0
- package/dist/remote/githubActions.js.map +1 -0
- package/dist/retrieval/queryLog.js +6 -4
- package/dist/retrieval/queryLog.js.map +1 -1
- package/dist/retrieval/runtime.js +46 -33
- package/dist/retrieval/runtime.js.map +1 -1
- package/dist/retrieval/search.js +21 -10
- package/dist/retrieval/search.js.map +1 -1
- package/dist/retrieval/server.js +1 -1
- package/dist/retrieval/setup.js +2 -1
- package/dist/retrieval/setup.js.map +1 -1
- package/package.json +2 -2
- package/templates/README.md +3 -2
- package/templates/claude/context/conventions.md +1 -1
- package/templates/claude/context/layer.md +1 -1
- package/templates/claude/push-notify.env.example +7 -2
- package/templates/claude/settings.autonomous.json +1 -1
- package/templates/github/workflows/harness-resume.yml +124 -0
- package/templates/github/workflows/harness-run.yml +446 -0
- package/templates/repo/gitignore +11 -1
- package/templates/scripts/README.md +1 -1
- package/templates/scripts/autonomous-watcher.sh +1296 -169
- package/templates/scripts/flow-walker.sh +629 -0
- package/templates/scripts/flows/task_plan_writing.graph.json +192 -0
- package/templates/scripts/lib/flow-walker-gates.sh +165 -0
- package/templates/scripts/lib/harness-run-lib.sh +595 -18
- package/templates/scripts/remote-run.sh +1784 -0
- package/templates/scripts/restart-watcher.sh +21 -1
- package/templates/scripts/run-test-suite.sh +182 -0
- package/templates/scripts/scratch-run.sh +2 -1
- package/templates/state-dir/README-root.md +1 -1
- package/templates/state-dir/autonomous_logs/README.md +2 -2
- package/templates/state-dir/improvement_observations/README.md +2 -0
- package/templates/state-dir/scratch/README.md +1 -1
- package/templates/state-dir/test_fix_plan_reviews/README.md +9 -0
- package/templates/state-dir/test_fix_plans/README.md +9 -0
- package/templates/state-dir/test_fix_point_reviews/README.md +9 -0
- package/templates/state-dir/test_run_logs/README.md +11 -0
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
# `harness.config.json` at run time, answers "is this branch protected?", and
|
|
5
5
|
# derives the anchors (main checkout, work root, worktree directory, repo slug,
|
|
6
6
|
# state-dir paths) the scripts would otherwise each re-derive slightly
|
|
7
|
-
# differently.
|
|
7
|
+
# differently. It also implements the run registry's reads and writes for the
|
|
8
|
+
# 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.
|
|
8
10
|
#
|
|
9
11
|
# WHO SOURCES THIS, AND HOW. Every script in the configured `scriptsDir` that
|
|
10
12
|
# needs this library sources it by a path computed from `${BASH_SOURCE[0]}` —
|
|
@@ -43,19 +45,47 @@
|
|
|
43
45
|
# nowhere else, where `<repo_root>` is resolved from a bare
|
|
44
46
|
# `git rev-parse --show-toplevel` at the directory the caller names. Nothing
|
|
45
47
|
# here reads an environment variable in place of a configured value, and nothing
|
|
46
|
-
# here writes
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
48
|
+
# here writes inside a repository except through the named write exceptions
|
|
49
|
+
# below.
|
|
50
|
+
#
|
|
51
|
+
# THE WRITE EXCEPTIONS TO "WRITES NOTHING", AND THEIR FENCES. Each entry names
|
|
52
|
+
# the section that writes, the functions that write, and where. Nothing outside
|
|
53
|
+
# this list writes at all; a section that adds a writer adds its entry here.
|
|
54
|
+
#
|
|
55
|
+
# 1. The machine-level usage lane (the section at the bottom of this file)
|
|
56
|
+
# publishes a state record and takes an advisory lock. Fence: both live
|
|
57
|
+
# under `hr_lane_dir` — a machine-local path outside every repository —
|
|
58
|
+
# and are written only by the `hr_lane_*` functions.
|
|
59
|
+
# 2. THE RUN REGISTRY writes the registry file its caller names. Fence: that
|
|
60
|
+
# file is `<root>/<state_dir>/autonomous_logs/registry.json`, resolved
|
|
61
|
+
# through `hr_state_path`, plus the lock directory `<file>.lock` (and its
|
|
62
|
+
# `.stale.*` move-aside and `.break` mutex while a stale one is broken)
|
|
63
|
+
# and the temp files
|
|
64
|
+
# `.registry.*` in the registry's own directory. Written only by
|
|
65
|
+
# `hr_registry_init`, `hr_registry_set` and the `hr_registry_lock` /
|
|
66
|
+
# `hr_registry_unlock` pair `hr_registry_set` calls.
|
|
67
|
+
# 3. THE REMOTE STATE BUNDLE writes the files its format lists. Fence: inside
|
|
68
|
+
# `<root>/<state_dir>/` (resolved through `hr_state_dir`), only
|
|
69
|
+
# `autonomous_logs/remote_status.json`, `clarifications/<branch>/`,
|
|
70
|
+
# `PAUSE_PROGRESS.md`, `.flow_walker_state` and the move-aside directory
|
|
71
|
+
# `autonomous_logs/remote_superseded/`; outside it, only the caller-named
|
|
72
|
+
# `<out_dir>` of `hr_remote_bundle_write` and the caller-named `<out_json>`
|
|
73
|
+
# of `hr_remote_status_write`. Written only by `hr_remote_status_write`,
|
|
74
|
+
# `hr_remote_bundle_write` and `hr_remote_bundle_restore`, and nothing
|
|
75
|
+
# there but a writer's own failed temp file is ever removed.
|
|
76
|
+
#
|
|
77
|
+
# A caller that calls no `hr_lane_*`, `hr_registry_init`, `hr_registry_set`,
|
|
78
|
+
# `hr_registry_lock`, `hr_registry_unlock`, `hr_remote_status_write`,
|
|
79
|
+
# `hr_remote_bundle_write` or `hr_remote_bundle_restore` function still gets a library that only reads. The
|
|
80
|
+
# lane's ceilings are the only environment values here that carry policy, because
|
|
81
|
+
# the lane is machine-scoped and has no configuration key to carry them; each is
|
|
55
82
|
# named where it is used. `XDG_STATE_HOME`, `XDG_CONFIG_HOME`, `XDG_CACHE_HOME`,
|
|
56
83
|
# `HOME` and `PWD` are also read, as location anchors only, and `PATH` is read by
|
|
57
84
|
# `hr_path_with_fallbacks` alone — as that function's input, which it prints back
|
|
58
|
-
# transformed and never assigns.
|
|
85
|
+
# transformed and never assigns. `GITHUB_RUN_ID`, `GITHUB_SERVER_URL`,
|
|
86
|
+
# `GITHUB_REPOSITORY` and `HARNESS_INPUT_CHAIN` are read by
|
|
87
|
+
# `hr_remote_status_write` alone, as provenance values copied into
|
|
88
|
+
# `status.json` — never a configured value and never policy.
|
|
59
89
|
#
|
|
60
90
|
# CONFIGURATION IS READ AT RUN TIME, NOT FROZEN AT GENERATION TIME. That is the
|
|
61
91
|
# whole reason the shipped scripts carry no `{{token}}`: a guard whose protected
|
|
@@ -84,8 +114,10 @@
|
|
|
84
114
|
# and return 2 when the configuration could not be read. A caller must be able
|
|
85
115
|
# to tell "the key is empty" from "the file could not be read", because the
|
|
86
116
|
# first is an ordinary project and the second is a repository this script has no
|
|
87
|
-
# business acting in.
|
|
88
|
-
# `
|
|
117
|
+
# business acting in. `hr_phase_enabled` answers through the same three
|
|
118
|
+
# statuses and prints nothing: 0 is `true`, 1 is `false` or unset, and 2 also
|
|
119
|
+
# covers an unknown phase name or a non-boolean value. A reader that wants the
|
|
120
|
+
# finer distinction between "no `harness.config.json` at all" and "one that would not parse" calls
|
|
89
121
|
# `hr_config_load` directly, which returns 1 for the first and 2 for the second.
|
|
90
122
|
#
|
|
91
123
|
# THE PROTECTED SET. It is *(`protectedBranches` if present, else that key's
|
|
@@ -131,10 +163,15 @@
|
|
|
131
163
|
# dialect marker for editors and linters). No `set -e` and no `set -u` — a
|
|
132
164
|
# sourced library must not change its caller's shell — but every parameter
|
|
133
165
|
# expansion here is defaulted, so it is safe to source into a caller that sets
|
|
134
|
-
# both. No top-level side effects, no exiting, no writes outside the
|
|
135
|
-
#
|
|
136
|
-
# silent on failure and signals through its return status,
|
|
137
|
-
# capture stdout.
|
|
166
|
+
# both. No top-level side effects, no exiting, no writes outside the fences of
|
|
167
|
+
# the write exceptions named above, and no diagnostics on stdout OR stderr:
|
|
168
|
+
# every reader is silent on failure and signals through its return status,
|
|
169
|
+
# because callers capture stdout. The one pass-through is the registry's two
|
|
170
|
+
# writers, `hr_registry_init` and `hr_registry_set`, which leave the shell's,
|
|
171
|
+
# `mktemp`'s and `jq`'s own stderr on a failed write to the caller, as the
|
|
172
|
+
# watcher's bodies they replaced did — that stream is the watcher's log — and
|
|
173
|
+
# add one line of their own, naming the lock, when `hr_registry_set` cannot
|
|
174
|
+
# take the registry lock. That silence is why the lane reports a lock it BROKE through a
|
|
138
175
|
# variable instead of a log line — the caller owns the log.
|
|
139
176
|
#
|
|
140
177
|
# NAMING. Every function is prefixed `hr_`; every variable this file touches
|
|
@@ -145,7 +182,8 @@
|
|
|
145
182
|
# `HR_CFG_VALUE`, `HR_CFG_COMMAND_KEYS`, `HR_PROTECTED_DEFAULT`, and the lane's
|
|
146
183
|
# `HR_LANE_RANK`, `HR_LANE_STATE`, `HR_LANE_RESUME_AT`, `HR_LANE_OBSERVED_AT`,
|
|
147
184
|
# `HR_LANE_OBSERVED_REPO`, `HR_LANE_OWNER_SLUG`, `HR_LANE_OWNER_PID`,
|
|
148
|
-
# `HR_LANE_OWNER_AT` and `HR_LANE_BROKEN_OWNER
|
|
185
|
+
# `HR_LANE_OWNER_AT` and `HR_LANE_BROKEN_OWNER`, and the remote state bundle's
|
|
186
|
+
# names, which `hr_remote_names_var` assigns. Every one of them is assigned
|
|
149
187
|
# before it is read by the function that owns it, so an inherited value from a
|
|
150
188
|
# parent process is overwritten rather than believed.
|
|
151
189
|
#
|
|
@@ -186,6 +224,13 @@
|
|
|
186
224
|
# hr_branch_is_protected "$root" "$(hr_current_branch "$root")"; echo $?
|
|
187
225
|
# -> the resolved set, then 0 or 1
|
|
188
226
|
# hr_state_dir "$root"; hr_command "$root" test
|
|
227
|
+
# phase toggles each against its own fresh fixture, so the cache is cold:
|
|
228
|
+
# {"defaultBranch":"main","phases":{"qa":true}}
|
|
229
|
+
# hr_phase_enabled "$root" qa; echo $? -> 0, prints nothing
|
|
230
|
+
# {"defaultBranch":"main"}
|
|
231
|
+
# hr_phase_enabled "$root" qa; echo $? -> 1
|
|
232
|
+
# {"defaultBranch":"main","phases":{"qa":"yes"}}
|
|
233
|
+
# hr_phase_enabled "$root" qa; echo $? -> 2
|
|
189
234
|
# adopted + broken printf 'x' > "$root/harness.config.json"
|
|
190
235
|
# hr_branch_is_protected "$root" <branch>; echo $? -> 2
|
|
191
236
|
# hr_state_dir "$root"; echo $? -> 2, prints nothing
|
|
@@ -522,6 +567,10 @@ hr_config_load() {
|
|
|
522
567
|
# `protectedBranches.present` records that the key was there AS AN ARRAY,
|
|
523
568
|
# which is what lets an explicitly EMPTY list read as a configured set rather
|
|
524
569
|
# than as an absent one.
|
|
570
|
+
#
|
|
571
|
+
# A `phases.*` value that is not a boolean is emitted as `invalid`, so the
|
|
572
|
+
# string `"true"` — which `tostring` would otherwise make indistinguishable
|
|
573
|
+
# from `true` — reaches `hr_phase_enabled` as a value it refuses (2).
|
|
525
574
|
out=$(jq -n -r '
|
|
526
575
|
def s($k; $v):
|
|
527
576
|
if $v == null then empty
|
|
@@ -552,6 +601,10 @@ hr_config_load() {
|
|
|
552
601
|
s("commands.build"; try .commands.build catch null),
|
|
553
602
|
s("commands.devServer"; try .commands.devServer catch null),
|
|
554
603
|
s("commands.depInstall"; try .commands.depInstall catch null),
|
|
604
|
+
s("phases.parity"; try (.phases.parity | if type == "boolean" or . == null then . else "invalid" end) catch null),
|
|
605
|
+
s("phases.qa"; try (.phases.qa | if type == "boolean" or . == null then . else "invalid" end) catch null),
|
|
606
|
+
s("phases.docs"; try (.phases.docs | if type == "boolean" or . == null then . else "invalid" end) catch null),
|
|
607
|
+
s("execution.target"; try .execution.target catch null),
|
|
555
608
|
s("protectedBranches.present";
|
|
556
609
|
try (if (.protectedBranches | type) == "array" then "1" else null end) catch null),
|
|
557
610
|
l("protectedBranches"; try .protectedBranches catch null)
|
|
@@ -775,6 +828,49 @@ hr_command() {
|
|
|
775
828
|
hr_config_scalar "$root" "commands.$key" ""
|
|
776
829
|
}
|
|
777
830
|
|
|
831
|
+
# `phases.<phase>` — whether one optional phase runs. THE ONE TYPED READER THAT
|
|
832
|
+
# PRINTS NOTHING: the answer is the status. 0 = `true`; 1 = `false` or unset (an
|
|
833
|
+
# unset flag is false, as the flow-progress ledger's `phases:` line records it);
|
|
834
|
+
# 2 = the configuration is unresolvable, <phase> is not `parity` / `qa` /
|
|
835
|
+
# `docs`, or the stored value is not a boolean — a value the schema forbids,
|
|
836
|
+
# which this refuses rather than guesses about.
|
|
837
|
+
hr_phase_enabled() {
|
|
838
|
+
local root="${1-}" phase="${2-}"
|
|
839
|
+
case "$phase" in
|
|
840
|
+
parity|qa|docs) ;;
|
|
841
|
+
*) return 2 ;;
|
|
842
|
+
esac
|
|
843
|
+
hr_config_load "$root" || return 2
|
|
844
|
+
hr_cfg_scalar_var "phases.$phase" || return 1
|
|
845
|
+
case "$HR_CFG_VALUE" in
|
|
846
|
+
true) return 0 ;;
|
|
847
|
+
false) return 1 ;;
|
|
848
|
+
esac
|
|
849
|
+
return 2
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
# `execution.target` — where an unattended run executes: `local` or
|
|
853
|
+
# `github-actions`. THE ONE READER OF THE KEY IN THIS FAMILY; a script that
|
|
854
|
+
# needs it calls this. Schema default `local`, so an absent key prints `local`
|
|
855
|
+
# and 1 is never returned. 2 — printing nothing — when the configuration is
|
|
856
|
+
# unresolvable or the stored value is outside the schema's enum, which this
|
|
857
|
+
# refuses rather than guesses about, as `hr_phase_enabled` does.
|
|
858
|
+
hr_execution_target() {
|
|
859
|
+
local root="${1-}"
|
|
860
|
+
hr_config_load "$root" || return 2
|
|
861
|
+
if ! hr_cfg_scalar_var "execution.target"; then
|
|
862
|
+
printf 'local\n'
|
|
863
|
+
return 0
|
|
864
|
+
fi
|
|
865
|
+
case "$HR_CFG_VALUE" in
|
|
866
|
+
local|github-actions)
|
|
867
|
+
printf '%s\n' "$HR_CFG_VALUE"
|
|
868
|
+
return 0
|
|
869
|
+
;;
|
|
870
|
+
esac
|
|
871
|
+
return 2
|
|
872
|
+
}
|
|
873
|
+
|
|
778
874
|
# ---------------------------------------------------------------------------
|
|
779
875
|
# The protected-branch trichotomy.
|
|
780
876
|
# ---------------------------------------------------------------------------
|
|
@@ -1008,6 +1104,487 @@ hr_push_env_files() {
|
|
|
1008
1104
|
return 0
|
|
1009
1105
|
}
|
|
1010
1106
|
|
|
1107
|
+
# ---------------------------------------------------------------------------
|
|
1108
|
+
# THE RUN REGISTRY.
|
|
1109
|
+
#
|
|
1110
|
+
# THE CONTRACT IS NOT THIS SECTION'S. The registry's JSON shape
|
|
1111
|
+
# (`{"runs": {"<branch>": {…}}}`), its field set and its status vocabulary are
|
|
1112
|
+
# stated in `autonomous-watcher.sh`'s header and registry comment block; this
|
|
1113
|
+
# section only reads and writes that shape, for every script that shares the
|
|
1114
|
+
# file, so no second copy of a writer exists.
|
|
1115
|
+
#
|
|
1116
|
+
# Each function takes the registry file as its first argument — the caller
|
|
1117
|
+
# resolves it as `hr_state_path <root> autonomous_logs/registry.json` — and is
|
|
1118
|
+
# write exception 2 in the header. Every value is written as a JSON string;
|
|
1119
|
+
# every write stamps `branch` and `updated_at` on the record. No shell option is
|
|
1120
|
+
# assumed: the caller may set `-e`, `-u` or neither.
|
|
1121
|
+
#
|
|
1122
|
+
# EVERY WRITE IS A LOCKED READ-MODIFY-WRITE. Every script sharing the file
|
|
1123
|
+
# writes it, and so does the watcher's own `( … ) &` engine subshell, so two
|
|
1124
|
+
# unserialized writers each read the same record and the second `mv` silently
|
|
1125
|
+
# drops the first one's key. The lock is a `mkdir` of `<file>.lock` holding an
|
|
1126
|
+
# `owner` file with a per-call token — the `mktemp` name of that call's temp
|
|
1127
|
+
# file, because `$$` inside a subshell is the parent's pid and bash 3.2 has no
|
|
1128
|
+
# per-subshell pid variable — and only a call whose token is still in `owner`
|
|
1129
|
+
# releases it.
|
|
1130
|
+
#
|
|
1131
|
+
# STALENESS IS THE LOCK DIRECTORY'S AGE, NEVER A PID. The stall watchdog kills
|
|
1132
|
+
# the engine subshell mid-write, so a dead holder is expected, and a pid says
|
|
1133
|
+
# nothing about a lock the killed subshell took under its parent's `$$`. A write
|
|
1134
|
+
# is one `jq` over a file of a few records, well under a second, so a lock 10 s
|
|
1135
|
+
# old is a crashed holder; a waiter polls every 0.05 s for up to 12 s — past the
|
|
1136
|
+
# stale age, so a waiter that arrives just after a crash breaks the lock rather
|
|
1137
|
+
# than giving up. Both are constants here, not environment values: the header
|
|
1138
|
+
# allows policy-carrying environment values for the lane only.
|
|
1139
|
+
#
|
|
1140
|
+
# BREAKERS ARE SERIALIZED, AND NONE RENAMES A LIVE LOCK. A waiter that judges
|
|
1141
|
+
# the lock stale takes a second `mkdir` mutex, `<file>.lock.break`, judges the
|
|
1142
|
+
# lock's age again under it, and only then renames the lock aside and empties
|
|
1143
|
+
# it, as `hr_lane_acquire` does. A lock re-taken since the first judgement is
|
|
1144
|
+
# fresh, so the second leaves it alone. Between the second judgement and the
|
|
1145
|
+
# rename, nothing but a breaker or the lock's own holder frees the path:
|
|
1146
|
+
# breakers are serialized, and the holder of a lock that old is dead by the
|
|
1147
|
+
# stale rule. The watchdog can kill a breaker inside the mutex, so a mutex 10 s
|
|
1148
|
+
# old is removed by the next waiter. That removal is the one window left open.
|
|
1149
|
+
# Two waiters that judge one abandoned mutex old together can both remove it,
|
|
1150
|
+
# and the second can remove the fresh mutex the first has just taken. Two
|
|
1151
|
+
# breakers then judge the lock at once, so reaching it takes a breaker killed
|
|
1152
|
+
# inside the mutex, a stale lock and three concurrent writers.
|
|
1153
|
+
#
|
|
1154
|
+
# A write that cannot take the lock prints one stderr line naming it, writes
|
|
1155
|
+
# nothing and returns 1.
|
|
1156
|
+
#
|
|
1157
|
+
# ONE CALL, SEVERAL KEYS. `hr_registry_set <file> <branch> <key> <value> [<key>
|
|
1158
|
+
# <value> …]` writes every pair in one `jq` pass under one lock and one `mv`.
|
|
1159
|
+
# A caller writing two keys that a concurrent reader must never see apart — a
|
|
1160
|
+
# status and the reason for it — MUST pass them in one call.
|
|
1161
|
+
#
|
|
1162
|
+
# READERS TAKE NO LOCK. The temp file sits beside the registry, so the `mv` is a
|
|
1163
|
+
# same-filesystem rename and `hr_registry_get`, `hr_registry_branches` and any
|
|
1164
|
+
# other `jq` over the file see the old record or the new one, never a partial
|
|
1165
|
+
# one. `hr_registry_init` creates the file by `ln`-ing a complete temp file to
|
|
1166
|
+
# the registry name, which fails when the name exists, so a reader's create
|
|
1167
|
+
# never truncates a registry a writer has just created.
|
|
1168
|
+
# ---------------------------------------------------------------------------
|
|
1169
|
+
|
|
1170
|
+
# The directory holding <file> — where its temp files go, so every `mv` over it
|
|
1171
|
+
# is a same-filesystem rename.
|
|
1172
|
+
hr_registry_dir() {
|
|
1173
|
+
local file="${1-}" dir
|
|
1174
|
+
case "$file" in
|
|
1175
|
+
*/*)
|
|
1176
|
+
dir="${file%/*}"
|
|
1177
|
+
[ -n "$dir" ] || dir=/
|
|
1178
|
+
;;
|
|
1179
|
+
*) dir=. ;;
|
|
1180
|
+
esac
|
|
1181
|
+
printf '%s\n' "$dir"
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
# Create an empty registry at <file> when none exists. Atomic: the name is
|
|
1185
|
+
# either absent or a complete `{"runs":{}}` — never truncated, never partial.
|
|
1186
|
+
hr_registry_init() {
|
|
1187
|
+
local file="${1-}" dir tmp
|
|
1188
|
+
[ -n "$file" ] || return 1
|
|
1189
|
+
[ -f "$file" ] && return 0
|
|
1190
|
+
dir=$(hr_registry_dir "$file")
|
|
1191
|
+
tmp=$(mktemp "$dir/.registry.XXXXXX") || return 1
|
|
1192
|
+
if printf '{"runs":{}}\n' >"$tmp"; then
|
|
1193
|
+
ln "$tmp" "$file" 2>/dev/null || :
|
|
1194
|
+
fi
|
|
1195
|
+
rm -f "$tmp"
|
|
1196
|
+
[ -f "$file" ]
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
# hr_registry_lock <file> <token> — take `<file>.lock` for <token>, breaking a
|
|
1200
|
+
# stale one (see the section comment). 0 when held; 1, with one stderr line,
|
|
1201
|
+
# when the wait ceiling passed.
|
|
1202
|
+
hr_registry_lock() {
|
|
1203
|
+
local lock="${1-}.lock" token="${2-}" polls=0 start="" now m stale bm
|
|
1204
|
+
# 12 — the wait ceiling and 10 — the stale age, both in seconds. The wait is
|
|
1205
|
+
# judged by the clock, because each poll's forks cost more than its sleep;
|
|
1206
|
+
# 240 polls is the ceiling only when `date` gives no epoch.
|
|
1207
|
+
while :; do
|
|
1208
|
+
if mkdir "$lock" 2>/dev/null; then
|
|
1209
|
+
if printf '%s\n' "$token" >"$lock/owner" 2>/dev/null; then
|
|
1210
|
+
return 0
|
|
1211
|
+
fi
|
|
1212
|
+
rm -f "$lock/owner" 2>/dev/null || :
|
|
1213
|
+
rmdir "$lock" 2>/dev/null || :
|
|
1214
|
+
break
|
|
1215
|
+
fi
|
|
1216
|
+
m=$(hr_lane_mtime "$lock")
|
|
1217
|
+
now=$(date +%s 2>/dev/null) || now=0
|
|
1218
|
+
case "$now" in '' | *[!0-9]*) now=0 ;; esac
|
|
1219
|
+
[ -n "$start" ] || start="$now"
|
|
1220
|
+
if [ "$now" -gt 0 ]; then
|
|
1221
|
+
[ $((now - start)) -lt 12 ] || break
|
|
1222
|
+
else
|
|
1223
|
+
[ "$polls" -lt 240 ] || break
|
|
1224
|
+
fi
|
|
1225
|
+
if [ "$m" -gt 0 ] && [ "$now" -gt 0 ] && [ $((now - m)) -ge 10 ]; then
|
|
1226
|
+
if mkdir "$lock.break" 2>/dev/null; then
|
|
1227
|
+
# One breaker at a time: judge the age again under the mutex, so a
|
|
1228
|
+
# lock re-taken since the judgement above is fresh and left alone.
|
|
1229
|
+
m=$(hr_lane_mtime "$lock")
|
|
1230
|
+
now=$(date +%s 2>/dev/null) || now=0
|
|
1231
|
+
case "$now" in '' | *[!0-9]*) now=0 ;; esac
|
|
1232
|
+
if [ "$m" -gt 0 ] && [ "$now" -gt 0 ] && [ $((now - m)) -ge 10 ]; then
|
|
1233
|
+
stale="$lock.stale.${token##*.}"
|
|
1234
|
+
if mv "$lock" "$stale" 2>/dev/null; then
|
|
1235
|
+
rm -f "$stale/owner" 2>/dev/null || :
|
|
1236
|
+
rmdir "$stale" 2>/dev/null || :
|
|
1237
|
+
fi
|
|
1238
|
+
fi
|
|
1239
|
+
rmdir "$lock.break" 2>/dev/null || :
|
|
1240
|
+
polls=$((polls + 1))
|
|
1241
|
+
continue
|
|
1242
|
+
fi
|
|
1243
|
+
# Another breaker holds the mutex. One the watchdog killed inside it
|
|
1244
|
+
# left it behind: a mutex 10 s old is removed here.
|
|
1245
|
+
bm=$(hr_lane_mtime "$lock.break")
|
|
1246
|
+
if [ "$bm" -gt 0 ] && [ $((now - bm)) -ge 10 ]; then
|
|
1247
|
+
rmdir "$lock.break" 2>/dev/null || :
|
|
1248
|
+
fi
|
|
1249
|
+
fi
|
|
1250
|
+
sleep 0.05
|
|
1251
|
+
polls=$((polls + 1))
|
|
1252
|
+
done
|
|
1253
|
+
printf 'hr_registry_set: could not take the registry lock %s\n' "$lock" >&2
|
|
1254
|
+
return 1
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
# hr_registry_unlock <file> <token> — release `<file>.lock` only while its
|
|
1258
|
+
# `owner` still holds <token>; a lock broken and re-taken is someone else's.
|
|
1259
|
+
hr_registry_unlock() {
|
|
1260
|
+
local lock="${1-}.lock" token="${2-}" owner=""
|
|
1261
|
+
[ -r "$lock/owner" ] && { IFS= read -r owner <"$lock/owner" || :; } 2>/dev/null
|
|
1262
|
+
[ -n "$token" ] && [ "$owner" = "$token" ] || return 0
|
|
1263
|
+
rm -f "$lock/owner" 2>/dev/null || :
|
|
1264
|
+
rmdir "$lock" 2>/dev/null || :
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
# hr_registry_set <file> <branch> <key> <value> [<key> <value> …]
|
|
1268
|
+
# 1, writing nothing, on no key/value pair or an odd count of them. Each pair
|
|
1269
|
+
# reaches `jq` as `--arg kN` / `--arg vN` — jq 1.5 has no `$ARGS` — and only
|
|
1270
|
+
# those generated names enter the program text, never a value.
|
|
1271
|
+
hr_registry_set() {
|
|
1272
|
+
local file="${1-}" branch="${2-}" dir tmp fields="" i=0 status=0
|
|
1273
|
+
local -a args
|
|
1274
|
+
[ -n "$file" ] && [ "$#" -ge 4 ] && [ $(($# % 2)) -eq 0 ] || return 1
|
|
1275
|
+
shift 2
|
|
1276
|
+
args=(--arg b "$branch" --arg now "$(date '+%Y-%m-%dT%H:%M:%S')")
|
|
1277
|
+
while [ "$#" -gt 0 ]; do
|
|
1278
|
+
args+=(--arg "k$i" "$1" --arg "v$i" "$2")
|
|
1279
|
+
fields="$fields(\$k$i): \$v$i, "
|
|
1280
|
+
i=$((i + 1))
|
|
1281
|
+
shift 2
|
|
1282
|
+
done
|
|
1283
|
+
dir=$(hr_registry_dir "$file")
|
|
1284
|
+
tmp=$(mktemp "$dir/.registry.XXXXXX") || return 1
|
|
1285
|
+
if ! hr_registry_lock "$file" "$tmp"; then
|
|
1286
|
+
rm -f "$tmp"
|
|
1287
|
+
return 1
|
|
1288
|
+
fi
|
|
1289
|
+
hr_registry_init "$file" || :
|
|
1290
|
+
if jq "${args[@]}" "
|
|
1291
|
+
.runs[\$b] = ((.runs[\$b] // {}) + {$fields\"branch\": \$b, \"updated_at\": \$now})
|
|
1292
|
+
" "$file" >"$tmp"; then
|
|
1293
|
+
mv "$tmp" "$file" || status=1
|
|
1294
|
+
else
|
|
1295
|
+
status=1
|
|
1296
|
+
fi
|
|
1297
|
+
[ "$status" -eq 0 ] || rm -f "$tmp"
|
|
1298
|
+
hr_registry_unlock "$file" "$tmp"
|
|
1299
|
+
return "$status"
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
# hr_registry_get <file> <branch> <key> -> the value, or nothing
|
|
1303
|
+
hr_registry_get() {
|
|
1304
|
+
local file="${1-}"
|
|
1305
|
+
hr_registry_init "$file" || :
|
|
1306
|
+
jq -r --arg b "${2-}" --arg k "${3-}" '.runs[$b][$k] // empty' "$file" 2>/dev/null
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
# Every branch in the registry at <file>, one per line. Prints nothing when the
|
|
1310
|
+
# file cannot be read as a registry, which leaves each caller iterating over an
|
|
1311
|
+
# empty set.
|
|
1312
|
+
hr_registry_branches() {
|
|
1313
|
+
local file="${1-}"
|
|
1314
|
+
hr_registry_init "$file" || :
|
|
1315
|
+
jq -r '.runs | keys[]' "$file" 2>/dev/null
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
# ---------------------------------------------------------------------------
|
|
1319
|
+
# THE REMOTE STATE BUNDLE.
|
|
1320
|
+
#
|
|
1321
|
+
# THE FORMAT OF RECORD. What a remote job carries across a job boundary and
|
|
1322
|
+
# reports back, uploaded as the Actions artifact `harness-state`. Every name
|
|
1323
|
+
# below is a variable `hr_remote_names_var` assigns; no function spells one.
|
|
1324
|
+
#
|
|
1325
|
+
# <bundle>/status.json the job's record, fixed schema below
|
|
1326
|
+
# <bundle>/clarifications/<branch>/ the whole branch directory, answered/ included
|
|
1327
|
+
# <bundle>/PAUSE_PROGRESS.md when present
|
|
1328
|
+
# <bundle>/flow_walker_state <state_dir>/.flow_walker_state, WITHOUT its dot
|
|
1329
|
+
# <bundle>/run.log <state_dir>/autonomous_logs/<branch>.log; never restored
|
|
1330
|
+
#
|
|
1331
|
+
# The walker state loses its dot because `actions/upload-artifact` skips hidden
|
|
1332
|
+
# files by default. NOTHING IN THE BUNDLE IS EVER COMMITTED: every file in it is
|
|
1333
|
+
# gitignored machine-local state, and the remote-status and move-aside paths sit
|
|
1334
|
+
# under `autonomous_logs/`, whose ignore rule already covers them.
|
|
1335
|
+
#
|
|
1336
|
+
# WHO READS EACH FILE. `status.json`: `remote-run.sh sync` / `status` (into the
|
|
1337
|
+
# local registry), `continue` / `poll` (the decision, `chain`, the reset time)
|
|
1338
|
+
# and the next job's seed. The clarification directory and `PAUSE_PROGRESS.md`:
|
|
1339
|
+
# the next job, and the user's local mirror. The walker state: the next job
|
|
1340
|
+
# only. `run.log`: the user only — `sync` copies it to the main checkout's logs
|
|
1341
|
+
# directory itself, and no restore places it.
|
|
1342
|
+
#
|
|
1343
|
+
# `status.json` — schema `HR_REMOTE_STATE_SCHEMA`; every value a JSON string:
|
|
1344
|
+
# schema a reader that does not recognise it treats the bundle as absent
|
|
1345
|
+
# branch, engine the run's branch; `task` | `user_review` | `docs`
|
|
1346
|
+
# status `running` | `parked` | `park_loop` | `paused` | `completed` | `failed`
|
|
1347
|
+
# pause_reason `usage` | `budget` | `user` | `overload` | empty. The registry's
|
|
1348
|
+
# registry-only `killed` and `expired` are never written
|
|
1349
|
+
# here: `sync` derives each from a run and its artifact
|
|
1350
|
+
# list, not from a bundle
|
|
1351
|
+
# usage_resume_at the epoch second a usage pause may resume at, or empty
|
|
1352
|
+
# park_loop_cycles, resume_max_question_index, auto_resumes, stall_restarts
|
|
1353
|
+
# the counters that must survive a job boundary
|
|
1354
|
+
# chain the writing job's OWN input `HARNESS_INPUT_CHAIN`, never
|
|
1355
|
+
# a value carried from an earlier bundle
|
|
1356
|
+
# control_polled_at the epoch second up to which the job checked for a
|
|
1357
|
+
# `harness pause <branch>` run, or empty
|
|
1358
|
+
# decision `continue` | `wait-poller` | `stop`
|
|
1359
|
+
# detail one human-readable line
|
|
1360
|
+
# run_id, run_url, written_at
|
|
1361
|
+
# `GITHUB_RUN_ID`, the run's URL, the epoch second written
|
|
1362
|
+
# ---------------------------------------------------------------------------
|
|
1363
|
+
|
|
1364
|
+
hr_remote_names_var() {
|
|
1365
|
+
HR_REMOTE_STATE_SCHEMA='1'
|
|
1366
|
+
HR_REMOTE_STATUS_FILE='status.json'
|
|
1367
|
+
HR_REMOTE_CLARIFY_DIR='clarifications'
|
|
1368
|
+
HR_REMOTE_PAUSE_FILE='PAUSE_PROGRESS.md'
|
|
1369
|
+
HR_REMOTE_WALKER_FILE='flow_walker_state'
|
|
1370
|
+
HR_REMOTE_WALKER_SOURCE=".$HR_REMOTE_WALKER_FILE"
|
|
1371
|
+
HR_REMOTE_LOG_FILE='run.log'
|
|
1372
|
+
HR_REMOTE_LOGS_DIR='autonomous_logs'
|
|
1373
|
+
HR_REMOTE_STATUS_SOURCE="$HR_REMOTE_LOGS_DIR/remote_status.json"
|
|
1374
|
+
HR_REMOTE_SUPERSEDED_DIR="$HR_REMOTE_LOGS_DIR/remote_superseded"
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
# hr_remote_status_write <registry_file> <branch> <out_json> <decision> <detail>
|
|
1378
|
+
#
|
|
1379
|
+
# Reads <branch>'s record through `hr_registry_get` and replaces <out_json> by
|
|
1380
|
+
# rename, so a concurrent reader sees the old file or the new one, never half.
|
|
1381
|
+
# Prints nothing. 1 — writing nothing — when an argument is missing, <decision>
|
|
1382
|
+
# is outside its vocabulary, the record has no in-vocabulary `status`, or the
|
|
1383
|
+
# write fails. A `pause_reason` outside the bundle's vocabulary is written empty.
|
|
1384
|
+
hr_remote_status_write() {
|
|
1385
|
+
local registry="${1-}" branch="${2-}" out="${3-}" decision="${4-}" detail="${5-}"
|
|
1386
|
+
local status engine reason chain run_id run_url tmp
|
|
1387
|
+
[ -n "$registry" ] && [ -n "$branch" ] && [ -n "$out" ] || return 1
|
|
1388
|
+
case "$decision" in
|
|
1389
|
+
continue|wait-poller|stop) ;;
|
|
1390
|
+
*) return 1 ;;
|
|
1391
|
+
esac
|
|
1392
|
+
hr_remote_names_var
|
|
1393
|
+
status=$(hr_registry_get "$registry" "$branch" status)
|
|
1394
|
+
case "$status" in
|
|
1395
|
+
running|parked|park_loop|paused|completed|failed) ;;
|
|
1396
|
+
*) return 1 ;;
|
|
1397
|
+
esac
|
|
1398
|
+
engine=$(hr_registry_get "$registry" "$branch" engine)
|
|
1399
|
+
reason=$(hr_registry_get "$registry" "$branch" pause_reason)
|
|
1400
|
+
case "$reason" in
|
|
1401
|
+
usage|budget|user|overload) ;;
|
|
1402
|
+
*) reason="" ;;
|
|
1403
|
+
esac
|
|
1404
|
+
chain="${HARNESS_INPUT_CHAIN-}"
|
|
1405
|
+
case "$chain" in
|
|
1406
|
+
''|*[!0-9]*) chain="" ;;
|
|
1407
|
+
esac
|
|
1408
|
+
run_id="${GITHUB_RUN_ID-}"
|
|
1409
|
+
run_url=""
|
|
1410
|
+
if [ -n "$run_id" ] && [ -n "${GITHUB_SERVER_URL-}" ] && [ -n "${GITHUB_REPOSITORY-}" ]; then
|
|
1411
|
+
run_url="${GITHUB_SERVER_URL%/}/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
|
|
1412
|
+
fi
|
|
1413
|
+
detail=${detail//$'\r'/ }
|
|
1414
|
+
detail=${detail//$'\n'/ }
|
|
1415
|
+
# The temp file sits beside <out_json> so the `mv` is a same-filesystem rename.
|
|
1416
|
+
tmp=$(mktemp "${out}.tmp.XXXXXX" 2>/dev/null) || return 1
|
|
1417
|
+
if jq -n \
|
|
1418
|
+
--arg schema "$HR_REMOTE_STATE_SCHEMA" \
|
|
1419
|
+
--arg branch "$branch" \
|
|
1420
|
+
--arg engine "$engine" \
|
|
1421
|
+
--arg status "$status" \
|
|
1422
|
+
--arg pause_reason "$reason" \
|
|
1423
|
+
--arg usage_resume_at "$(hr_registry_get "$registry" "$branch" usage_resume_at)" \
|
|
1424
|
+
--arg park_loop_cycles "$(hr_registry_get "$registry" "$branch" park_loop_cycles)" \
|
|
1425
|
+
--arg resume_max_question_index "$(hr_registry_get "$registry" "$branch" resume_max_question_index)" \
|
|
1426
|
+
--arg auto_resumes "$(hr_registry_get "$registry" "$branch" auto_resumes)" \
|
|
1427
|
+
--arg stall_restarts "$(hr_registry_get "$registry" "$branch" stall_restarts)" \
|
|
1428
|
+
--arg chain "$chain" \
|
|
1429
|
+
--arg control_polled_at "$(hr_registry_get "$registry" "$branch" control_polled_at)" \
|
|
1430
|
+
--arg decision "$decision" \
|
|
1431
|
+
--arg detail "$detail" \
|
|
1432
|
+
--arg run_id "$run_id" \
|
|
1433
|
+
--arg run_url "$run_url" \
|
|
1434
|
+
--arg written_at "$(date +%s)" \
|
|
1435
|
+
'{schema: $schema, branch: $branch, engine: $engine, status: $status,
|
|
1436
|
+
pause_reason: $pause_reason, usage_resume_at: $usage_resume_at,
|
|
1437
|
+
park_loop_cycles: $park_loop_cycles,
|
|
1438
|
+
resume_max_question_index: $resume_max_question_index,
|
|
1439
|
+
auto_resumes: $auto_resumes, stall_restarts: $stall_restarts,
|
|
1440
|
+
chain: $chain, control_polled_at: $control_polled_at,
|
|
1441
|
+
decision: $decision, detail: $detail,
|
|
1442
|
+
run_id: $run_id, run_url: $run_url, written_at: $written_at}' \
|
|
1443
|
+
>"$tmp" 2>/dev/null && mv "$tmp" "$out" 2>/dev/null; then
|
|
1444
|
+
return 0
|
|
1445
|
+
fi
|
|
1446
|
+
rm -f "$tmp"
|
|
1447
|
+
return 1
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
# hr_remote_status_get <status_json> <key> -> the value
|
|
1451
|
+
#
|
|
1452
|
+
# 0 with the value printed; 1 when <key> is absent; 2 when the file is
|
|
1453
|
+
# unreadable, is not JSON, or its `schema` is not `HR_REMOTE_STATE_SCHEMA` —
|
|
1454
|
+
# the bundle is then treated as absent, whatever <key> holds.
|
|
1455
|
+
hr_remote_status_get() {
|
|
1456
|
+
local file="${1-}" key="${2-}" out
|
|
1457
|
+
[ -n "$file" ] && [ -r "$file" ] || return 2
|
|
1458
|
+
hr_remote_names_var
|
|
1459
|
+
# One `jq`, one marker character: `S` a schema it does not recognise, `A` an
|
|
1460
|
+
# absent key, `V` a value — an exit status cannot carry three outcomes.
|
|
1461
|
+
out=$(jq -r --arg k "$key" --arg s "$HR_REMOTE_STATE_SCHEMA" '
|
|
1462
|
+
if type != "object" or .schema != $s then "S"
|
|
1463
|
+
elif (.[$k] | type) == "string" then "V" + .[$k]
|
|
1464
|
+
else "A" end' "$file" 2>/dev/null) || return 2
|
|
1465
|
+
case "$out" in
|
|
1466
|
+
V*) printf '%s\n' "${out#V}" ;;
|
|
1467
|
+
A) return 1 ;;
|
|
1468
|
+
*) return 2 ;;
|
|
1469
|
+
esac
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
# hr_remote_bundle_write <root> <branch> <registry_file> <out_dir>
|
|
1473
|
+
#
|
|
1474
|
+
# Assembles the format above in <out_dir>, which must be absent or empty, from
|
|
1475
|
+
# <root>'s configured state directory. `status.json` is the job's own
|
|
1476
|
+
# `autonomous_logs/remote_status.json` when present; otherwise it is written
|
|
1477
|
+
# from <branch>'s registry record with decision `stop`, because a job that never
|
|
1478
|
+
# wrote its status never decided to continue. 0 written; 1 a missing argument,
|
|
1479
|
+
# a non-empty <out_dir> or a failed copy; 2 <root>'s configuration unresolvable.
|
|
1480
|
+
hr_remote_bundle_write() {
|
|
1481
|
+
local root="${1-}" branch="${2-}" registry="${3-}" out="${4-}" state base clarify
|
|
1482
|
+
[ -n "$root" ] && [ -n "$branch" ] && [ -n "$registry" ] && [ -n "$out" ] || return 1
|
|
1483
|
+
state=$(hr_state_dir "$root") || return 2
|
|
1484
|
+
hr_remote_names_var
|
|
1485
|
+
base="${root%/}/$state"
|
|
1486
|
+
out=${out%/}
|
|
1487
|
+
if [ -e "$out" ]; then
|
|
1488
|
+
[ -d "$out" ] || return 1
|
|
1489
|
+
[ -z "$(ls -A "$out" 2>/dev/null)" ] || return 1
|
|
1490
|
+
fi
|
|
1491
|
+
mkdir -p "$out" 2>/dev/null || return 1
|
|
1492
|
+
|
|
1493
|
+
if [ -f "$base/$HR_REMOTE_STATUS_SOURCE" ]; then
|
|
1494
|
+
cp "$base/$HR_REMOTE_STATUS_SOURCE" "$out/$HR_REMOTE_STATUS_FILE" 2>/dev/null || return 1
|
|
1495
|
+
else
|
|
1496
|
+
hr_remote_status_write "$registry" "$branch" "$out/$HR_REMOTE_STATUS_FILE" stop \
|
|
1497
|
+
"the job wrote no status; derived from the registry record when the bundle was saved" || return 1
|
|
1498
|
+
fi
|
|
1499
|
+
if [ -d "$base/$HR_REMOTE_CLARIFY_DIR/$branch" ]; then
|
|
1500
|
+
clarify="$out/$HR_REMOTE_CLARIFY_DIR/$branch"
|
|
1501
|
+
mkdir -p "${clarify%/*}" 2>/dev/null || return 1
|
|
1502
|
+
# No trailing slash on the source: BSD `cp -R dir/` copies the contents instead.
|
|
1503
|
+
cp -R "$base/$HR_REMOTE_CLARIFY_DIR/$branch" "$clarify" 2>/dev/null || return 1
|
|
1504
|
+
fi
|
|
1505
|
+
if [ -f "$base/$HR_REMOTE_PAUSE_FILE" ]; then
|
|
1506
|
+
cp "$base/$HR_REMOTE_PAUSE_FILE" "$out/$HR_REMOTE_PAUSE_FILE" 2>/dev/null || return 1
|
|
1507
|
+
fi
|
|
1508
|
+
if [ -f "$base/$HR_REMOTE_WALKER_SOURCE" ]; then
|
|
1509
|
+
cp "$base/$HR_REMOTE_WALKER_SOURCE" "$out/$HR_REMOTE_WALKER_FILE" 2>/dev/null || return 1
|
|
1510
|
+
fi
|
|
1511
|
+
if [ -f "$base/$HR_REMOTE_LOGS_DIR/$branch.log" ]; then
|
|
1512
|
+
cp "$base/$HR_REMOTE_LOGS_DIR/$branch.log" "$out/$HR_REMOTE_LOG_FILE" 2>/dev/null || return 1
|
|
1513
|
+
fi
|
|
1514
|
+
return 0
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
# hr_remote_bundle_restore <bundle_dir> <root> <branch> <mode>
|
|
1518
|
+
#
|
|
1519
|
+
# <mode> `job` places the clarification directory, `PAUSE_PROGRESS.md`, the
|
|
1520
|
+
# walker state (back under its dotted name) and `status.json` (as
|
|
1521
|
+
# `autonomous_logs/remote_status.json`); `mirror` places the first two only. The
|
|
1522
|
+
# run log is placed by neither.
|
|
1523
|
+
#
|
|
1524
|
+
# THE CLARIFICATION DIRECTORY IS REPLACED WHOLESALE, AND NOTHING IS DELETED. An
|
|
1525
|
+
# existing target is moved aside with one `mv` into
|
|
1526
|
+
# `autonomous_logs/remote_superseded/<epoch>/clarifications/<branch>` (`<epoch>-<n>`
|
|
1527
|
+
# when an earlier restore took that second) before the
|
|
1528
|
+
# bundle's copy goes in, so a stale local pair cannot survive and no recursive
|
|
1529
|
+
# removal is ever shelled out. A bundle carrying no clarification directory
|
|
1530
|
+
# leaves the target alone: in a mirror it may hold an answer not yet relayed.
|
|
1531
|
+
#
|
|
1532
|
+
# 0 restored; 1 a missing argument, an unknown <mode> or a failed copy; 2 —
|
|
1533
|
+
# touching nothing — when the bundle is unrecognised (no readable `status.json`,
|
|
1534
|
+
# a schema other than `HR_REMOTE_STATE_SCHEMA`, or a `branch` other than
|
|
1535
|
+
# <branch>) or <root>'s configuration is unresolvable.
|
|
1536
|
+
hr_remote_bundle_restore() {
|
|
1537
|
+
local bundle="${1-}" root="${2-}" branch="${3-}" mode="${4-}"
|
|
1538
|
+
local state base named target epoch aside n tmp
|
|
1539
|
+
[ -n "$bundle" ] && [ -n "$root" ] && [ -n "$branch" ] || return 1
|
|
1540
|
+
case "$mode" in
|
|
1541
|
+
job|mirror) ;;
|
|
1542
|
+
*) return 1 ;;
|
|
1543
|
+
esac
|
|
1544
|
+
hr_remote_names_var
|
|
1545
|
+
bundle=${bundle%/}
|
|
1546
|
+
named=$(hr_remote_status_get "$bundle/$HR_REMOTE_STATUS_FILE" branch) || return 2
|
|
1547
|
+
[ "$named" = "$branch" ] || return 2
|
|
1548
|
+
state=$(hr_state_dir "$root") || return 2
|
|
1549
|
+
base="${root%/}/$state"
|
|
1550
|
+
|
|
1551
|
+
if [ -d "$bundle/$HR_REMOTE_CLARIFY_DIR/$branch" ]; then
|
|
1552
|
+
target="$base/$HR_REMOTE_CLARIFY_DIR/$branch"
|
|
1553
|
+
if [ -e "$target" ]; then
|
|
1554
|
+
epoch=$(date +%s)
|
|
1555
|
+
aside="$base/$HR_REMOTE_SUPERSEDED_DIR/$epoch"
|
|
1556
|
+
n=0
|
|
1557
|
+
while [ -e "$aside/$HR_REMOTE_CLARIFY_DIR/$branch" ]; do
|
|
1558
|
+
n=$((n + 1))
|
|
1559
|
+
aside="$base/$HR_REMOTE_SUPERSEDED_DIR/$epoch-$n"
|
|
1560
|
+
done
|
|
1561
|
+
aside="$aside/$HR_REMOTE_CLARIFY_DIR/$branch"
|
|
1562
|
+
mkdir -p "${aside%/*}" 2>/dev/null || return 1
|
|
1563
|
+
mv "$target" "$aside" 2>/dev/null || return 1
|
|
1564
|
+
fi
|
|
1565
|
+
mkdir -p "${target%/*}" 2>/dev/null || return 1
|
|
1566
|
+
cp -R "$bundle/$HR_REMOTE_CLARIFY_DIR/$branch" "$target" 2>/dev/null || return 1
|
|
1567
|
+
fi
|
|
1568
|
+
if [ -f "$bundle/$HR_REMOTE_PAUSE_FILE" ]; then
|
|
1569
|
+
mkdir -p "$base" 2>/dev/null || return 1
|
|
1570
|
+
cp "$bundle/$HR_REMOTE_PAUSE_FILE" "$base/$HR_REMOTE_PAUSE_FILE" 2>/dev/null || return 1
|
|
1571
|
+
fi
|
|
1572
|
+
[ "$mode" = job ] || return 0
|
|
1573
|
+
|
|
1574
|
+
if [ -f "$bundle/$HR_REMOTE_WALKER_FILE" ]; then
|
|
1575
|
+
mkdir -p "$base" 2>/dev/null || return 1
|
|
1576
|
+
cp "$bundle/$HR_REMOTE_WALKER_FILE" "$base/$HR_REMOTE_WALKER_SOURCE" 2>/dev/null || return 1
|
|
1577
|
+
fi
|
|
1578
|
+
mkdir -p "$base/$HR_REMOTE_LOGS_DIR" 2>/dev/null || return 1
|
|
1579
|
+
tmp=$(mktemp "$base/$HR_REMOTE_STATUS_SOURCE.tmp.XXXXXX" 2>/dev/null) || return 1
|
|
1580
|
+
if cp "$bundle/$HR_REMOTE_STATUS_FILE" "$tmp" 2>/dev/null \
|
|
1581
|
+
&& mv "$tmp" "$base/$HR_REMOTE_STATUS_SOURCE" 2>/dev/null; then
|
|
1582
|
+
return 0
|
|
1583
|
+
fi
|
|
1584
|
+
rm -f "$tmp"
|
|
1585
|
+
return 1
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1011
1588
|
# ---------------------------------------------------------------------------
|
|
1012
1589
|
# THE MACHINE-LEVEL USAGE LANE.
|
|
1013
1590
|
#
|