autonomous-sdlc-harness 0.5.0 → 0.6.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/dist/cli.js +0 -0
- package/dist/commands/init.js +17 -8
- package/dist/commands/init.js.map +1 -1
- package/dist/config/check.js +27 -5
- package/dist/config/check.js.map +1 -1
- package/dist/config/model.js +54 -11
- 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 +1 -0
- package/dist/core/writer.js.map +1 -1
- package/dist/doctor/checks.js +317 -40
- package/dist/doctor/checks.js.map +1 -1
- package/dist/generators/githubWorkflows.js +21 -11
- package/dist/generators/githubWorkflows.js.map +1 -1
- package/dist/remote/githubActions.js +66 -4
- 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 +214 -0
- package/templates/github/workflows/harness-resume.yml +7 -0
- package/templates/github/workflows/harness-run.yml +127 -3
- package/templates/github/workflows/harness-trigger.yml +5 -4
- package/templates/scripts/README.md +1 -1
- package/templates/scripts/autonomous-watcher.sh +162 -30
- package/templates/scripts/docs-search-server.sh +88 -17
- package/templates/scripts/lib/harness-run-lib.sh +264 -53
- package/templates/scripts/remote-run.sh +3224 -162
- package/templates/scripts/scratch-run.sh +54 -73
- package/templates/state-dir/README-root.md +1 -1
- package/templates/state-dir/clarifications/README.md +1 -1
- package/templates/state-dir/scratch/README.md +4 -2
- package/templates/state-dir/user_reviews/README.md +2 -2
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# docs-search-server.sh — start the docs-retrieval MCP server for the checkout
|
|
3
|
-
# this script sits in,
|
|
4
|
-
# `docs serve`.
|
|
3
|
+
# this script sits in, on the backend `docs.retrievalBackend` selects.
|
|
5
4
|
#
|
|
6
5
|
# WHO RUNS IT. The agent runner, from the `harness-docs` entry `init` writes into
|
|
7
6
|
# `.mcp.json` when `docs.retrieval` is on — never a dispatched agent's Bash call,
|
|
@@ -9,29 +8,52 @@
|
|
|
9
8
|
# `agentInvocable: false` and the generated permission profile carries no entry
|
|
10
9
|
# for it.
|
|
11
10
|
#
|
|
12
|
-
# STDOUT BELONGS TO THE MCP TRANSPORT. Nothing here may print to stdout
|
|
13
|
-
#
|
|
14
|
-
#
|
|
11
|
+
# STDOUT BELONGS TO THE MCP TRANSPORT. Nothing here may print to stdout: a stray
|
|
12
|
+
# byte there is a malformed frame for the client. Every diagnostic goes to
|
|
13
|
+
# stderr.
|
|
15
14
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
15
|
+
# WHICH SERVER. Two backends, chosen by `docs.retrievalBackend` read at run
|
|
16
|
+
# time, with no fallback between them and no fallback to any other
|
|
17
|
+
# installation. The key is read ONLY WHEN `phases.docs` and `docs.retrieval` are
|
|
18
|
+
# both `true` (`hr_docs_retrieval_applies`, the shell mirror of
|
|
19
|
+
# `retrievalApplies`): `init` merges `.mcp.json` and removes nothing, so this
|
|
20
|
+
# entry outlives turning retrieval off, and with the gate closed the TypeScript
|
|
21
|
+
# runtime is `exec`ed whatever the key holds. `typescript`, or the key absent,
|
|
22
|
+
# `exec`s the machine-shared runtime's `docs serve`; `doctor`'s
|
|
23
|
+
# `retrieval-dependencies` check and `init`'s install both key on the same entry
|
|
24
|
+
# file this script tests. `python` starts the Python package's stdio server.
|
|
25
|
+
# This script checks no database and runs no `self-check`: grading the backend's
|
|
26
|
+
# prerequisites is `doctor`'s. `PATH` is settled through the library's fallback
|
|
27
|
+
# list first, because the starter is the agent runner rather than a login shell.
|
|
28
|
+
#
|
|
29
|
+
# WHY THE PYTHON BRANCH IS NOT `exec`ED. Its failure must surface as exit 3,
|
|
30
|
+
# which an `exec` could not report. It runs as a child with stdin passed
|
|
31
|
+
# explicitly, and a `TERM` or an `INT` the launcher receives is passed on to it
|
|
32
|
+
# as `TERM`, the signal the server stops on: bash starts a background child with
|
|
33
|
+
# `SIGINT` ignored when job control is off, and Python installs no handler over
|
|
34
|
+
# an ignored `SIGINT`, so an `INT` passed on as itself would never arrive.
|
|
22
35
|
#
|
|
23
36
|
# MIRRORS — a change to any owner below is an edit here too:
|
|
24
37
|
# `retrieval/runtime` and `node_modules/autonomous-sdlc-harness/dist/cli.js`
|
|
25
38
|
# mirror `RETRIEVAL_CACHE_DIRNAME`, `RETRIEVAL_RUNTIME_DIRNAME` and
|
|
26
39
|
# `RUNTIME_CLI_RELATIVE` in `cli/src/retrieval/runtime.ts`;
|
|
27
|
-
# `hr_cache_dir` mirrors `machineCacheDir()` in `cli/src/machine/paths.ts
|
|
40
|
+
# `hr_cache_dir` mirrors `machineCacheDir()` in `cli/src/machine/paths.ts`;
|
|
41
|
+
# `harness-docs-retrieval`, `serve-mcp`, `HARNESS_DOCS_RETRIEVAL_DATABASE_URL`,
|
|
42
|
+
# the default connection string and exit 3 mirror `PYTHON_RETRIEVAL_COMMAND`,
|
|
43
|
+
# `PYTHON_SERVE_SUB_COMMAND`, `PYTHON_DATABASE_URL_VARIABLE`,
|
|
44
|
+
# `PYTHON_DEFAULT_DATABASE_URL` and `PYTHON_BACKEND_UNAVAILABLE_EXIT` in
|
|
45
|
+
# `cli/src/retrieval/pythonBackend.ts`.
|
|
28
46
|
#
|
|
29
47
|
# Exit contract:
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
48
|
+
# 0 the Python backend exited 0
|
|
49
|
+
# 1 no repository at this script's location; `docs.retrievalBackend`
|
|
50
|
+
# outside the enum; or, on the TypeScript backend, no runtime entry at
|
|
51
|
+
# the resolved path or no cache directory to resolve it under; stderr
|
|
52
|
+
# names which
|
|
53
|
+
# 3 the Python backend is selected but cannot serve: its command does not
|
|
54
|
+
# resolve on `PATH`, or it exited non-zero; stderr names which
|
|
55
|
+
# N on the TypeScript backend, `docs serve`'s own status, through `exec`; a
|
|
56
|
+
# failure to source the library exits non-zero before that, on stderr only
|
|
35
57
|
|
|
36
58
|
set -euo pipefail
|
|
37
59
|
|
|
@@ -50,6 +72,55 @@ if ! root="$(hr_repo_root "$script_dir")"; then
|
|
|
50
72
|
exit 1
|
|
51
73
|
fi
|
|
52
74
|
|
|
75
|
+
backend=typescript
|
|
76
|
+
config_status=0
|
|
77
|
+
hr_config_load "$root" || config_status=$?
|
|
78
|
+
if [ "$config_status" -eq 2 ]; then
|
|
79
|
+
echo "docs-search-server: $root/harness.config.json could not be read, so the default backend (typescript) was taken" >&2
|
|
80
|
+
elif [ "$config_status" -eq 0 ] && hr_docs_retrieval_applies "$root"; then
|
|
81
|
+
backend_status=0
|
|
82
|
+
backend="$(hr_docs_retrieval_backend "$root")" || backend_status=$?
|
|
83
|
+
if [ "$backend_status" -ne 0 ]; then
|
|
84
|
+
echo "docs-search-server: docs.retrievalBackend in $root/harness.config.json is neither \`typescript\` nor \`python\`; set one of them, then run \`npx autonomous-sdlc-harness doctor\`" >&2
|
|
85
|
+
exit 1
|
|
86
|
+
fi
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
if [ "$backend" = python ]; then
|
|
90
|
+
if ! command -v harness-docs-retrieval >/dev/null 2>&1; then
|
|
91
|
+
echo "docs-search-server: the Python backend is selected but \`harness-docs-retrieval\` does not resolve on PATH (interpreter or package missing); see docs/retrieval.md and run \`npx autonomous-sdlc-harness doctor\`" >&2
|
|
92
|
+
exit 3
|
|
93
|
+
fi
|
|
94
|
+
if [ -z "${HARNESS_DOCS_RETRIEVAL_DATABASE_URL-}" ]; then
|
|
95
|
+
HARNESS_DOCS_RETRIEVAL_DATABASE_URL='postgresql://harness:harness@127.0.0.1:5432/docs_retrieval'
|
|
96
|
+
fi
|
|
97
|
+
export HARNESS_DOCS_RETRIEVAL_DATABASE_URL
|
|
98
|
+
|
|
99
|
+
# A background job's stdin is /dev/null unless it is passed explicitly.
|
|
100
|
+
harness-docs-retrieval serve-mcp --repo "$root" <&0 &
|
|
101
|
+
child=$!
|
|
102
|
+
trapped=0
|
|
103
|
+
trap 'trapped=1; kill -TERM "$child" 2>/dev/null || true' TERM
|
|
104
|
+
trap 'trapped=1; kill -TERM "$child" 2>/dev/null || true' INT
|
|
105
|
+
|
|
106
|
+
# A trapped signal interrupts `wait` before the child's status is collected, so wait again. 127
|
|
107
|
+
# means the first `wait` had already collected it.
|
|
108
|
+
child_status=0
|
|
109
|
+
wait "$child" || child_status=$?
|
|
110
|
+
while [ "$trapped" -eq 1 ]; do
|
|
111
|
+
trapped=0
|
|
112
|
+
again=0
|
|
113
|
+
wait "$child" 2>/dev/null || again=$?
|
|
114
|
+
[ "$again" -eq 127 ] || child_status=$again
|
|
115
|
+
done
|
|
116
|
+
|
|
117
|
+
if [ "$child_status" -eq 0 ]; then
|
|
118
|
+
exit 0
|
|
119
|
+
fi
|
|
120
|
+
echo "docs-search-server: the Python backend exited with status $child_status before or while serving; the line above (\`harness-docs-retrieval: …\`) names why; run \`npx autonomous-sdlc-harness doctor\`" >&2
|
|
121
|
+
exit 3
|
|
122
|
+
fi
|
|
123
|
+
|
|
53
124
|
if ! cache_dir="$(hr_cache_dir)"; then
|
|
54
125
|
echo "docs-search-server: neither XDG_CACHE_HOME nor HOME is set, so the retrieval runtime cannot be located" >&2
|
|
55
126
|
exit 1
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
# the repository it is operating on, reads that repository's
|
|
4
4
|
# `harness.config.json` at run time, answers "is this branch protected?",
|
|
5
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`),
|
|
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
|
|
7
9
|
# artifact in a working copy and commits and pushes it, and derives the
|
|
8
10
|
# anchors (main checkout, work root, worktree directory, repo slug,
|
|
9
11
|
# state-dir paths) the scripts would otherwise each re-derive slightly
|
|
@@ -77,9 +79,12 @@
|
|
|
77
79
|
# files under the eight planning paths `hr_remote_planning_paths` assigns;
|
|
78
80
|
# outside it, only the caller-named `<out_dir>` (its `planning/` included)
|
|
79
81
|
# of `hr_remote_bundle_write` and the caller-named `<out_json>`
|
|
80
|
-
# of `hr_remote_status_write`.
|
|
81
|
-
#
|
|
82
|
-
#
|
|
82
|
+
# of `hr_remote_status_write`. The move-aside directory receives the
|
|
83
|
+
# clarification directory and `PAUSE_PROGRESS.md`, both moved there by
|
|
84
|
+
# `hr_remote_move_aside` alone. Written only by `hr_remote_status_write`,
|
|
85
|
+
# `hr_remote_bundle_write`, `hr_remote_bundle_restore` and
|
|
86
|
+
# `hr_remote_move_aside`, and nothing there but a writer's own failed temp
|
|
87
|
+
# file is ever removed.
|
|
83
88
|
# 4. THE ARTIFACT PLACEMENT writes one artifact into a working copy. Fence:
|
|
84
89
|
# the caller-named `<worktree>/<rel>`, its parent directories and that
|
|
85
90
|
# path's index entry, plus whatever the two caller-named wrappers do.
|
|
@@ -93,7 +98,7 @@
|
|
|
93
98
|
#
|
|
94
99
|
# A caller that calls no `hr_lane_*`, `hr_registry_init`, `hr_registry_set`,
|
|
95
100
|
# `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`,
|
|
101
|
+
# `hr_remote_bundle_write`, `hr_remote_bundle_restore`, `hr_remote_move_aside`, `hr_place_artifact`,
|
|
97
102
|
# `hr_commit_placed` or `hr_push_landed` function still gets a library that only reads. The
|
|
98
103
|
# lane's ceilings are the only environment values here that carry policy, because
|
|
99
104
|
# the lane is machine-scoped and has no configuration key to carry them; each is
|
|
@@ -209,7 +214,8 @@
|
|
|
209
214
|
# `HR_LANE_OWNER_AT` and `HR_LANE_BROKEN_OWNER`, and the remote state bundle's
|
|
210
215
|
# names, which `hr_remote_names_var` assigns, with `HR_REMOTE_PLANNING_PATHS`
|
|
211
216
|
# (`hr_remote_planning_paths`) and `HR_REMOTE_PLANNING_PLACED` /
|
|
212
|
-
# `HR_REMOTE_PLANNING_KEPT` (`hr_remote_bundle_restore`)
|
|
217
|
+
# `HR_REMOTE_PLANNING_KEPT` (`hr_remote_bundle_restore`) and `HR_REMOTE_ASIDE`
|
|
218
|
+
# (`hr_remote_move_aside`). Every one of them is assigned
|
|
213
219
|
# before it is read by the function that owns it, so an inherited value from a
|
|
214
220
|
# parent process is overwritten rather than believed.
|
|
215
221
|
#
|
|
@@ -594,9 +600,10 @@ hr_config_load() {
|
|
|
594
600
|
# which is what lets an explicitly EMPTY list read as a configured set rather
|
|
595
601
|
# than as an absent one.
|
|
596
602
|
#
|
|
597
|
-
# A `phases.*` value that is not a boolean is emitted as
|
|
598
|
-
# string `"true"` — which `tostring` would otherwise make
|
|
599
|
-
# from `true` — reaches `hr_phase_enabled`
|
|
603
|
+
# A `phases.*` or `docs.retrieval` value that is not a boolean is emitted as
|
|
604
|
+
# `invalid`, so the string `"true"` — which `tostring` would otherwise make
|
|
605
|
+
# indistinguishable from `true` — reaches `hr_phase_enabled` or
|
|
606
|
+
# `hr_docs_retrieval_applies` as a value it refuses (2).
|
|
600
607
|
out=$(jq -n -r '
|
|
601
608
|
def s($k; $v):
|
|
602
609
|
if $v == null then empty
|
|
@@ -631,6 +638,8 @@ hr_config_load() {
|
|
|
631
638
|
s("phases.qa"; try (.phases.qa | if type == "boolean" or . == null then . else "invalid" end) catch null),
|
|
632
639
|
s("phases.docs"; try (.phases.docs | if type == "boolean" or . == null then . else "invalid" end) catch null),
|
|
633
640
|
s("execution.target"; try .execution.target catch null),
|
|
641
|
+
s("docs.retrievalBackend"; try .docs.retrievalBackend catch null),
|
|
642
|
+
s("docs.retrieval"; try (.docs.retrieval | if type == "boolean" or . == null then . else "invalid" end) catch null),
|
|
634
643
|
s("forge"; try .forge catch null),
|
|
635
644
|
s("protectedBranches.present";
|
|
636
645
|
try (if (.protectedBranches | type) == "array" then "1" else null end) catch null),
|
|
@@ -918,6 +927,53 @@ hr_forge() {
|
|
|
918
927
|
return 2
|
|
919
928
|
}
|
|
920
929
|
|
|
930
|
+
# Whether the docs phase's retrieval step runs: `phases.docs` and
|
|
931
|
+
# `docs.retrieval` both `true`. The shell mirror of `cli/src/config/model.ts` →
|
|
932
|
+
# `retrievalApplies`: change the predicate there and here together. PRINTS
|
|
933
|
+
# NOTHING: the answer is the status, as `hr_phase_enabled`'s is. 0 = both
|
|
934
|
+
# `true`; 1 = either `false` or unset; 2 = the configuration is unresolvable or
|
|
935
|
+
# either value is not a boolean — refused rather than guessed about.
|
|
936
|
+
hr_docs_retrieval_applies() {
|
|
937
|
+
local root="${1-}" status
|
|
938
|
+
hr_phase_enabled "$root" docs
|
|
939
|
+
status=$?
|
|
940
|
+
[ "$status" -eq 0 ] || return "$status"
|
|
941
|
+
hr_cfg_scalar_var "docs.retrieval" || return 1
|
|
942
|
+
case "$HR_CFG_VALUE" in
|
|
943
|
+
true) return 0 ;;
|
|
944
|
+
false) return 1 ;;
|
|
945
|
+
esac
|
|
946
|
+
return 2
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
# `docs.retrievalBackend` — which runtime the docs-retrieval index uses:
|
|
950
|
+
# `typescript` or `python`. THE ONE READER OF THE KEY IN THIS FAMILY, and the
|
|
951
|
+
# shell mirror of `cli/src/config/model.ts` → `RETRIEVAL_BACKENDS` /
|
|
952
|
+
# `DEFAULT_RETRIEVAL_BACKEND`: change the enum there and here together. Schema
|
|
953
|
+
# default `typescript`, so an absent key prints `typescript` and 1 is never
|
|
954
|
+
# returned; a `docs` parent of the wrong type (`"docs": "x"`) reads as absent
|
|
955
|
+
# too, because `hr_config_load`'s `try … catch` nulls that key alone — refusing
|
|
956
|
+
# that shape is the schema's job. 2 — printing nothing — when the configuration
|
|
957
|
+
# is unresolvable or the value is outside the enum, a refusal rather than a
|
|
958
|
+
# guess. It does NOT consult `phases.docs` / `docs.retrieval`: the key is read
|
|
959
|
+
# only inside the gate, so a caller asks `hr_docs_retrieval_applies` first and
|
|
960
|
+
# calls this only on its status 0.
|
|
961
|
+
hr_docs_retrieval_backend() {
|
|
962
|
+
local root="${1-}"
|
|
963
|
+
hr_config_load "$root" || return 2
|
|
964
|
+
if ! hr_cfg_scalar_var "docs.retrievalBackend"; then
|
|
965
|
+
printf 'typescript\n'
|
|
966
|
+
return 0
|
|
967
|
+
fi
|
|
968
|
+
case "$HR_CFG_VALUE" in
|
|
969
|
+
typescript|python)
|
|
970
|
+
printf '%s\n' "$HR_CFG_VALUE"
|
|
971
|
+
return 0
|
|
972
|
+
;;
|
|
973
|
+
esac
|
|
974
|
+
return 2
|
|
975
|
+
}
|
|
976
|
+
|
|
921
977
|
# ---------------------------------------------------------------------------
|
|
922
978
|
# Inbox routing — the one owner of the drop filename patterns.
|
|
923
979
|
# ---------------------------------------------------------------------------
|
|
@@ -1140,6 +1196,92 @@ hr_state_path() {
|
|
|
1140
1196
|
fi
|
|
1141
1197
|
}
|
|
1142
1198
|
|
|
1199
|
+
# Judge whether <path> resolves STRICTLY INSIDE `<root>/<state_dir>/scratch/`.
|
|
1200
|
+
# Usage: hr_scratch_path_var <root> <path> [<base>]
|
|
1201
|
+
#
|
|
1202
|
+
# Two callers, differing only in what they do with an accepted path:
|
|
1203
|
+
# `scratch-run.sh` executes a file there and `remote-run.sh discard` removes a
|
|
1204
|
+
# directory there. So this reports a keyword in `HR_SCRATCH_WHY` and leaves every
|
|
1205
|
+
# message and exit code to its caller.
|
|
1206
|
+
#
|
|
1207
|
+
# The rule, stated once:
|
|
1208
|
+
# - `..` is refused ANYWHERE in the argument, not only as a whole segment —
|
|
1209
|
+
# strictly stronger, and free, because nothing in that directory depends on
|
|
1210
|
+
# a file's name. Then any character outside `A-Za-z0-9._/-` is refused. Both
|
|
1211
|
+
# tests run on <path> as the caller received it, before anything resolves.
|
|
1212
|
+
# - A relative <path> resolves against <base> when given, else <root>; an
|
|
1213
|
+
# absolute one is taken as given.
|
|
1214
|
+
# - Both sides of the comparison are resolved PHYSICALLY (`cd … && pwd -P`), so
|
|
1215
|
+
# a symlinked directory planted inside the scratch tree cannot widen the
|
|
1216
|
+
# fence. The target's parent must be the scratch directory or beneath it, and
|
|
1217
|
+
# a basename of `.` (the scratch directory itself) is refused.
|
|
1218
|
+
# - A target that is itself a symlink is refused rather than followed: its
|
|
1219
|
+
# destination is outside this judgement. The target need not exist.
|
|
1220
|
+
#
|
|
1221
|
+
# Clears the other four, then sets: `HR_SCRATCH_SUBDIR` (`scratch`, mirroring the `scratch` row
|
|
1222
|
+
# of `STATE_DIR_ENTRIES` in `cli/src/generators/stateDir.ts`), `HR_SCRATCH_DIR`
|
|
1223
|
+
# (physical), `HR_SCRATCH_PARENT` (physical), `HR_SCRATCH_TARGET`
|
|
1224
|
+
# (`<parent>/<basename>`, the path a caller acts on) and `HR_SCRATCH_WHY`.
|
|
1225
|
+
# Status / `HR_SCRATCH_WHY`:
|
|
1226
|
+
# 0 accepted (WHY empty)
|
|
1227
|
+
# 1 refused — `dotdot`, `charset`, `outside`, `itself`, `symlink`
|
|
1228
|
+
# 2 the target's parent does not resolve — `no-parent`
|
|
1229
|
+
# 3 the scratch directory cannot be located — `no-config`, `no-scratch`
|
|
1230
|
+
# 4 <root> or <path> is empty — `usage`
|
|
1231
|
+
hr_scratch_path_var() {
|
|
1232
|
+
local root="${1-}" path="${2-}" base="${3-}" scratch candidate name
|
|
1233
|
+
HR_SCRATCH_DIR=""
|
|
1234
|
+
HR_SCRATCH_PARENT=""
|
|
1235
|
+
HR_SCRATCH_TARGET=""
|
|
1236
|
+
HR_SCRATCH_WHY=""
|
|
1237
|
+
HR_SCRATCH_SUBDIR='scratch'
|
|
1238
|
+
if [ -z "$root" ] || [ -z "$path" ]; then
|
|
1239
|
+
HR_SCRATCH_WHY="usage"
|
|
1240
|
+
return 4
|
|
1241
|
+
fi
|
|
1242
|
+
case "$path" in
|
|
1243
|
+
*..*) HR_SCRATCH_WHY="dotdot"; return 1 ;;
|
|
1244
|
+
esac
|
|
1245
|
+
case "$path" in
|
|
1246
|
+
*[!A-Za-z0-9._/-]*) HR_SCRATCH_WHY="charset"; return 1 ;;
|
|
1247
|
+
esac
|
|
1248
|
+
scratch=$(hr_state_path "$root" "$HR_SCRATCH_SUBDIR")
|
|
1249
|
+
if [ "$?" -ne 0 ] || [ -z "$scratch" ]; then
|
|
1250
|
+
HR_SCRATCH_WHY="no-config"
|
|
1251
|
+
return 3
|
|
1252
|
+
fi
|
|
1253
|
+
HR_SCRATCH_DIR=$(cd "$scratch" 2>/dev/null && pwd -P)
|
|
1254
|
+
if [ -z "$HR_SCRATCH_DIR" ]; then
|
|
1255
|
+
HR_SCRATCH_WHY="no-scratch"
|
|
1256
|
+
return 3
|
|
1257
|
+
fi
|
|
1258
|
+
[ -n "$base" ] || base="$root"
|
|
1259
|
+
case "$path" in
|
|
1260
|
+
/*) candidate="$path" ;;
|
|
1261
|
+
*) candidate="${base%/}/$path" ;;
|
|
1262
|
+
esac
|
|
1263
|
+
HR_SCRATCH_PARENT=$(cd "$(dirname "$candidate")" 2>/dev/null && pwd -P)
|
|
1264
|
+
if [ -z "$HR_SCRATCH_PARENT" ]; then
|
|
1265
|
+
HR_SCRATCH_WHY="no-parent"
|
|
1266
|
+
return 2
|
|
1267
|
+
fi
|
|
1268
|
+
case "$HR_SCRATCH_PARENT" in
|
|
1269
|
+
"$HR_SCRATCH_DIR" | "$HR_SCRATCH_DIR"/*) ;;
|
|
1270
|
+
*) HR_SCRATCH_WHY="outside"; return 1 ;;
|
|
1271
|
+
esac
|
|
1272
|
+
name=$(basename "$candidate")
|
|
1273
|
+
if [ "$name" = "." ]; then
|
|
1274
|
+
HR_SCRATCH_WHY="itself"
|
|
1275
|
+
return 1
|
|
1276
|
+
fi
|
|
1277
|
+
HR_SCRATCH_TARGET="$HR_SCRATCH_PARENT/$name"
|
|
1278
|
+
if [ -L "$HR_SCRATCH_TARGET" ]; then
|
|
1279
|
+
HR_SCRATCH_WHY="symlink"
|
|
1280
|
+
return 1
|
|
1281
|
+
fi
|
|
1282
|
+
return 0
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1143
1285
|
# The machine-local settings directory — one place an operator keeps values that
|
|
1144
1286
|
# vary per machine rather than per repository, which is why it is not a config
|
|
1145
1287
|
# key and not a repository file. Return 1 when there is no home to anchor it to.
|
|
@@ -1442,8 +1584,10 @@ hr_remote_record_init() {
|
|
|
1442
1584
|
# limit and readable in the Actions run list. GitHub documents no ref limit.
|
|
1443
1585
|
#
|
|
1444
1586
|
# THIS SECTION ONLY READS. It fetches nothing and creates no branch or file; a
|
|
1445
|
-
# caller that wants `origin` fresh fetches first.
|
|
1446
|
-
#
|
|
1587
|
+
# caller that wants `origin` fresh fetches first. Given a <gh_cli>, it also asks
|
|
1588
|
+
# GitHub one read per candidate — the run workflow's runs under that name — and
|
|
1589
|
+
# still writes nothing. A registry is read only when it already exists, because
|
|
1590
|
+
# `hr_registry_get` creates an absent one.
|
|
1447
1591
|
# ---------------------------------------------------------------------------
|
|
1448
1592
|
|
|
1449
1593
|
# The two limits the rule above names.
|
|
@@ -1525,11 +1669,37 @@ EOF
|
|
|
1525
1669
|
return 0
|
|
1526
1670
|
}
|
|
1527
1671
|
|
|
1528
|
-
#
|
|
1529
|
-
#
|
|
1672
|
+
# hr_branch_run_history <root> <gh_cli> <name> — 0 when the run workflow
|
|
1673
|
+
# (`HR_REMOTE_WORKFLOW_RUN_FILE`) lists a run under <name>, 1 when it lists
|
|
1674
|
+
# none, 2 when <gh_cli> cannot run, exits non-zero, or answers anything but a
|
|
1675
|
+
# JSON array. Sets no `HR_` variable: the names are assigned inside the
|
|
1676
|
+
# subshell that `cd`s to <root>.
|
|
1677
|
+
#
|
|
1678
|
+
# GitHub keeps a deleted branch's runs listed under its name, and a later run
|
|
1679
|
+
# of that name inherits them: Gate 12 round 5 found run `36569531374` of the
|
|
1680
|
+
# deleted `feat_invoices` (`docs/development.md` → Gate 12 → Round 5, finding
|
|
1681
|
+
# 1). One bounded read per candidate, not one listing of every branch, because
|
|
1682
|
+
# a bounded all-branch listing could miss an old name. Bash 3.2 and jq 1.5.
|
|
1683
|
+
hr_branch_run_history() {
|
|
1684
|
+
local root="${1-}" gh_cli="${2-}" name="${3-}" answer verdict
|
|
1685
|
+
[ -n "$root" ] && [ -n "$gh_cli" ] && [ -n "$name" ] || return 2
|
|
1686
|
+
answer=$(cd "$root" 2>/dev/null && hr_remote_names_var &&
|
|
1687
|
+
"$gh_cli" run list --workflow "$HR_REMOTE_WORKFLOW_RUN_FILE" --branch "$name" --limit 1 --json databaseId 2>/dev/null) || return 2
|
|
1688
|
+
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
|
|
1689
|
+
case "$verdict" in
|
|
1690
|
+
history) return 0 ;;
|
|
1691
|
+
none) return 1 ;;
|
|
1692
|
+
esac
|
|
1693
|
+
return 2
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
# Judge <name> against the listings `hr_branch_taken_lists_var` last read, then,
|
|
1697
|
+
# given a <gh_cli>, against the run workflow's history — last, so the free local
|
|
1698
|
+
# checks answer first. The answers and `HR_TAKEN_WHY` are
|
|
1699
|
+
# `hr_branch_name_taken`'s.
|
|
1530
1700
|
hr_branch_taken_judge() {
|
|
1531
1701
|
local LC_ALL=C
|
|
1532
|
-
local root="${1-}" name="${2-}" registry="${3-}" lower status nl
|
|
1702
|
+
local root="${1-}" name="${2-}" registry="${3-}" gh_cli="${4-}" lower status nl
|
|
1533
1703
|
nl='
|
|
1534
1704
|
'
|
|
1535
1705
|
HR_TAKEN_WHY=""
|
|
@@ -1557,18 +1727,29 @@ hr_branch_taken_judge() {
|
|
|
1557
1727
|
HR_TAKEN_WHY="a run registry record"
|
|
1558
1728
|
return 0
|
|
1559
1729
|
fi
|
|
1730
|
+
if [ -n "$gh_cli" ]; then
|
|
1731
|
+
status=0
|
|
1732
|
+
hr_branch_run_history "$root" "$gh_cli" "$name" || status=$?
|
|
1733
|
+
case "$status" in
|
|
1734
|
+
0) HR_TAKEN_WHY="a run of the run workflow listed under that name"; return 0 ;;
|
|
1735
|
+
1) ;;
|
|
1736
|
+
*) HR_TAKEN_WHY="the run history of $name could not be listed"; return 2 ;;
|
|
1737
|
+
esac
|
|
1738
|
+
fi
|
|
1560
1739
|
return 1
|
|
1561
1740
|
}
|
|
1562
1741
|
|
|
1563
|
-
# hr_branch_name_taken <root> <name> [<registry>] — 0 taken, 1 free,
|
|
1564
|
-
# tell. Sets `HR_TAKEN_WHY` to a short phrase naming the collision or
|
|
1565
|
-
# failure.
|
|
1566
|
-
# case-insensitively; a local
|
|
1567
|
-
# `origin/<defaultBranch>`, a directory named
|
|
1568
|
-
# `<name>_task_prompt.md`, `<name>_story_plan.md` or
|
|
1569
|
-
# merged and deleted branch still leaves; a record in
|
|
1742
|
+
# hr_branch_name_taken <root> <name> [<registry>] [<gh_cli>] — 0 taken, 1 free,
|
|
1743
|
+
# 2 cannot tell. Sets `HR_TAKEN_WHY` to a short phrase naming the collision or
|
|
1744
|
+
# the failure. A caller with no registry passes `""` before <gh_cli>. Taken: a
|
|
1745
|
+
# protected name; a branch on `origin`, compared case-insensitively; a local
|
|
1746
|
+
# branch; under `<state_dir>` on `origin/<defaultBranch>`, a directory named
|
|
1747
|
+
# <name> or a file `<name>_task_prompt.md`, `<name>_story_plan.md` or
|
|
1748
|
+
# `<name>_docs.md` — which a merged and deleted branch still leaves; a record in
|
|
1749
|
+
# an existing <registry>; given a <gh_cli>, any run of the run workflow listed
|
|
1750
|
+
# under <name> (`hr_branch_run_history`), whose listing failing is a 2.
|
|
1570
1751
|
hr_branch_name_taken() {
|
|
1571
|
-
local root="${1-}" name="${2-}" registry="${3-}"
|
|
1752
|
+
local root="${1-}" name="${2-}" registry="${3-}" gh_cli="${4-}"
|
|
1572
1753
|
HR_TAKEN_WHY=""
|
|
1573
1754
|
if [ -z "$root" ] || [ -z "$name" ]; then
|
|
1574
1755
|
HR_TAKEN_WHY="no branch name to judge"
|
|
@@ -1576,20 +1757,21 @@ hr_branch_name_taken() {
|
|
|
1576
1757
|
fi
|
|
1577
1758
|
hr_config_load "$root" || :
|
|
1578
1759
|
hr_branch_taken_lists_var "$root" || return 2
|
|
1579
|
-
hr_branch_taken_judge "$root" "$name" "$registry"
|
|
1760
|
+
hr_branch_taken_judge "$root" "$name" "$registry" "$gh_cli"
|
|
1580
1761
|
}
|
|
1581
1762
|
|
|
1582
|
-
# hr_derive_branch <root> <text> <fallback> [<registry>] — print the
|
|
1583
|
-
# and return 0; return 2, printing nothing, when a `taken`
|
|
1584
|
-
# tell or no base routes back to itself; return 3, printing
|
|
1585
|
-
# suffix through `HR_BRANCH_SUFFIX_MAX` is taken. Never a
|
|
1763
|
+
# hr_derive_branch <root> <text> <fallback> [<registry>] [<gh_cli>] — print the
|
|
1764
|
+
# derived name and return 0; return 2, printing nothing, when a `taken`
|
|
1765
|
+
# judgement could not tell or no base routes back to itself; return 3, printing
|
|
1766
|
+
# nothing, when every suffix through `HR_BRANCH_SUFFIX_MAX` is taken. Never a
|
|
1767
|
+
# guessed name. <gh_cli> reaches every judgement as `hr_branch_name_taken`'s.
|
|
1586
1768
|
#
|
|
1587
1769
|
# THE BASE MUST ROUTE BACK TO ITSELF: `hr_inbox_route_var` on each drop filename
|
|
1588
1770
|
# a run of that name produces has to give the base as its branch, so no derived
|
|
1589
1771
|
# name makes the inbox patterns ambiguous. A base that does not is replaced by
|
|
1590
1772
|
# <fallback> once.
|
|
1591
1773
|
hr_derive_branch() {
|
|
1592
|
-
local root="${1-}" text="${2-}" fallback="${3-}" registry="${4-}"
|
|
1774
|
+
local root="${1-}" text="${2-}" fallback="${3-}" registry="${4-}" gh_cli="${5-}"
|
|
1593
1775
|
local base="" candidate suffix routes n status
|
|
1594
1776
|
hr_branch_limits_var
|
|
1595
1777
|
candidate=$(hr_branch_slug "$text") || candidate="$fallback"
|
|
@@ -1615,7 +1797,7 @@ hr_derive_branch() {
|
|
|
1615
1797
|
n=1
|
|
1616
1798
|
while :; do
|
|
1617
1799
|
status=0
|
|
1618
|
-
hr_branch_taken_judge "$root" "$candidate" "$registry" || status=$?
|
|
1800
|
+
hr_branch_taken_judge "$root" "$candidate" "$registry" "$gh_cli" || status=$?
|
|
1619
1801
|
case "$status" in
|
|
1620
1802
|
1) printf '%s\n' "$candidate"; return 0 ;;
|
|
1621
1803
|
2) return 2 ;;
|
|
@@ -1704,8 +1886,9 @@ hr_push_landed() {
|
|
|
1704
1886
|
# names. Every name below is a variable `hr_remote_names_var` assigns, and it
|
|
1705
1887
|
# also assigns `HR_REMOTE_WORKFLOW_RUN_FILE`, the run workflow's file name, for
|
|
1706
1888
|
# the GitHub-route producers `hr_github_answer_route` and
|
|
1707
|
-
# `hr_github_resume_route
|
|
1708
|
-
# function spells either one,
|
|
1889
|
+
# `hr_github_resume_route` and for the run-history probe `hr_branch_run_history`.
|
|
1890
|
+
# Those two are mirrors of the header's table; no function spells either one,
|
|
1891
|
+
# or any other name here.
|
|
1709
1892
|
#
|
|
1710
1893
|
# <bundle>/status.json the job's record, fixed schema below
|
|
1711
1894
|
# <bundle>/clarifications/<branch>/ the whole branch directory, answered/ included
|
|
@@ -2000,6 +2183,45 @@ EOF
|
|
|
2000
2183
|
return 0
|
|
2001
2184
|
}
|
|
2002
2185
|
|
|
2186
|
+
# hr_remote_move_aside <root> <rel_path>
|
|
2187
|
+
#
|
|
2188
|
+
# Moves `<root>/<state_dir>/<rel_path>`, a file or a directory, with one `mv`
|
|
2189
|
+
# into `autonomous_logs/remote_superseded/<epoch>/<rel_path>` (`<epoch>-<n>`,
|
|
2190
|
+
# the first `n` = 1, 2, … where that path is free, when an earlier move took
|
|
2191
|
+
# that second), creating the parents and removing nothing. On success
|
|
2192
|
+
# `HR_REMOTE_ASIDE` holds the absolute destination; it is empty at entry.
|
|
2193
|
+
#
|
|
2194
|
+
# 0 moved; 1 a missing argument, a <rel_path> that is absolute or has a `..`
|
|
2195
|
+
# segment, or a failed `mkdir` / `mv`; 2 — touching nothing — <root>'s
|
|
2196
|
+
# configuration is unresolvable; 3 — touching nothing — no source exists.
|
|
2197
|
+
hr_remote_move_aside() {
|
|
2198
|
+
local root="${1-}" rel="${2-}" state base epoch aside n
|
|
2199
|
+
HR_REMOTE_ASIDE=''
|
|
2200
|
+
[ -n "$root" ] && [ -n "$rel" ] || return 1
|
|
2201
|
+
case "$rel" in
|
|
2202
|
+
/*) return 1 ;;
|
|
2203
|
+
esac
|
|
2204
|
+
case "/$rel/" in
|
|
2205
|
+
*/../*) return 1 ;;
|
|
2206
|
+
esac
|
|
2207
|
+
state=$(hr_state_dir "$root") || return 2
|
|
2208
|
+
hr_remote_names_var
|
|
2209
|
+
base="${root%/}/$state"
|
|
2210
|
+
[ -e "$base/$rel" ] || [ -L "$base/$rel" ] || return 3
|
|
2211
|
+
epoch=$(date +%s)
|
|
2212
|
+
aside="$base/$HR_REMOTE_SUPERSEDED_DIR/$epoch"
|
|
2213
|
+
n=0
|
|
2214
|
+
while [ -e "$aside/$rel" ] || [ -L "$aside/$rel" ]; do
|
|
2215
|
+
n=$((n + 1))
|
|
2216
|
+
aside="$base/$HR_REMOTE_SUPERSEDED_DIR/$epoch-$n"
|
|
2217
|
+
done
|
|
2218
|
+
aside="$aside/$rel"
|
|
2219
|
+
mkdir -p "${aside%/*}" 2>/dev/null || return 1
|
|
2220
|
+
mv "$base/$rel" "$aside" 2>/dev/null || return 1
|
|
2221
|
+
HR_REMOTE_ASIDE=$aside
|
|
2222
|
+
return 0
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2003
2225
|
# hr_remote_bundle_restore <bundle_dir> <root> <branch> <mode>
|
|
2004
2226
|
#
|
|
2005
2227
|
# <mode> `job` places the clarification directory, `PAUSE_PROGRESS.md`, the
|
|
@@ -2016,20 +2238,18 @@ EOF
|
|
|
2016
2238
|
# stay `0` in `mirror` mode.
|
|
2017
2239
|
#
|
|
2018
2240
|
# THE CLARIFICATION DIRECTORY IS REPLACED WHOLESALE, AND NOTHING IS DELETED. An
|
|
2019
|
-
# existing target is moved aside
|
|
2020
|
-
#
|
|
2021
|
-
#
|
|
2022
|
-
#
|
|
2023
|
-
#
|
|
2024
|
-
#
|
|
2025
|
-
#
|
|
2026
|
-
#
|
|
2027
|
-
#
|
|
2028
|
-
# a schema other than `HR_REMOTE_STATE_SCHEMA`, or a `branch` other than
|
|
2029
|
-
# <branch>) or <root>'s configuration is unresolvable.
|
|
2241
|
+
# existing target is moved aside by `hr_remote_move_aside` before the bundle's
|
|
2242
|
+
# copy goes in, so a stale local pair cannot survive and no recursive removal is
|
|
2243
|
+
# ever shelled out. A bundle carrying no clarification directory leaves the
|
|
2244
|
+
# target alone: in a mirror it may hold an answer not yet relayed.
|
|
2245
|
+
#
|
|
2246
|
+
# 0 restored; 1 a missing argument, an unknown <mode>, a failed move aside or a
|
|
2247
|
+
# failed copy; 2 — touching nothing — when the bundle is unrecognised (no
|
|
2248
|
+
# readable `status.json`, a schema other than `HR_REMOTE_STATE_SCHEMA`, or a
|
|
2249
|
+
# `branch` other than <branch>) or <root>'s configuration is unresolvable.
|
|
2030
2250
|
hr_remote_bundle_restore() {
|
|
2031
2251
|
local bundle="${1-}" root="${2-}" branch="${3-}" mode="${4-}"
|
|
2032
|
-
local state base named target
|
|
2252
|
+
local state base named target tmp pdir file rel p inset
|
|
2033
2253
|
HR_REMOTE_PLANNING_PLACED=0
|
|
2034
2254
|
HR_REMOTE_PLANNING_KEPT=0
|
|
2035
2255
|
[ -n "$bundle" ] && [ -n "$root" ] && [ -n "$branch" ] || return 1
|
|
@@ -2047,16 +2267,7 @@ hr_remote_bundle_restore() {
|
|
|
2047
2267
|
if [ -d "$bundle/$HR_REMOTE_CLARIFY_DIR/$branch" ]; then
|
|
2048
2268
|
target="$base/$HR_REMOTE_CLARIFY_DIR/$branch"
|
|
2049
2269
|
if [ -e "$target" ]; then
|
|
2050
|
-
|
|
2051
|
-
aside="$base/$HR_REMOTE_SUPERSEDED_DIR/$epoch"
|
|
2052
|
-
n=0
|
|
2053
|
-
while [ -e "$aside/$HR_REMOTE_CLARIFY_DIR/$branch" ]; do
|
|
2054
|
-
n=$((n + 1))
|
|
2055
|
-
aside="$base/$HR_REMOTE_SUPERSEDED_DIR/$epoch-$n"
|
|
2056
|
-
done
|
|
2057
|
-
aside="$aside/$HR_REMOTE_CLARIFY_DIR/$branch"
|
|
2058
|
-
mkdir -p "${aside%/*}" 2>/dev/null || return 1
|
|
2059
|
-
mv "$target" "$aside" 2>/dev/null || return 1
|
|
2270
|
+
hr_remote_move_aside "$root" "$HR_REMOTE_CLARIFY_DIR/$branch" || return 1
|
|
2060
2271
|
fi
|
|
2061
2272
|
mkdir -p "${target%/*}" 2>/dev/null || return 1
|
|
2062
2273
|
cp -R "$bundle/$HR_REMOTE_CLARIFY_DIR/$branch" "$target" 2>/dev/null || return 1
|