autonomous-sdlc-harness 0.4.1 → 0.5.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 +101 -13
- package/dist/commands/init.js.map +1 -1
- package/dist/config/check.js +1 -1
- package/dist/config/check.js.map +1 -1
- package/dist/config/model.js +17 -1
- package/dist/config/model.js.map +1 -1
- package/dist/core/writer.js +9 -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 +169 -15
- package/dist/doctor/checks.js.map +1 -1
- package/dist/generators/githubWorkflows.js +115 -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 +51 -7
- package/dist/remote/githubActions.js.map +1 -1
- package/package.json +1 -1
- package/templates/README.md +1 -1
- package/templates/github/workflows/harness-resume.yml +23 -5
- package/templates/github/workflows/harness-run.yml +68 -30
- package/templates/github/workflows/harness-trigger.yml +143 -0
- package/templates/repo/gitignore +5 -0
- package/templates/scripts/autonomous-watcher.sh +95 -248
- package/templates/scripts/create-worktree.sh +52 -10
- package/templates/scripts/lib/harness-run-lib.sh +553 -26
- package/templates/scripts/remote-run.sh +1198 -114
|
@@ -1,12 +1,16 @@
|
|
|
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`), places a dropped
|
|
7
|
+
# artifact in a working copy and commits and pushes it, and derives the
|
|
8
|
+
# anchors (main checkout, work root, worktree directory, repo slug,
|
|
6
9
|
# state-dir paths) the scripts would otherwise each re-derive slightly
|
|
7
10
|
# differently. It also implements the run registry's reads and writes for the
|
|
8
11
|
# 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
|
|
12
|
+
# with the one writer and restorer every remote-execution consumer shares, and
|
|
13
|
+
# the GitHub route a job-side notification names beside its local command.
|
|
10
14
|
#
|
|
11
15
|
# WHO SOURCES THIS, AND HOW. Every script in the configured `scriptsDir` that
|
|
12
16
|
# needs this library sources it by a path computed from `${BASH_SOURCE[0]}` —
|
|
@@ -62,21 +66,35 @@
|
|
|
62
66
|
# `.stale.*` move-aside and `.break` mutex while a stale one is broken)
|
|
63
67
|
# and the temp files
|
|
64
68
|
# `.registry.*` in the registry's own directory. Written only by
|
|
65
|
-
# `hr_registry_init`, `hr_registry_set
|
|
66
|
-
# `
|
|
69
|
+
# `hr_registry_init`, `hr_registry_set`, `hr_remote_record_init` (through
|
|
70
|
+
# `hr_registry_set`) and the `hr_registry_lock` / `hr_registry_unlock`
|
|
71
|
+
# pair `hr_registry_set` calls.
|
|
67
72
|
# 3. THE REMOTE STATE BUNDLE writes the files its format lists. Fence: inside
|
|
68
73
|
# `<root>/<state_dir>/` (resolved through `hr_state_dir`), only
|
|
69
74
|
# `autonomous_logs/remote_status.json`, `clarifications/<branch>/`,
|
|
70
|
-
# `PAUSE_PROGRESS.md`, `.flow_walker_state
|
|
71
|
-
# `autonomous_logs/remote_superseded
|
|
72
|
-
#
|
|
75
|
+
# `PAUSE_PROGRESS.md`, `.flow_walker_state`, the move-aside directory
|
|
76
|
+
# `autonomous_logs/remote_superseded/` and, only where nothing exists yet,
|
|
77
|
+
# files under the eight planning paths `hr_remote_planning_paths` assigns;
|
|
78
|
+
# outside it, only the caller-named `<out_dir>` (its `planning/` included)
|
|
79
|
+
# of `hr_remote_bundle_write` and the caller-named `<out_json>`
|
|
73
80
|
# of `hr_remote_status_write`. Written only by `hr_remote_status_write`,
|
|
74
81
|
# `hr_remote_bundle_write` and `hr_remote_bundle_restore`, and nothing
|
|
75
82
|
# there but a writer's own failed temp file is ever removed.
|
|
83
|
+
# 4. THE ARTIFACT PLACEMENT writes one artifact into a working copy. Fence:
|
|
84
|
+
# the caller-named `<worktree>/<rel>`, its parent directories and that
|
|
85
|
+
# path's index entry, plus whatever the two caller-named wrappers do.
|
|
86
|
+
# Written only by `hr_place_artifact`, `hr_commit_placed` and
|
|
87
|
+
# `hr_push_landed`.
|
|
88
|
+
#
|
|
89
|
+
# MIRRORS OF `cli/src/remote/githubActions.ts`, which owns these names; a
|
|
90
|
+
# rename there is an edit here, byte for byte:
|
|
91
|
+
# HR_REMOTE_WORKFLOW_RUN_FILE mirrors WORKFLOW_RUN_FILE
|
|
92
|
+
# HR_REMOTE_STATE_ARTIFACT mirrors STATE_ARTIFACT_NAME
|
|
76
93
|
#
|
|
77
94
|
# 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
|
|
95
|
+
# `hr_remote_record_init`, `hr_registry_lock`, `hr_registry_unlock`, `hr_remote_status_write`,
|
|
96
|
+
# `hr_remote_bundle_write`, `hr_remote_bundle_restore`, `hr_place_artifact`,
|
|
97
|
+
# `hr_commit_placed` or `hr_push_landed` function still gets a library that only reads. The
|
|
80
98
|
# lane's ceilings are the only environment values here that carry policy, because
|
|
81
99
|
# the lane is machine-scoped and has no configuration key to carry them; each is
|
|
82
100
|
# named where it is used. `XDG_STATE_HOME`, `XDG_CONFIG_HOME`, `XDG_CACHE_HOME`,
|
|
@@ -171,7 +189,10 @@
|
|
|
171
189
|
# `mktemp`'s and `jq`'s own stderr on a failed write to the caller, as the
|
|
172
190
|
# watcher's bodies they replaced did — that stream is the watcher's log — and
|
|
173
191
|
# add one line of their own, naming the lock, when `hr_registry_set` cannot
|
|
174
|
-
# take the registry lock.
|
|
192
|
+
# take the registry lock. The artifact placement's three writers likewise leave
|
|
193
|
+
# `mkdir`'s, `cp`'s, `git add`'s and both wrappers' own output on their stdout
|
|
194
|
+
# and stderr for the caller to redirect into its log.
|
|
195
|
+
# That silence is why the lane reports a lock it BROKE through a
|
|
175
196
|
# variable instead of a log line — the caller owns the log.
|
|
176
197
|
#
|
|
177
198
|
# NAMING. Every function is prefixed `hr_`; every variable this file touches
|
|
@@ -179,11 +200,16 @@
|
|
|
179
200
|
# value WITHOUT a command substitution — a `$(…)` forks a subshell, and the
|
|
180
201
|
# watcher calls these on every tick: `HR_CFG_PID`, `HR_CFG_ROOT`,
|
|
181
202
|
# `HR_CFG_STATE`, `HR_CFG_FILE`, `HR_CFG_SCALARS`, `HR_CFG_LISTS`,
|
|
182
|
-
# `HR_CFG_VALUE`, `HR_CFG_COMMAND_KEYS`, `HR_PROTECTED_DEFAULT`,
|
|
203
|
+
# `HR_CFG_VALUE`, `HR_CFG_COMMAND_KEYS`, `HR_PROTECTED_DEFAULT`,
|
|
204
|
+
# `HR_INBOX_KIND`, `HR_INBOX_BRANCH`, the branch derivation's
|
|
205
|
+
# `HR_BRANCH_SLUG_MAX`, `HR_BRANCH_SUFFIX_MAX`, `HR_TAKEN_REMOTE`,
|
|
206
|
+
# `HR_TAKEN_ARTIFACTS` and `HR_TAKEN_WHY`, and the lane's
|
|
183
207
|
# `HR_LANE_RANK`, `HR_LANE_STATE`, `HR_LANE_RESUME_AT`, `HR_LANE_OBSERVED_AT`,
|
|
184
208
|
# `HR_LANE_OBSERVED_REPO`, `HR_LANE_OWNER_SLUG`, `HR_LANE_OWNER_PID`,
|
|
185
209
|
# `HR_LANE_OWNER_AT` and `HR_LANE_BROKEN_OWNER`, and the remote state bundle's
|
|
186
|
-
# names, which `hr_remote_names_var` assigns
|
|
210
|
+
# names, which `hr_remote_names_var` assigns, with `HR_REMOTE_PLANNING_PATHS`
|
|
211
|
+
# (`hr_remote_planning_paths`) and `HR_REMOTE_PLANNING_PLACED` /
|
|
212
|
+
# `HR_REMOTE_PLANNING_KEPT` (`hr_remote_bundle_restore`). Every one of them is assigned
|
|
187
213
|
# before it is read by the function that owns it, so an inherited value from a
|
|
188
214
|
# parent process is overwritten rather than believed.
|
|
189
215
|
#
|
|
@@ -605,6 +631,7 @@ hr_config_load() {
|
|
|
605
631
|
s("phases.qa"; try (.phases.qa | if type == "boolean" or . == null then . else "invalid" end) catch null),
|
|
606
632
|
s("phases.docs"; try (.phases.docs | if type == "boolean" or . == null then . else "invalid" end) catch null),
|
|
607
633
|
s("execution.target"; try .execution.target catch null),
|
|
634
|
+
s("forge"; try .forge catch null),
|
|
608
635
|
s("protectedBranches.present";
|
|
609
636
|
try (if (.protectedBranches | type) == "array" then "1" else null end) catch null),
|
|
610
637
|
l("protectedBranches"; try .protectedBranches catch null)
|
|
@@ -871,6 +898,68 @@ hr_execution_target() {
|
|
|
871
898
|
return 2
|
|
872
899
|
}
|
|
873
900
|
|
|
901
|
+
# `forge` — which code-hosting platform the flow integrates with: `github`,
|
|
902
|
+
# `gitlab` or `none`. THE ONE READER OF THE KEY IN THIS FAMILY, and the shell
|
|
903
|
+
# mirror of `cli/src/config/model.ts` → `FORGE_KINDS`: change the enum there and
|
|
904
|
+
# here together. 1 — printing nothing — when the key is absent: "not yet
|
|
905
|
+
# decided", which is not `none`, and the schema withholds a default on purpose.
|
|
906
|
+
# 2 — printing nothing — when the configuration is unresolvable or the value is
|
|
907
|
+
# outside the enum, a refusal rather than a guess.
|
|
908
|
+
hr_forge() {
|
|
909
|
+
local root="${1-}"
|
|
910
|
+
hr_config_load "$root" || return 2
|
|
911
|
+
hr_cfg_scalar_var "forge" || return 1
|
|
912
|
+
case "$HR_CFG_VALUE" in
|
|
913
|
+
github|gitlab|none)
|
|
914
|
+
printf '%s\n' "$HR_CFG_VALUE"
|
|
915
|
+
return 0
|
|
916
|
+
;;
|
|
917
|
+
esac
|
|
918
|
+
return 2
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
# ---------------------------------------------------------------------------
|
|
922
|
+
# Inbox routing — the one owner of the drop filename patterns.
|
|
923
|
+
# ---------------------------------------------------------------------------
|
|
924
|
+
|
|
925
|
+
# Route one inbox filename: set `HR_INBOX_KIND` (`task` | `user_review` |
|
|
926
|
+
# `docs`) and `HR_INBOX_BRANCH`, and return 0; on no match return 1 with both
|
|
927
|
+
# empty. Takes a basename, not a path.
|
|
928
|
+
#
|
|
929
|
+
# The task-prompt pattern is tested FIRST (the more specific suffix), but the
|
|
930
|
+
# anchored SUFFIX regexes are mutually exclusive by construction: a filename
|
|
931
|
+
# cannot end in more than one of `_task_prompt.md` / `_review[_<n>].md` /
|
|
932
|
+
# `_docs.md`, so a branch whose own name contains `review` or `task_prompt`
|
|
933
|
+
# cannot be mis-routed — `foo_review_task_prompt.md` is the task engine on branch
|
|
934
|
+
# `foo_review`, and `foo_task_prompt_review.md` is the review engine on branch
|
|
935
|
+
# `foo_task_prompt`. POSIX leftmost-longest matching of the greedy `(.+)` derives
|
|
936
|
+
# the right branch from a round-suffixed name: `foo_review_2.md` -> branch `foo`
|
|
937
|
+
# (the `_2` is consumed by the optional `(_[0-9]+)?`), while
|
|
938
|
+
# `foo_review_2_review.md` -> branch `foo_review_2`. THE WATCHER DERIVES ONLY THE
|
|
939
|
+
# BRANCH, never the round: the engine resolves the latest round itself, inside
|
|
940
|
+
# the working copy, which is why nothing here has to remember one.
|
|
941
|
+
hr_inbox_route_var() {
|
|
942
|
+
local fname="${1-}" branch
|
|
943
|
+
HR_INBOX_KIND=""
|
|
944
|
+
HR_INBOX_BRANCH=""
|
|
945
|
+
[ -n "$fname" ] || return 1
|
|
946
|
+
branch="$(printf '%s' "$fname" | sed -nE 's/^(.+)_task_prompt\.md$/\1/p')"
|
|
947
|
+
if [ -n "$branch" ]; then
|
|
948
|
+
HR_INBOX_KIND="task"
|
|
949
|
+
else
|
|
950
|
+
branch="$(printf '%s' "$fname" | sed -nE 's/^(.+)_review(_[0-9]+)?\.md$/\1/p')"
|
|
951
|
+
if [ -n "$branch" ]; then
|
|
952
|
+
HR_INBOX_KIND="user_review"
|
|
953
|
+
else
|
|
954
|
+
branch="$(printf '%s' "$fname" | sed -nE 's/^(.+)_docs\.md$/\1/p')"
|
|
955
|
+
[ -n "$branch" ] || return 1
|
|
956
|
+
HR_INBOX_KIND="docs"
|
|
957
|
+
fi
|
|
958
|
+
fi
|
|
959
|
+
HR_INBOX_BRANCH="$branch"
|
|
960
|
+
return 0
|
|
961
|
+
}
|
|
962
|
+
|
|
874
963
|
# ---------------------------------------------------------------------------
|
|
875
964
|
# The protected-branch trichotomy.
|
|
876
965
|
# ---------------------------------------------------------------------------
|
|
@@ -1315,30 +1404,351 @@ hr_registry_branches() {
|
|
|
1315
1404
|
jq -r '.runs | keys[]' "$file" 2>/dev/null
|
|
1316
1405
|
}
|
|
1317
1406
|
|
|
1407
|
+
# hr_remote_record_init <file> <branch> <worktree> <log_path> <engine>
|
|
1408
|
+
# The fields a remote run's record starts with — the one list, written by the
|
|
1409
|
+
# 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
|
|
1410
|
+
# once its run exists. 1 when the write failed.
|
|
1411
|
+
hr_remote_record_init() {
|
|
1412
|
+
local file="${1-}" branch="${2-}"
|
|
1413
|
+
hr_registry_set "$file" "$branch" \
|
|
1414
|
+
worktree "${3-}" log_path "${4-}" engine "${5-}" \
|
|
1415
|
+
execution github-actions \
|
|
1416
|
+
started_at "$(date '+%Y-%m-%dT%H:%M:%S')" \
|
|
1417
|
+
pid "" remote_dispatched_at "" \
|
|
1418
|
+
stall_restarts 0 stall_warned "" stall_killing "" \
|
|
1419
|
+
paused_by "" usage_resume_at "" \
|
|
1420
|
+
resume_kind "" park_loop_cycles 0
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
# ---------------------------------------------------------------------------
|
|
1424
|
+
# DERIVING A BRANCH NAME FROM A TITLE.
|
|
1425
|
+
#
|
|
1426
|
+
# THE RULE. A title becomes a branch name by a fixed fold, with no model and no
|
|
1427
|
+
# confirmation step: lowercase A–Z, turn every run of characters outside
|
|
1428
|
+
# `[a-z0-9]` into one `_`, trim `_` from both ends, cut to `HR_BRANCH_SLUG_MAX`
|
|
1429
|
+
# and trim a trailing `_` the cut exposed. An empty result takes the caller's
|
|
1430
|
+
# <fallback> (`issue_<number>` for an issue, `task_<run id>` for a dispatch). A
|
|
1431
|
+
# taken name takes the lowest free `<name>_<n>`, `_2` through
|
|
1432
|
+
# `HR_BRANCH_SUFFIX_MAX`; the first branch carries no suffix (`Version bump` →
|
|
1433
|
+
# `version_bump`), and `_2` reads as "the second".
|
|
1434
|
+
#
|
|
1435
|
+
# - The fold is ASCII-only under `LC_ALL=C`, so `é` is a separator, never a
|
|
1436
|
+
# letter — `hr_repo_slug`'s precedent. A locale-dependent fold would derive
|
|
1437
|
+
# different names on different runners; a lowercase ASCII name passes every
|
|
1438
|
+
# `git check-ref-format` rule and cannot collide by case on macOS or Windows.
|
|
1439
|
+
# - The cap is 60, cut before the suffix. The name becomes a working-copy
|
|
1440
|
+
# directory component (`<projectName>-<branch>`) and prefixes artifact names
|
|
1441
|
+
# (`<branch>_task_prompt.md`); 60 keeps each far under a 255-byte file-name
|
|
1442
|
+
# limit and readable in the Actions run list. GitHub documents no ref limit.
|
|
1443
|
+
#
|
|
1444
|
+
# THIS SECTION ONLY READS. It fetches nothing and creates no branch or file; a
|
|
1445
|
+
# caller that wants `origin` fresh fetches first. A registry is read only when
|
|
1446
|
+
# it already exists, because `hr_registry_get` creates an absent one.
|
|
1447
|
+
# ---------------------------------------------------------------------------
|
|
1448
|
+
|
|
1449
|
+
# The two limits the rule above names.
|
|
1450
|
+
hr_branch_limits_var() {
|
|
1451
|
+
HR_BRANCH_SLUG_MAX=60
|
|
1452
|
+
HR_BRANCH_SUFFIX_MAX=99
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
# hr_branch_slug <text> — print the slug and return 0; print nothing and return
|
|
1456
|
+
# 1 when the fold leaves nothing (`🚀🚀`).
|
|
1457
|
+
hr_branch_slug() {
|
|
1458
|
+
# `[!a-z0-9]` is a collation range: under a UTF-8 locale it would keep `é`.
|
|
1459
|
+
local LC_ALL=C
|
|
1460
|
+
local text="${1-}" slug
|
|
1461
|
+
hr_branch_limits_var
|
|
1462
|
+
slug=$(printf '%s' "$text" | LC_ALL=C tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
|
|
1463
|
+
slug=${slug//[!a-z0-9]/_}
|
|
1464
|
+
while :; do
|
|
1465
|
+
case "$slug" in
|
|
1466
|
+
*__*) slug=${slug//__/_} ;;
|
|
1467
|
+
*) break ;;
|
|
1468
|
+
esac
|
|
1469
|
+
done
|
|
1470
|
+
slug=${slug#_}
|
|
1471
|
+
slug=${slug%_}
|
|
1472
|
+
if [ "${#slug}" -gt "$HR_BRANCH_SLUG_MAX" ]; then
|
|
1473
|
+
slug=${slug:0:$HR_BRANCH_SLUG_MAX}
|
|
1474
|
+
slug=${slug%_}
|
|
1475
|
+
fi
|
|
1476
|
+
[ -n "$slug" ] || return 1
|
|
1477
|
+
printf '%s\n' "$slug"
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
# Read, once, the two listings every `taken` judgement compares against:
|
|
1481
|
+
# `HR_TAKEN_REMOTE` — each branch on `origin`, lowercased — and
|
|
1482
|
+
# `HR_TAKEN_ARTIFACTS` — the basename of every file and directory under
|
|
1483
|
+
# `<state_dir>` on `origin/<defaultBranch>`; one per line in both. 0 when both
|
|
1484
|
+
# were read; 2, with `HR_TAKEN_WHY` naming which, when either could not be.
|
|
1485
|
+
hr_branch_taken_lists_var() {
|
|
1486
|
+
local LC_ALL=C
|
|
1487
|
+
local root="${1-}" heads state default tree line tab
|
|
1488
|
+
tab=$(printf '\t')
|
|
1489
|
+
HR_TAKEN_REMOTE=""
|
|
1490
|
+
HR_TAKEN_ARTIFACTS=""
|
|
1491
|
+
HR_TAKEN_WHY=""
|
|
1492
|
+
if ! heads=$(git -C "$root" ls-remote --heads origin 2>/dev/null); then
|
|
1493
|
+
HR_TAKEN_WHY="the branches on origin could not be listed"
|
|
1494
|
+
return 2
|
|
1495
|
+
fi
|
|
1496
|
+
while IFS= read -r line; do
|
|
1497
|
+
line=${line#*"$tab"refs/heads/}
|
|
1498
|
+
[ -n "$line" ] || continue
|
|
1499
|
+
HR_TAKEN_REMOTE="$HR_TAKEN_REMOTE$line
|
|
1500
|
+
"
|
|
1501
|
+
done <<EOF
|
|
1502
|
+
$heads
|
|
1503
|
+
EOF
|
|
1504
|
+
HR_TAKEN_REMOTE=$(printf '%s' "$HR_TAKEN_REMOTE" | LC_ALL=C tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
|
|
1505
|
+
|
|
1506
|
+
if ! state=$(hr_state_dir "$root") || ! default=$(hr_default_branch "$root"); then
|
|
1507
|
+
HR_TAKEN_WHY="the configuration could not be read"
|
|
1508
|
+
return 2
|
|
1509
|
+
fi
|
|
1510
|
+
if ! git -C "$root" rev-parse --verify --quiet "refs/remotes/origin/$default^{commit}" >/dev/null 2>&1; then
|
|
1511
|
+
HR_TAKEN_WHY="origin/$default is not present"
|
|
1512
|
+
return 2
|
|
1513
|
+
fi
|
|
1514
|
+
if ! tree=$(git -C "$root" ls-tree -r -t --name-only "refs/remotes/origin/$default" -- "$state/" 2>/dev/null); then
|
|
1515
|
+
HR_TAKEN_WHY="the tree of origin/$default could not be read"
|
|
1516
|
+
return 2
|
|
1517
|
+
fi
|
|
1518
|
+
while IFS= read -r line; do
|
|
1519
|
+
[ -n "$line" ] || continue
|
|
1520
|
+
HR_TAKEN_ARTIFACTS="$HR_TAKEN_ARTIFACTS${line##*/}
|
|
1521
|
+
"
|
|
1522
|
+
done <<EOF
|
|
1523
|
+
$tree
|
|
1524
|
+
EOF
|
|
1525
|
+
return 0
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
# Judge <name> against the listings `hr_branch_taken_lists_var` last read. The
|
|
1529
|
+
# answers and `HR_TAKEN_WHY` are `hr_branch_name_taken`'s.
|
|
1530
|
+
hr_branch_taken_judge() {
|
|
1531
|
+
local LC_ALL=C
|
|
1532
|
+
local root="${1-}" name="${2-}" registry="${3-}" lower status nl
|
|
1533
|
+
nl='
|
|
1534
|
+
'
|
|
1535
|
+
HR_TAKEN_WHY=""
|
|
1536
|
+
status=0
|
|
1537
|
+
hr_branch_is_protected "$root" "$name" || status=$?
|
|
1538
|
+
case "$status" in
|
|
1539
|
+
0) HR_TAKEN_WHY="a protected branch"; return 0 ;;
|
|
1540
|
+
2) HR_TAKEN_WHY="the protected branches could not be resolved"; return 2 ;;
|
|
1541
|
+
esac
|
|
1542
|
+
lower=$(printf '%s' "$name" | LC_ALL=C tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz')
|
|
1543
|
+
case "$nl$HR_TAKEN_REMOTE$nl" in
|
|
1544
|
+
*"$nl$lower$nl"*) HR_TAKEN_WHY="a branch on origin"; return 0 ;;
|
|
1545
|
+
esac
|
|
1546
|
+
if git -C "$root" show-ref --verify --quiet "refs/heads/$name" 2>/dev/null; then
|
|
1547
|
+
HR_TAKEN_WHY="a local branch"
|
|
1548
|
+
return 0
|
|
1549
|
+
fi
|
|
1550
|
+
case "$nl$HR_TAKEN_ARTIFACTS" in
|
|
1551
|
+
*"$nl$name$nl"* | *"$nl${name}_task_prompt.md$nl"* | *"$nl${name}_story_plan.md$nl"* | *"$nl${name}_docs.md$nl"*)
|
|
1552
|
+
HR_TAKEN_WHY="a run's artifacts on the default branch"
|
|
1553
|
+
return 0
|
|
1554
|
+
;;
|
|
1555
|
+
esac
|
|
1556
|
+
if [ -n "$registry" ] && [ -f "$registry" ] && [ -n "$(hr_registry_get "$registry" "$name" branch)" ]; then
|
|
1557
|
+
HR_TAKEN_WHY="a run registry record"
|
|
1558
|
+
return 0
|
|
1559
|
+
fi
|
|
1560
|
+
return 1
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
# hr_branch_name_taken <root> <name> [<registry>] — 0 taken, 1 free, 2 cannot
|
|
1564
|
+
# tell. Sets `HR_TAKEN_WHY` to a short phrase naming the collision or the
|
|
1565
|
+
# failure. Taken: a protected name; a branch on `origin`, compared
|
|
1566
|
+
# case-insensitively; a local branch; under `<state_dir>` on
|
|
1567
|
+
# `origin/<defaultBranch>`, a directory named <name> or a file
|
|
1568
|
+
# `<name>_task_prompt.md`, `<name>_story_plan.md` or `<name>_docs.md` — which a
|
|
1569
|
+
# merged and deleted branch still leaves; a record in an existing <registry>.
|
|
1570
|
+
hr_branch_name_taken() {
|
|
1571
|
+
local root="${1-}" name="${2-}" registry="${3-}"
|
|
1572
|
+
HR_TAKEN_WHY=""
|
|
1573
|
+
if [ -z "$root" ] || [ -z "$name" ]; then
|
|
1574
|
+
HR_TAKEN_WHY="no branch name to judge"
|
|
1575
|
+
return 2
|
|
1576
|
+
fi
|
|
1577
|
+
hr_config_load "$root" || :
|
|
1578
|
+
hr_branch_taken_lists_var "$root" || return 2
|
|
1579
|
+
hr_branch_taken_judge "$root" "$name" "$registry"
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
# hr_derive_branch <root> <text> <fallback> [<registry>] — print the derived name
|
|
1583
|
+
# and return 0; return 2, printing nothing, when a `taken` judgement could not
|
|
1584
|
+
# tell or no base routes back to itself; return 3, printing nothing, when every
|
|
1585
|
+
# suffix through `HR_BRANCH_SUFFIX_MAX` is taken. Never a guessed name.
|
|
1586
|
+
#
|
|
1587
|
+
# THE BASE MUST ROUTE BACK TO ITSELF: `hr_inbox_route_var` on each drop filename
|
|
1588
|
+
# a run of that name produces has to give the base as its branch, so no derived
|
|
1589
|
+
# name makes the inbox patterns ambiguous. A base that does not is replaced by
|
|
1590
|
+
# <fallback> once.
|
|
1591
|
+
hr_derive_branch() {
|
|
1592
|
+
local root="${1-}" text="${2-}" fallback="${3-}" registry="${4-}"
|
|
1593
|
+
local base="" candidate suffix routes n status
|
|
1594
|
+
hr_branch_limits_var
|
|
1595
|
+
candidate=$(hr_branch_slug "$text") || candidate="$fallback"
|
|
1596
|
+
for candidate in "$candidate" "$fallback"; do
|
|
1597
|
+
[ -n "$candidate" ] || continue
|
|
1598
|
+
routes=0
|
|
1599
|
+
for suffix in _task_prompt.md _review.md _review_2.md _docs.md; do
|
|
1600
|
+
if ! hr_inbox_route_var "$candidate$suffix" || [ "$HR_INBOX_BRANCH" != "$candidate" ]; then
|
|
1601
|
+
routes=1
|
|
1602
|
+
break
|
|
1603
|
+
fi
|
|
1604
|
+
done
|
|
1605
|
+
if [ "$routes" -eq 0 ]; then
|
|
1606
|
+
base="$candidate"
|
|
1607
|
+
break
|
|
1608
|
+
fi
|
|
1609
|
+
done
|
|
1610
|
+
[ -n "$base" ] || return 2
|
|
1611
|
+
|
|
1612
|
+
hr_config_load "$root" || :
|
|
1613
|
+
hr_branch_taken_lists_var "$root" || return 2
|
|
1614
|
+
candidate="$base"
|
|
1615
|
+
n=1
|
|
1616
|
+
while :; do
|
|
1617
|
+
status=0
|
|
1618
|
+
hr_branch_taken_judge "$root" "$candidate" "$registry" || status=$?
|
|
1619
|
+
case "$status" in
|
|
1620
|
+
1) printf '%s\n' "$candidate"; return 0 ;;
|
|
1621
|
+
2) return 2 ;;
|
|
1622
|
+
esac
|
|
1623
|
+
n=$((n + 1))
|
|
1624
|
+
[ "$n" -le "$HR_BRANCH_SUFFIX_MAX" ] || return 3
|
|
1625
|
+
candidate="${base}_$n"
|
|
1626
|
+
done
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
# ---------------------------------------------------------------------------
|
|
1630
|
+
# THE ARTIFACT PLACEMENT.
|
|
1631
|
+
#
|
|
1632
|
+
# THE CONTRACT. The one placement the watcher's inbox pass and a job starting a
|
|
1633
|
+
# run both perform: copy a dropped artifact into a working copy, stage exactly
|
|
1634
|
+
# that path, skip an identical re-drop, commit through the caller-named commit
|
|
1635
|
+
# wrapper, push through the caller-named push wrapper, and read "landed" as
|
|
1636
|
+
# `origin/<branch>` equal to `HEAD`. Every step reports by exit status only;
|
|
1637
|
+
# what a failure means — log and launch anyway, or block the dispatch — is the
|
|
1638
|
+
# caller's decision. The wrapper paths are arguments because this library
|
|
1639
|
+
# resolves no sibling script. Write exception 4 in the header is this section's.
|
|
1640
|
+
# ---------------------------------------------------------------------------
|
|
1641
|
+
|
|
1642
|
+
# hr_task_prompt_rel <state_rel> <branch> — print the task prompt's
|
|
1643
|
+
# repo-relative path, with <state_rel>'s trailing `/` dropped.
|
|
1644
|
+
hr_task_prompt_rel() {
|
|
1645
|
+
local state_rel="${1-}" branch="${2-}"
|
|
1646
|
+
printf '%s/task_prompts/%s_task_prompt.md\n' "${state_rel%/}" "$branch"
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
# hr_task_prompt_subject <branch> — print the task prompt's commit subject. The
|
|
1650
|
+
# one producer of it; `.claude/context/conventions.md` → `## Commit-message
|
|
1651
|
+
# policy` lists it byte for byte.
|
|
1652
|
+
hr_task_prompt_subject() {
|
|
1653
|
+
printf 'chore: add task prompt for %s\n' "${1-}"
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
# hr_user_review_subject <branch> — print a user review round's commit subject.
|
|
1657
|
+
# The one producer of it, for the watcher's remote inbox pass and
|
|
1658
|
+
# `remote-run.sh review`.
|
|
1659
|
+
hr_user_review_subject() {
|
|
1660
|
+
printf 'chore: add user review for %s\n' "${1-}"
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
# hr_place_artifact <worktree> <src_file> <rel> — copy <src_file> to
|
|
1664
|
+
# <worktree>/<rel>, creating its parent. 0, or 1 on a failure.
|
|
1665
|
+
hr_place_artifact() {
|
|
1666
|
+
local worktree="${1-}" src="${2-}" rel="${3-}" dest
|
|
1667
|
+
[ -n "$worktree" ] && [ -n "$src" ] && [ -n "$rel" ] || return 1
|
|
1668
|
+
dest="$worktree/$rel"
|
|
1669
|
+
mkdir -p "${dest%/*}" || return 1
|
|
1670
|
+
cp "$src" "$dest" || return 1
|
|
1671
|
+
return 0
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
# hr_commit_placed <commit_wrapper> <worktree> <rel> <subject> — stage <rel> and
|
|
1675
|
+
# commit it through <commit_wrapper>. 0 committed; 3 nothing staged for <rel> (an
|
|
1676
|
+
# identical re-drop — nothing committed); 1 staging or the wrapper failed.
|
|
1677
|
+
hr_commit_placed() {
|
|
1678
|
+
local wrapper="${1-}" worktree="${2-}" rel="${3-}" subject="${4-}"
|
|
1679
|
+
[ -n "$wrapper" ] && [ -n "$worktree" ] && [ -n "$rel" ] && [ -n "$subject" ] || return 1
|
|
1680
|
+
git -C "$worktree" add -- "$rel" || return 1
|
|
1681
|
+
git -C "$worktree" diff --cached --quiet -- "$rel" && return 3
|
|
1682
|
+
"$wrapper" --repo "$worktree" "$rel" -- "$subject" || return 1
|
|
1683
|
+
return 0
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
# hr_push_landed <push_wrapper> <worktree> <branch> — run <push_wrapper>, then 0
|
|
1687
|
+
# only when `HEAD` and `refs/remotes/origin/<branch>` both resolve and are
|
|
1688
|
+
# equal; 1 otherwise. `push-branch.sh` exits 0 on every path, so its status is
|
|
1689
|
+
# never the answer.
|
|
1690
|
+
hr_push_landed() {
|
|
1691
|
+
local wrapper="${1-}" worktree="${2-}" branch="${3-}" head upstream
|
|
1692
|
+
[ -n "$wrapper" ] && [ -n "$worktree" ] && [ -n "$branch" ] || return 1
|
|
1693
|
+
"$wrapper" "$worktree"
|
|
1694
|
+
head=$(git -C "$worktree" rev-parse --verify --quiet HEAD) || return 1
|
|
1695
|
+
upstream=$(git -C "$worktree" rev-parse --verify --quiet "refs/remotes/origin/$branch") || return 1
|
|
1696
|
+
[ -n "$head" ] && [ "$head" = "$upstream" ]
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1318
1699
|
# ---------------------------------------------------------------------------
|
|
1319
1700
|
# THE REMOTE STATE BUNDLE.
|
|
1320
1701
|
#
|
|
1321
1702
|
# THE FORMAT OF RECORD. What a remote job carries across a job boundary and
|
|
1322
|
-
# reports back, uploaded as the Actions artifact `
|
|
1323
|
-
# below is a variable `hr_remote_names_var` assigns
|
|
1703
|
+
# reports back, uploaded as the Actions artifact `HR_REMOTE_STATE_ARTIFACT`
|
|
1704
|
+
# names. Every name below is a variable `hr_remote_names_var` assigns, and it
|
|
1705
|
+
# also assigns `HR_REMOTE_WORKFLOW_RUN_FILE`, the run workflow's file name, for
|
|
1706
|
+
# the GitHub-route producers `hr_github_answer_route` and
|
|
1707
|
+
# `hr_github_resume_route`. Those two are mirrors of the header's table; no
|
|
1708
|
+
# function spells either one, or any other name here.
|
|
1324
1709
|
#
|
|
1325
1710
|
# <bundle>/status.json the job's record, fixed schema below
|
|
1326
1711
|
# <bundle>/clarifications/<branch>/ the whole branch directory, answered/ included
|
|
1327
1712
|
# <bundle>/PAUSE_PROGRESS.md when present
|
|
1328
1713
|
# <bundle>/flow_walker_state <state_dir>/.flow_walker_state, WITHOUT its dot
|
|
1329
1714
|
# <bundle>/run.log <state_dir>/autonomous_logs/<branch>.log; never restored
|
|
1715
|
+
# <bundle>/planning/<path> each of these under <state_dir>, when present:
|
|
1716
|
+
# story_plans/<branch>_story_plan.md
|
|
1717
|
+
# task_plans/<branch>
|
|
1718
|
+
# ui_test_plans/<branch>_ui_test_plan.md
|
|
1719
|
+
# ui_test_plans/<branch>
|
|
1720
|
+
# task_plan_reviews/<branch>
|
|
1721
|
+
# business_parity_reviews/<branch>
|
|
1722
|
+
# architecture_reviews/<branch>
|
|
1723
|
+
# ui_test_plan_reviews/<branch>
|
|
1330
1724
|
#
|
|
1331
1725
|
# The walker state loses its dot because `actions/upload-artifact` skips hidden
|
|
1332
|
-
# files by default.
|
|
1333
|
-
# gitignored machine-local state, and the remote-status and
|
|
1334
|
-
# under `autonomous_logs/`, whose ignore rule already
|
|
1726
|
+
# files by default. THE BUNDLE ITSELF IS NEVER COMMITTED. Every file outside
|
|
1727
|
+
# `planning/` is gitignored machine-local state, and the remote-status and
|
|
1728
|
+
# move-aside paths sit under `autonomous_logs/`, whose ignore rule already
|
|
1729
|
+
# covers them. The files under `planning/` are untracked drafts that the flow
|
|
1730
|
+
# commits itself at its P1/P3 convergence; the bundle only carries them.
|
|
1731
|
+
#
|
|
1732
|
+
# THE PLANNING PATHS ARE A MIRROR of the contracts that write and stage them:
|
|
1733
|
+
# `plugin/instructions/task_plan_writing_instructions_autonomous.md` →
|
|
1734
|
+
# `## Override 3` and `## Override 4` staging lists, and
|
|
1735
|
+
# `<scripts_dir>/flows/task_plan_writing.graph.json` → each node's `findingsFolder`. A path
|
|
1736
|
+
# added there is an edit to `hr_remote_planning_paths`. Only planning is
|
|
1737
|
+
# carried because the walker's one graph is `task_plan_writing.graph.json`;
|
|
1738
|
+
# implementation-phase per-unit review folders never span a pause, which waits
|
|
1739
|
+
# for a clean tracked tree. `HR_REMOTE_STATE_SCHEMA` stays `'1'`: `planning/`
|
|
1740
|
+
# is additive, an older reader ignores it, and a newer reader of an older
|
|
1741
|
+
# bundle finds none.
|
|
1335
1742
|
#
|
|
1336
1743
|
# WHO READS EACH FILE. `status.json`: `remote-run.sh sync` / `status` (into the
|
|
1337
1744
|
# local registry), `continue` / `poll` (the decision, `chain`, the reset time)
|
|
1338
1745
|
# and the next job's seed. The clarification directory and `PAUSE_PROGRESS.md`:
|
|
1339
1746
|
# the next job, and the user's local mirror. The walker state: the next job
|
|
1340
|
-
# only. `
|
|
1341
|
-
#
|
|
1747
|
+
# only. `planning/`: the next job only, never a mirror — an untracked draft left
|
|
1748
|
+
# in the mirror would make its later fast-forward to `origin/<branch>` refuse,
|
|
1749
|
+
# because the draft's own convergence commit adds the same path. `run.log`: the
|
|
1750
|
+
# user only — `sync` copies it to the main checkout's logs directory itself,
|
|
1751
|
+
# and no restore places it.
|
|
1342
1752
|
#
|
|
1343
1753
|
# `status.json` — schema `HR_REMOTE_STATE_SCHEMA`; every value a JSON string:
|
|
1344
1754
|
# schema a reader that does not recognise it treats the bundle as absent
|
|
@@ -1372,6 +1782,67 @@ hr_remote_names_var() {
|
|
|
1372
1782
|
HR_REMOTE_LOGS_DIR='autonomous_logs'
|
|
1373
1783
|
HR_REMOTE_STATUS_SOURCE="$HR_REMOTE_LOGS_DIR/remote_status.json"
|
|
1374
1784
|
HR_REMOTE_SUPERSEDED_DIR="$HR_REMOTE_LOGS_DIR/remote_superseded"
|
|
1785
|
+
HR_REMOTE_PLANNING_DIR='planning'
|
|
1786
|
+
HR_REMOTE_WORKFLOW_RUN_FILE='harness-run.yml'
|
|
1787
|
+
HR_REMOTE_STATE_ARTIFACT='harness-state'
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
# hr_github_answer_route <branch> <engine> [park_loop_clear]
|
|
1791
|
+
# hr_github_resume_route <branch> <engine>
|
|
1792
|
+
#
|
|
1793
|
+
# Print the GitHub route for a remote-only reader of a job-side notification,
|
|
1794
|
+
# one clause with no trailing period, for the caller to join after its local
|
|
1795
|
+
# command. The route names the engine because `harness-run.yml`'s `engine`
|
|
1796
|
+
# input defaults to `task`: an empty <engine> prints where to read the run's
|
|
1797
|
+
# own instead. It names the branch twice, as the form's *Use workflow from*
|
|
1798
|
+
# ref and as the `branch` input: a run dispatched from the default branch is
|
|
1799
|
+
# listed under that branch, where every `gh run list --branch <branch>`
|
|
1800
|
+
# lookup (`sync`, `status`, `restore`) misses it. A non-empty third argument to the answer route adds the
|
|
1801
|
+
# park-loop clear. The section cited is `## 1. The lifecycle of a remote run`;
|
|
1802
|
+
# renumbering or retitling it is an edit here.
|
|
1803
|
+
hr_github_answer_route() {
|
|
1804
|
+
local branch="${1-}" engine="${2-}" clear='' eng
|
|
1805
|
+
hr_remote_names_var
|
|
1806
|
+
if [ -n "$engine" ]; then
|
|
1807
|
+
eng="engine \`$engine\`"
|
|
1808
|
+
else
|
|
1809
|
+
eng="engine the run's own (the \`engine\` field of \`$HR_REMOTE_STATUS_FILE\` in its \`$HR_REMOTE_STATE_ARTIFACT\` artifact)"
|
|
1810
|
+
fi
|
|
1811
|
+
[ -n "${3-}" ] && clear=', park_loop_clear true'
|
|
1812
|
+
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)' \
|
|
1813
|
+
"$HR_REMOTE_STATE_ARTIFACT" "$HR_REMOTE_WORKFLOW_RUN_FILE" "$branch" "$branch" "$eng" "$clear"
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
hr_github_resume_route() {
|
|
1817
|
+
local branch="${1-}" engine="${2-}" eng
|
|
1818
|
+
hr_remote_names_var
|
|
1819
|
+
if [ -n "$engine" ]; then
|
|
1820
|
+
eng="engine \`$engine\`"
|
|
1821
|
+
else
|
|
1822
|
+
eng="engine the run's own (the \`engine\` field of \`$HR_REMOTE_STATUS_FILE\` in its \`$HR_REMOTE_STATE_ARTIFACT\` artifact)"
|
|
1823
|
+
fi
|
|
1824
|
+
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)' \
|
|
1825
|
+
"$HR_REMOTE_WORKFLOW_RUN_FILE" "$branch" "$branch" "$eng"
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
# hr_remote_planning_paths <branch>
|
|
1829
|
+
#
|
|
1830
|
+
# Assigns `HR_REMOTE_PLANNING_PATHS`: the eight planning paths of the format
|
|
1831
|
+
# above, relative to <state_dir>, newline-separated, no trailing slash. 1 with
|
|
1832
|
+
# an empty value for an empty <branch>.
|
|
1833
|
+
hr_remote_planning_paths() {
|
|
1834
|
+
local branch="${1-}"
|
|
1835
|
+
HR_REMOTE_PLANNING_PATHS=''
|
|
1836
|
+
[ -n "$branch" ] || return 1
|
|
1837
|
+
HR_REMOTE_PLANNING_PATHS="story_plans/${branch}_story_plan.md
|
|
1838
|
+
task_plans/$branch
|
|
1839
|
+
ui_test_plans/${branch}_ui_test_plan.md
|
|
1840
|
+
ui_test_plans/$branch
|
|
1841
|
+
task_plan_reviews/$branch
|
|
1842
|
+
business_parity_reviews/$branch
|
|
1843
|
+
architecture_reviews/$branch
|
|
1844
|
+
ui_test_plan_reviews/$branch"
|
|
1845
|
+
return 0
|
|
1375
1846
|
}
|
|
1376
1847
|
|
|
1377
1848
|
# hr_remote_status_write <registry_file> <branch> <out_json> <decision> <detail>
|
|
@@ -1475,10 +1946,12 @@ hr_remote_status_get() {
|
|
|
1475
1946
|
# <root>'s configured state directory. `status.json` is the job's own
|
|
1476
1947
|
# `autonomous_logs/remote_status.json` when present; otherwise it is written
|
|
1477
1948
|
# from <branch>'s registry record with decision `stop`, because a job that never
|
|
1478
|
-
# wrote its status never decided to continue.
|
|
1949
|
+
# wrote its status never decided to continue. Each planning path present is
|
|
1950
|
+
# copied under `planning/`, whether or not the branch tracks it: the restore
|
|
1951
|
+
# never overwrites, so a tracked copy is inert. 0 written; 1 a missing argument,
|
|
1479
1952
|
# a non-empty <out_dir> or a failed copy; 2 <root>'s configuration unresolvable.
|
|
1480
1953
|
hr_remote_bundle_write() {
|
|
1481
|
-
local root="${1-}" branch="${2-}" registry="${3-}" out="${4-}" state base clarify
|
|
1954
|
+
local root="${1-}" branch="${2-}" registry="${3-}" out="${4-}" state base clarify rel dst
|
|
1482
1955
|
[ -n "$root" ] && [ -n "$branch" ] && [ -n "$registry" ] && [ -n "$out" ] || return 1
|
|
1483
1956
|
state=$(hr_state_dir "$root") || return 2
|
|
1484
1957
|
hr_remote_names_var
|
|
@@ -1511,15 +1984,36 @@ hr_remote_bundle_write() {
|
|
|
1511
1984
|
if [ -f "$base/$HR_REMOTE_LOGS_DIR/$branch.log" ]; then
|
|
1512
1985
|
cp "$base/$HR_REMOTE_LOGS_DIR/$branch.log" "$out/$HR_REMOTE_LOG_FILE" 2>/dev/null || return 1
|
|
1513
1986
|
fi
|
|
1987
|
+
hr_remote_planning_paths "$branch" || return 1
|
|
1988
|
+
while IFS= read -r rel; do
|
|
1989
|
+
dst="$out/$HR_REMOTE_PLANNING_DIR/$rel"
|
|
1990
|
+
if [ -f "$base/$rel" ]; then
|
|
1991
|
+
mkdir -p "${dst%/*}" 2>/dev/null || return 1
|
|
1992
|
+
cp "$base/$rel" "$dst" 2>/dev/null || return 1
|
|
1993
|
+
elif [ -d "$base/$rel" ]; then
|
|
1994
|
+
mkdir -p "${dst%/*}" 2>/dev/null || return 1
|
|
1995
|
+
cp -R "$base/$rel" "$dst" 2>/dev/null || return 1
|
|
1996
|
+
fi
|
|
1997
|
+
done <<EOF
|
|
1998
|
+
$HR_REMOTE_PLANNING_PATHS
|
|
1999
|
+
EOF
|
|
1514
2000
|
return 0
|
|
1515
2001
|
}
|
|
1516
2002
|
|
|
1517
2003
|
# hr_remote_bundle_restore <bundle_dir> <root> <branch> <mode>
|
|
1518
2004
|
#
|
|
1519
2005
|
# <mode> `job` places the clarification directory, `PAUSE_PROGRESS.md`, the
|
|
1520
|
-
# walker state (back under its dotted name)
|
|
1521
|
-
# `autonomous_logs/remote_status.json`); `mirror` places the
|
|
1522
|
-
# run log is placed by neither.
|
|
2006
|
+
# walker state (back under its dotted name), the planning drafts and
|
|
2007
|
+
# `status.json` (as `autonomous_logs/remote_status.json`); `mirror` places the
|
|
2008
|
+
# first two only. The run log is placed by neither.
|
|
2009
|
+
#
|
|
2010
|
+
# A PLANNING DRAFT NEVER OVERWRITES. A regular file under `planning/` whose
|
|
2011
|
+
# path lies in `HR_REMOTE_PLANNING_PATHS` and has no `..` segment is placed only
|
|
2012
|
+
# where nothing exists, counted in `HR_REMOTE_PLANNING_PLACED`; one whose target
|
|
2013
|
+
# exists is left byte-identical — the checkout's copy is the branch's committed
|
|
2014
|
+
# record — and counted in `HR_REMOTE_PLANNING_KEPT`. A symlink, a non-regular
|
|
2015
|
+
# entry or a path outside the set counts in neither. Both are `0` at entry and
|
|
2016
|
+
# stay `0` in `mirror` mode.
|
|
1523
2017
|
#
|
|
1524
2018
|
# THE CLARIFICATION DIRECTORY IS REPLACED WHOLESALE, AND NOTHING IS DELETED. An
|
|
1525
2019
|
# existing target is moved aside with one `mv` into
|
|
@@ -1535,7 +2029,9 @@ hr_remote_bundle_write() {
|
|
|
1535
2029
|
# <branch>) or <root>'s configuration is unresolvable.
|
|
1536
2030
|
hr_remote_bundle_restore() {
|
|
1537
2031
|
local bundle="${1-}" root="${2-}" branch="${3-}" mode="${4-}"
|
|
1538
|
-
local state base named target epoch aside n tmp
|
|
2032
|
+
local state base named target epoch aside n tmp pdir file rel p inset
|
|
2033
|
+
HR_REMOTE_PLANNING_PLACED=0
|
|
2034
|
+
HR_REMOTE_PLANNING_KEPT=0
|
|
1539
2035
|
[ -n "$bundle" ] && [ -n "$root" ] && [ -n "$branch" ] || return 1
|
|
1540
2036
|
case "$mode" in
|
|
1541
2037
|
job|mirror) ;;
|
|
@@ -1575,6 +2071,37 @@ hr_remote_bundle_restore() {
|
|
|
1575
2071
|
mkdir -p "$base" 2>/dev/null || return 1
|
|
1576
2072
|
cp "$bundle/$HR_REMOTE_WALKER_FILE" "$base/$HR_REMOTE_WALKER_SOURCE" 2>/dev/null || return 1
|
|
1577
2073
|
fi
|
|
2074
|
+
pdir="$bundle/$HR_REMOTE_PLANNING_DIR"
|
|
2075
|
+
if [ -d "$pdir" ] && [ ! -L "$pdir" ]; then
|
|
2076
|
+
hr_remote_planning_paths "$branch" || return 1
|
|
2077
|
+
# Process substitution, not a pipe: the loop must run in this shell so the
|
|
2078
|
+
# counters and `return 1` reach the caller.
|
|
2079
|
+
while IFS= read -r file; do
|
|
2080
|
+
# Re-tested per line: a name holding a newline arrives split and fails here.
|
|
2081
|
+
[ -f "$file" ] && [ ! -L "$file" ] || continue
|
|
2082
|
+
rel=${file#"$pdir"/}
|
|
2083
|
+
case "/$rel/" in
|
|
2084
|
+
*/../*) continue ;;
|
|
2085
|
+
esac
|
|
2086
|
+
inset=0
|
|
2087
|
+
while IFS= read -r p; do
|
|
2088
|
+
case "$rel" in
|
|
2089
|
+
"$p"|"$p"/*) inset=1; break ;;
|
|
2090
|
+
esac
|
|
2091
|
+
done <<EOF
|
|
2092
|
+
$HR_REMOTE_PLANNING_PATHS
|
|
2093
|
+
EOF
|
|
2094
|
+
[ "$inset" = 1 ] || continue
|
|
2095
|
+
if [ -e "$base/$rel" ] || [ -L "$base/$rel" ]; then
|
|
2096
|
+
HR_REMOTE_PLANNING_KEPT=$((HR_REMOTE_PLANNING_KEPT + 1))
|
|
2097
|
+
continue
|
|
2098
|
+
fi
|
|
2099
|
+
target="$base/$rel"
|
|
2100
|
+
mkdir -p "${target%/*}" 2>/dev/null || return 1
|
|
2101
|
+
cp "$file" "$target" 2>/dev/null || return 1
|
|
2102
|
+
HR_REMOTE_PLANNING_PLACED=$((HR_REMOTE_PLANNING_PLACED + 1))
|
|
2103
|
+
done < <(find "$pdir" -type f 2>/dev/null)
|
|
2104
|
+
fi
|
|
1578
2105
|
mkdir -p "$base/$HR_REMOTE_LOGS_DIR" 2>/dev/null || return 1
|
|
1579
2106
|
tmp=$(mktemp "$base/$HR_REMOTE_STATUS_SOURCE.tmp.XXXXXX" 2>/dev/null) || return 1
|
|
1580
2107
|
if cp "$bundle/$HR_REMOTE_STATUS_FILE" "$tmp" 2>/dev/null \
|