autonomous-sdlc-harness 0.6.0 → 0.6.2
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 +72 -9
- package/dist/commands/init.js.map +1 -1
- package/dist/core/writer.js +3 -2
- package/dist/core/writer.js.map +1 -1
- package/dist/doctor/checks.js +255 -12
- package/dist/doctor/checks.js.map +1 -1
- package/dist/generators/githubWorkflows.js +60 -2
- package/dist/generators/githubWorkflows.js.map +1 -1
- package/dist/remote/githubActions.js +61 -3
- package/dist/remote/githubActions.js.map +1 -1
- package/package.json +1 -1
- package/templates/claude/context/conventions.md +1 -1
- package/templates/claude/context/layer.md +1 -1
- package/templates/github/workflows/harness-control.yml +63 -17
- package/templates/github/workflows/harness-run.yml +144 -5
- package/templates/github/workflows/harness-trigger.yml +4 -1
- package/templates/scripts/autonomous-watcher.sh +133 -28
- package/templates/scripts/lib/harness-run-lib.sh +59 -27
- package/templates/scripts/remote-run.sh +872 -122
- package/templates/state-dir/clarifications/README.md +1 -1
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
#
|
|
10
10
|
# THE INPUT CONTRACT. Composed on the shell side by `remote-run.sh` alone:
|
|
11
11
|
# action run | pause | warm | stop. Only `run` and `warm` start a
|
|
12
|
-
# job; `pause` and `stop` are jobless, titled marker runs
|
|
12
|
+
# job; `pause` and `stop` are jobless, titled marker runs,
|
|
13
|
+
# except a `pause` from the wrong ref (THE REF CHECK)
|
|
13
14
|
# branch the run's branch (for warm: GitHub's default branch)
|
|
14
15
|
# engine task | user_review | docs (action=run)
|
|
15
16
|
# resume none | answer | pause (action=run)
|
|
@@ -33,7 +34,8 @@
|
|
|
33
34
|
# HARNESS_REMOTE_STOP, CLAUDE_CODE_OAUTH_TOKEN,
|
|
34
35
|
# ANTHROPIC_API_KEY, HARNESS_PUSH_URL,
|
|
35
36
|
# HARNESS_GIT_TOKEN, the artifact harness-state,
|
|
36
|
-
# HARNESS_CLI_VERSION
|
|
37
|
+
# HARNESS_CLI_VERSION, HARNESS_RUN_ACTORS,
|
|
38
|
+
# HARNESS_TRIGGER_ALLOWED_BOTS
|
|
37
39
|
# cli/src/generators/githubWorkflows.ts --upgrade-workflows
|
|
38
40
|
# cli/src/generators/projectSettings.ts the marketplace and plugin key
|
|
39
41
|
# autonomous-sdlc-harness@autonomous-sdlc-harness
|
|
@@ -69,6 +71,38 @@
|
|
|
69
71
|
# report step's reason below: scripts older than this file carry no `collect`
|
|
70
72
|
# verb.
|
|
71
73
|
#
|
|
74
|
+
# THE REF CHECK. Finding 6 of Gate 12 round 6: GitHub lists a run under the ref
|
|
75
|
+
# it was dispatched from (Use workflow from), so a run or pause dispatched from
|
|
76
|
+
# a ref other than its `branch` input is invisible to every lookup by that
|
|
77
|
+
# branch. When `action` is `run` or `pause` and `github.ref_name` is not
|
|
78
|
+
# `inputs.branch`, the `wrong-ref` job fails, naming the ref to use, and `run`
|
|
79
|
+
# and `collect` are skipped; the jobless `pause` marker gains that failing job
|
|
80
|
+
# only when its ref is wrong. `warm` is exempt because it dispatches on the
|
|
81
|
+
# default branch with that branch as its input, so the two already agree.
|
|
82
|
+
# `stop` is exempt because a deleted branch's stop marker is sent from the
|
|
83
|
+
# default branch, its own ref being gone, and the stop check matches the
|
|
84
|
+
# `harness stop <branch>` title across every branch's runs, under any ref.
|
|
85
|
+
#
|
|
86
|
+
# THE RUN-ACTOR GATE. The first step of `run` and of `collect`, byte-identical
|
|
87
|
+
# in both, refuses a `github.triggering_actor` the repository variable
|
|
88
|
+
# HARNESS_RUN_ACTORS does not admit, so the Run workflow form, `gh workflow run`
|
|
89
|
+
# and a re-run by someone not on the list launch nothing; a refused `run` sets
|
|
90
|
+
# no SCRIPTS_DIR, so its `always()` and `!cancelled()` steps skip too. The list:
|
|
91
|
+
# split on `,`, each entry trimmed, empty entries dropped, matched
|
|
92
|
+
# case-insensitively, `*` admitting every writer; an empty list admits the owner
|
|
93
|
+
# of a user-owned repository alone, and nobody otherwise.
|
|
94
|
+
# * A step, not a job: Re-run failed jobs and Re-run job do not re-run an
|
|
95
|
+
# upstream job that succeeded, so a gate job would never see the re-runner's
|
|
96
|
+
# triggering_actor (docs/team-accounts-research.md, G2).
|
|
97
|
+
# * `github-actions[bot]` passes: every harness dispatch is made with
|
|
98
|
+
# GITHUB_TOKEN and names it. Measured 2026-10-05 for the trigger, the comment
|
|
99
|
+
# commands and `collect`; inferred for the `remote-run.sh continue` chain and
|
|
100
|
+
# the harness-resume.yml poller, which Gate 12 observation (xv) confirms.
|
|
101
|
+
# * Not closed: a writer who edits this file on a branch can remove the gate
|
|
102
|
+
# or dispatch as the bot. That writer can already read the secret (G7).
|
|
103
|
+
# * Resolved before it: the job-level HARNESS_PUSH_URL secret, at job start.
|
|
104
|
+
# The credential secrets and HARNESS_GIT_TOKEN are read only by later steps.
|
|
105
|
+
#
|
|
72
106
|
# ACTION PINS.
|
|
73
107
|
# actions/checkout@v5
|
|
74
108
|
# actions/setup-node@v5
|
|
@@ -93,7 +127,8 @@
|
|
|
93
127
|
# so your CI runs on it without an approval click), HARNESS_PUSH_URL
|
|
94
128
|
# (optional notifications).
|
|
95
129
|
# Variables: HARNESS_RUNNER, HARNESS_REMOTE_STOP (any value stops every job
|
|
96
|
-
# before it launches anything),
|
|
130
|
+
# before it launches anything), HARNESS_RUN_ACTORS (THE RUN-ACTOR GATE),
|
|
131
|
+
# HARNESS_STEP_TIMEOUT_MINUTES,
|
|
97
132
|
# HARNESS_SELF_PAUSE_AFTER_MINUTES, HARNESS_MAX_CHAIN, and the watcher
|
|
98
133
|
# tunables listed in the `run` job's `env:` under their own names. An unset
|
|
99
134
|
# variable arrives empty and the script reading it applies its own default.
|
|
@@ -234,8 +269,20 @@ defaults:
|
|
|
234
269
|
shell: bash
|
|
235
270
|
|
|
236
271
|
jobs:
|
|
272
|
+
wrong-ref:
|
|
273
|
+
if: (inputs.action == 'run' || inputs.action == 'pause') && github.ref_name != inputs.branch
|
|
274
|
+
runs-on: ${{ vars.HARNESS_RUNNER || 'ubuntu-latest' }}
|
|
275
|
+
steps:
|
|
276
|
+
- name: Refuse a dispatch from another ref
|
|
277
|
+
env:
|
|
278
|
+
IN_REF: ${{ github.ref_name }}
|
|
279
|
+
IN_BRANCH: ${{ inputs.branch }}
|
|
280
|
+
run: |
|
|
281
|
+
echo "::error::this run was dispatched from '$IN_REF', but its branch input is '$IN_BRANCH': GitHub lists a run under the ref it was dispatched from, so no lookup of '$IN_BRANCH' would find it. Run the workflow again with Use workflow from set to '$IN_BRANCH'."
|
|
282
|
+
exit 1
|
|
283
|
+
|
|
237
284
|
run:
|
|
238
|
-
if: inputs.action == 'run'
|
|
285
|
+
if: inputs.action == 'run' && github.ref_name == inputs.branch
|
|
239
286
|
runs-on: ${{ vars.HARNESS_RUNNER || 'ubuntu-latest' }}
|
|
240
287
|
# The self-hosted job limit, 5 days; a hosted job stops at its own limit.
|
|
241
288
|
timeout-minutes: 7200
|
|
@@ -253,6 +300,7 @@ jobs:
|
|
|
253
300
|
HARNESS_INPUT_PARK_LOOP_CLEAR: ${{ inputs.park_loop_clear }}
|
|
254
301
|
HARNESS_INPUT_CHAIN: ${{ inputs.chain }}
|
|
255
302
|
HARNESS_REMOTE_STOP: ${{ vars.HARNESS_REMOTE_STOP }}
|
|
303
|
+
HARNESS_RUN_ACTORS: ${{ vars.HARNESS_RUN_ACTORS }}
|
|
256
304
|
HARNESS_MAX_CHAIN: ${{ vars.HARNESS_MAX_CHAIN }}
|
|
257
305
|
STALL_WARN_SECS: ${{ vars.STALL_WARN_SECS }}
|
|
258
306
|
STALL_KILL_SECS: ${{ vars.STALL_KILL_SECS }}
|
|
@@ -274,6 +322,51 @@ jobs:
|
|
|
274
322
|
HARNESS_STEP_ALLOWANCE_MINUTES: '30'
|
|
275
323
|
HARNESS_SELF_PAUSE_MARGIN_MINUTES: '90'
|
|
276
324
|
steps:
|
|
325
|
+
- name: Refuse an actor not on HARNESS_RUN_ACTORS
|
|
326
|
+
env:
|
|
327
|
+
IN_TRIGGERING_ACTOR: ${{ github.triggering_actor }}
|
|
328
|
+
IN_OWNER: ${{ github.repository_owner }}
|
|
329
|
+
IN_OWNER_TYPE: ${{ github.event.repository.owner.type }}
|
|
330
|
+
run: |
|
|
331
|
+
actor="$IN_TRIGGERING_ACTOR"
|
|
332
|
+
if [ "$actor" = 'github-actions[bot]' ]; then
|
|
333
|
+
echo "@$actor passes: every harness dispatch is made with GITHUB_TOKEN and names it."
|
|
334
|
+
exit 0
|
|
335
|
+
fi
|
|
336
|
+
lower() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; }
|
|
337
|
+
want=$(lower "$actor")
|
|
338
|
+
rest="${HARNESS_RUN_ACTORS-},"
|
|
339
|
+
listed=""
|
|
340
|
+
while [ -n "$rest" ]; do
|
|
341
|
+
entry="${rest%%,*}"
|
|
342
|
+
rest="${rest#*,}"
|
|
343
|
+
entry="${entry#"${entry%%[![:space:]]*}"}"
|
|
344
|
+
entry="${entry%"${entry##*[![:space:]]}"}"
|
|
345
|
+
[ -n "$entry" ] || continue
|
|
346
|
+
listed=1
|
|
347
|
+
if [ "$entry" = '*' ]; then
|
|
348
|
+
echo "@$actor passes: HARNESS_RUN_ACTORS carries *, which admits every writer."
|
|
349
|
+
exit 0
|
|
350
|
+
fi
|
|
351
|
+
if [ -n "$want" ] && [ "$(lower "$entry")" = "$want" ]; then
|
|
352
|
+
echo "@$actor passes: listed in HARNESS_RUN_ACTORS."
|
|
353
|
+
exit 0
|
|
354
|
+
fi
|
|
355
|
+
done
|
|
356
|
+
if [ -n "$listed" ]; then
|
|
357
|
+
why="@${actor:-(no actor)} is not listed in it."
|
|
358
|
+
elif [ "$IN_OWNER_TYPE" = User ] && [ -n "$IN_OWNER" ]; then
|
|
359
|
+
if [ -n "$want" ] && [ "$(lower "$IN_OWNER")" = "$want" ]; then
|
|
360
|
+
echo "@$actor passes: HARNESS_RUN_ACTORS is unset, which admits the owner of this user-owned repository alone."
|
|
361
|
+
exit 0
|
|
362
|
+
fi
|
|
363
|
+
why="it is unset, which admits the owner of this user-owned repository, @$IN_OWNER, alone, and @${actor:-(no actor)} is not that owner."
|
|
364
|
+
else
|
|
365
|
+
why="it is unset, and this repository has no single owner to admit (its owner type is ${IN_OWNER_TYPE:-unreadable}), so it admits nobody."
|
|
366
|
+
fi
|
|
367
|
+
echo "::error::@${actor:-(no actor)} dispatched or re-ran this run, and the repository variable HARNESS_RUN_ACTORS does not admit them: $why Nothing was launched and no credential was read. Add the login to the comma-separated repository variable HARNESS_RUN_ACTORS, or set it to * for every writer (docs/remote-execution.md, section 9)."
|
|
368
|
+
exit 1
|
|
369
|
+
|
|
277
370
|
- name: Compute the time budget
|
|
278
371
|
env:
|
|
279
372
|
IN_RUNNER_ENVIRONMENT: ${{ runner.environment }}
|
|
@@ -486,7 +579,7 @@ jobs:
|
|
|
486
579
|
|
|
487
580
|
collect:
|
|
488
581
|
needs: run
|
|
489
|
-
if: ${{ inputs.action == 'run' && !cancelled() }}
|
|
582
|
+
if: ${{ inputs.action == 'run' && !cancelled() && github.ref_name == inputs.branch }}
|
|
490
583
|
runs-on: ${{ vars.HARNESS_RUNNER || 'ubuntu-latest' }}
|
|
491
584
|
concurrency:
|
|
492
585
|
group: harness-review-${{ inputs.branch }}
|
|
@@ -497,7 +590,53 @@ jobs:
|
|
|
497
590
|
HARNESS_INPUT_BRANCH: ${{ inputs.branch }}
|
|
498
591
|
HARNESS_REMOTE_STOP: ${{ vars.HARNESS_REMOTE_STOP }}
|
|
499
592
|
HARNESS_TRIGGER_ALLOWED_BOTS: ${{ vars.HARNESS_TRIGGER_ALLOWED_BOTS }}
|
|
593
|
+
HARNESS_RUN_ACTORS: ${{ vars.HARNESS_RUN_ACTORS }}
|
|
500
594
|
steps:
|
|
595
|
+
- name: Refuse an actor not on HARNESS_RUN_ACTORS before collecting
|
|
596
|
+
env:
|
|
597
|
+
IN_TRIGGERING_ACTOR: ${{ github.triggering_actor }}
|
|
598
|
+
IN_OWNER: ${{ github.repository_owner }}
|
|
599
|
+
IN_OWNER_TYPE: ${{ github.event.repository.owner.type }}
|
|
600
|
+
run: |
|
|
601
|
+
actor="$IN_TRIGGERING_ACTOR"
|
|
602
|
+
if [ "$actor" = 'github-actions[bot]' ]; then
|
|
603
|
+
echo "@$actor passes: every harness dispatch is made with GITHUB_TOKEN and names it."
|
|
604
|
+
exit 0
|
|
605
|
+
fi
|
|
606
|
+
lower() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; }
|
|
607
|
+
want=$(lower "$actor")
|
|
608
|
+
rest="${HARNESS_RUN_ACTORS-},"
|
|
609
|
+
listed=""
|
|
610
|
+
while [ -n "$rest" ]; do
|
|
611
|
+
entry="${rest%%,*}"
|
|
612
|
+
rest="${rest#*,}"
|
|
613
|
+
entry="${entry#"${entry%%[![:space:]]*}"}"
|
|
614
|
+
entry="${entry%"${entry##*[![:space:]]}"}"
|
|
615
|
+
[ -n "$entry" ] || continue
|
|
616
|
+
listed=1
|
|
617
|
+
if [ "$entry" = '*' ]; then
|
|
618
|
+
echo "@$actor passes: HARNESS_RUN_ACTORS carries *, which admits every writer."
|
|
619
|
+
exit 0
|
|
620
|
+
fi
|
|
621
|
+
if [ -n "$want" ] && [ "$(lower "$entry")" = "$want" ]; then
|
|
622
|
+
echo "@$actor passes: listed in HARNESS_RUN_ACTORS."
|
|
623
|
+
exit 0
|
|
624
|
+
fi
|
|
625
|
+
done
|
|
626
|
+
if [ -n "$listed" ]; then
|
|
627
|
+
why="@${actor:-(no actor)} is not listed in it."
|
|
628
|
+
elif [ "$IN_OWNER_TYPE" = User ] && [ -n "$IN_OWNER" ]; then
|
|
629
|
+
if [ -n "$want" ] && [ "$(lower "$IN_OWNER")" = "$want" ]; then
|
|
630
|
+
echo "@$actor passes: HARNESS_RUN_ACTORS is unset, which admits the owner of this user-owned repository alone."
|
|
631
|
+
exit 0
|
|
632
|
+
fi
|
|
633
|
+
why="it is unset, which admits the owner of this user-owned repository, @$IN_OWNER, alone, and @${actor:-(no actor)} is not that owner."
|
|
634
|
+
else
|
|
635
|
+
why="it is unset, and this repository has no single owner to admit (its owner type is ${IN_OWNER_TYPE:-unreadable}), so it admits nobody."
|
|
636
|
+
fi
|
|
637
|
+
echo "::error::@${actor:-(no actor)} dispatched or re-ran this run, and the repository variable HARNESS_RUN_ACTORS does not admit them: $why Nothing was launched and no credential was read. Add the login to the comma-separated repository variable HARNESS_RUN_ACTORS, or set it to * for every writer (docs/remote-execution.md, section 9)."
|
|
638
|
+
exit 1
|
|
639
|
+
|
|
501
640
|
- name: Check out the default branch for collecting
|
|
502
641
|
uses: actions/checkout@v5
|
|
503
642
|
with:
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
# the authority.
|
|
34
34
|
#
|
|
35
35
|
# WHO CAN START A RUN. `remote-run.sh`'s `trigger` paragraph owns the check —
|
|
36
|
-
# the labeller, bots and HARNESS_TRIGGER_ALLOWED_BOTS, the permission call
|
|
36
|
+
# the labeller, bots and HARNESS_TRIGGER_ALLOWED_BOTS, the permission call, the
|
|
37
|
+
# allow-list HARNESS_RUN_ACTORS, and the `repository_dispatch` sender check —
|
|
37
38
|
# and the order its refusals are made in. Nothing here pre-filters on it.
|
|
38
39
|
#
|
|
39
40
|
# THE PERMISSIONS, declared because the default token may be read-only
|
|
@@ -69,6 +70,7 @@
|
|
|
69
70
|
# HARNESS_REMOTE_STOP, HARNESS_TRIGGER_LABEL,
|
|
70
71
|
# DEFAULT_TRIGGER_LABEL (`sdlc-harness`),
|
|
71
72
|
# HARNESS_TRIGGER_ALLOWED_BOTS,
|
|
73
|
+
# HARNESS_RUN_ACTORS,
|
|
72
74
|
# TRIGGER_DISPATCH_EVENT_TYPE (`harness-task`)
|
|
73
75
|
# cli/src/config/model.ts DEFAULTS.scriptsDir
|
|
74
76
|
#
|
|
@@ -112,6 +114,7 @@ jobs:
|
|
|
112
114
|
HARNESS_REMOTE_SLUG: ${{ github.repository }}
|
|
113
115
|
HARNESS_TRIGGER_LABEL: ${{ vars.HARNESS_TRIGGER_LABEL || 'sdlc-harness' }}
|
|
114
116
|
HARNESS_TRIGGER_ALLOWED_BOTS: ${{ vars.HARNESS_TRIGGER_ALLOWED_BOTS }}
|
|
117
|
+
HARNESS_RUN_ACTORS: ${{ vars.HARNESS_RUN_ACTORS }}
|
|
115
118
|
steps:
|
|
116
119
|
- name: Check out the default branch
|
|
117
120
|
uses: actions/checkout@v5
|
|
@@ -329,6 +329,20 @@
|
|
|
329
329
|
# count are seeded from it; the auto-resume count only when
|
|
330
330
|
# HARNESS_INPUT_CHAIN is above 0, because a chain of 0 is a user's own
|
|
331
331
|
# dispatch; `chain` never — every write records this job's own input.
|
|
332
|
+
# * THE PAUSE NOTE belongs to the run that wrote it. Before its own first
|
|
333
|
+
# write, the job reads the restored `remote_status.json`'s `status` and
|
|
334
|
+
# `engine`. In job mode, a carried `PAUSE_PROGRESS.md` is kept only when
|
|
335
|
+
# the job's `resume` is `pause`, the restored `remote_status.json` says
|
|
336
|
+
# `status: paused`, and its `engine` equals the job's engine. In every other
|
|
337
|
+
# case — a fresh launch (`resume none`), an answer resume (`resume
|
|
338
|
+
# answer`), a pause resume whose previous job did not pause (a stop or a
|
|
339
|
+
# kill), or a different engine — it is moved aside, never deleted, to
|
|
340
|
+
# `<state_dir>/autonomous_logs/remote_superseded/<epoch>[-<n>]/PAUSE_PROGRESS.md`.
|
|
341
|
+
# The move is lib/harness-run-lib.sh's `hr_remote_move_aside`; this script
|
|
342
|
+
# writes nothing under `remote_superseded/` itself. Separately, a pause
|
|
343
|
+
# resume whose previous job did not pause, or paused for a different
|
|
344
|
+
# engine, is re-launched with a clause saying there is no pause note,
|
|
345
|
+
# whether or not a note was carried (`pause_note_stale` in the registry).
|
|
332
346
|
# * IT WRITES `remote_status.json` with decision `continue` before the spawn,
|
|
333
347
|
# so a job killed mid-run leaves a bundle that says continue, again after
|
|
334
348
|
# every successful control poll, and once more when the run leaves
|
|
@@ -1205,17 +1219,27 @@ job_report() {
|
|
|
1205
1219
|
# resumed_at when the most recent resume happened — stamped by BOTH
|
|
1206
1220
|
# resume paths, so it does not say which one
|
|
1207
1221
|
# resumed_for_index a space-separated list of the clarification indexes one
|
|
1208
|
-
# park-resume consumed (`1 2 3`). Set by
|
|
1209
|
-
# and archived then cleared by classify_run_exit
|
|
1210
|
-
# the whole of its
|
|
1211
|
-
#
|
|
1212
|
-
#
|
|
1213
|
-
#
|
|
1214
|
-
#
|
|
1215
|
-
#
|
|
1216
|
-
#
|
|
1217
|
-
#
|
|
1218
|
-
#
|
|
1222
|
+
# park-resume consumed (`1 2 3`). Set by begin_park_resume
|
|
1223
|
+
# and archived then cleared by classify_run_exit together
|
|
1224
|
+
# with `launch_answered_set`, which is the whole of its
|
|
1225
|
+
# lifetime — so A NON-EMPTY VALUE ON A `completed` RECORD
|
|
1226
|
+
# IS A DEFECT: it means the pairs it names are still
|
|
1227
|
+
# sitting unarchived at the top level, where they trigger
|
|
1228
|
+
# a resume of a run that already read them. It is NOT a
|
|
1229
|
+
# defect on a `paused` record: the pause branch returns
|
|
1230
|
+
# before the archival and leaves it set on purpose,
|
|
1231
|
+
# because a pause mid park-resume left those answers
|
|
1232
|
+
# unconsumed. The pause resume never writes this field —
|
|
1233
|
+
# a pause is not an answer. It is not the only record of
|
|
1234
|
+
# the consumed set: `launch_answered_set` re-derives it
|
|
1235
|
+
# launch_answered_set every top-level answered pair's index when the current
|
|
1236
|
+
# session launched (`1 2`), which is the set a re-entering
|
|
1237
|
+
# engine consumes. Written by spawn_engine at every launch,
|
|
1238
|
+
# local and job mode alike, from the clarification channel
|
|
1239
|
+
# on disk; archived then cleared by classify_run_exit on a
|
|
1240
|
+
# non-pause exit, and left set by a pause exit. The remote
|
|
1241
|
+
# bundle does not carry it: the next job's launch
|
|
1242
|
+
# re-derives it from the restored channel
|
|
1219
1243
|
# resume_kind `answer` when the park resume launched the current
|
|
1220
1244
|
# session, `pause` when the pause resume did, and empty
|
|
1221
1245
|
# otherwise. Read and cleared by classify_run_exit on every
|
|
@@ -1311,7 +1335,14 @@ job_report() {
|
|
|
1311
1335
|
# bound of the next control poll, this job's or the next
|
|
1312
1336
|
# chained one's. Set at start (see JOB MODE) and advanced
|
|
1313
1337
|
# by every successful poll
|
|
1314
|
-
#
|
|
1338
|
+
# pause_note_stale job mode only: `1` when a `pause` job's restored
|
|
1339
|
+
# `status.json` was not `paused` or named another engine,
|
|
1340
|
+
# so spawn_engine's pause-resume prompt says there is no
|
|
1341
|
+
# pause note instead of naming PAUSE_PROGRESS.md. Cleared
|
|
1342
|
+
# with run_job's fresh-launch defaults, and by
|
|
1343
|
+
# classify_run_exit's job-mode pause arm, whose session
|
|
1344
|
+
# just wrote its own note for a later relaunch to read
|
|
1345
|
+
# execution `github-actions` on a remote record — one
|
|
1315
1346
|
# launch_remote_run wrote through
|
|
1316
1347
|
# lib/harness-run-lib.sh's `hr_remote_record_init` — and
|
|
1317
1348
|
# absent on a local one. Fixed for the run's life: a later
|
|
@@ -1803,8 +1834,18 @@ files, each paired by index with its question_<i>.md, and resume from the park p
|
|
|
1803
1834
|
# committed flow-progress LEDGER (deterministic), with PAUSE_PROGRESS.md as a
|
|
1804
1835
|
# human-readable hint. Mutually exclusive with the clarification resume above:
|
|
1805
1836
|
# a run resumes from a park OR from a pause, never both.
|
|
1837
|
+
# JOB MODE ONLY: `pause_note_stale` (see the registry) swaps the note-naming
|
|
1838
|
+
# opening for the stop/kill one; a local record never carries the field.
|
|
1839
|
+
local pause_note_stale=""
|
|
1840
|
+
if [ -n "$pause_resume" ] && [ "$JOB_MODE" = "1" ]; then
|
|
1841
|
+
pause_note_stale="$(registry_get "$branch" pause_note_stale)"
|
|
1842
|
+
fi
|
|
1806
1843
|
local pause_resume_clause=""
|
|
1807
|
-
if [ -n "$pause_resume" ]; then
|
|
1844
|
+
if [ -n "$pause_resume" ] && [ "$pause_note_stale" = "1" ]; then
|
|
1845
|
+
pause_resume_clause="This is a RESUME after the previous job was stopped or ended without pausing: there is no pause \
|
|
1846
|
+
note — resume strictly from the committed flow-progress ledger ${state_rel}/flow_progress/${branch}_progress.md — continue \
|
|
1847
|
+
at the first phase entry still marked [ ] and SKIP every phase already marked [x]; do NOT restart completed phases. "
|
|
1848
|
+
elif [ -n "$pause_resume" ]; then
|
|
1808
1849
|
pause_resume_clause="This is a RESUME from a PAUSE: read ${state_rel}/PAUSE_PROGRESS.md for the pause note, then \
|
|
1809
1850
|
resume strictly from the committed flow-progress ledger ${state_rel}/flow_progress/${branch}_progress.md — continue at the \
|
|
1810
1851
|
first phase entry still marked [ ] and SKIP every phase already marked [x]; do NOT restart completed phases. "
|
|
@@ -1852,7 +1893,11 @@ ${GLOBAL_STOP}. End at 'branch ready for review' — never merge, never push to
|
|
|
1852
1893
|
# own pause-resume clause pointing at the checklist, and omits the
|
|
1853
1894
|
# clarification-channel language the other two carry.
|
|
1854
1895
|
local docs_pause_clause=""
|
|
1855
|
-
if [ -n "$pause_resume" ]; then
|
|
1896
|
+
if [ -n "$pause_resume" ] && [ "$pause_note_stale" = "1" ]; then
|
|
1897
|
+
docs_pause_clause="This is a RESUME after the previous job was stopped or ended without pausing: there is no \
|
|
1898
|
+
pause note — resume strictly from the checklist ${state_rel}/docs_catalog/${branch}_docs.md — continue at the first entry \
|
|
1899
|
+
still marked [ ] and SKIP every entry already marked [x]; do NOT rewrite completed docs. "
|
|
1900
|
+
elif [ -n "$pause_resume" ]; then
|
|
1856
1901
|
docs_pause_clause="This is a RESUME from a PAUSE: read ${state_rel}/PAUSE_PROGRESS.md for the pause note, then \
|
|
1857
1902
|
resume strictly from the checklist ${state_rel}/docs_catalog/${branch}_docs.md — continue at the first entry still marked [ ] \
|
|
1858
1903
|
and SKIP every entry already marked [x]; do NOT rewrite completed docs. "
|
|
@@ -1929,6 +1974,11 @@ EOF
|
|
|
1929
1974
|
formatter="cat"
|
|
1930
1975
|
fi
|
|
1931
1976
|
|
|
1977
|
+
# The answered pairs this session will consume, re-derived from disk at every
|
|
1978
|
+
# launch so no job boundary can lose them; classify_run_exit archives them.
|
|
1979
|
+
registry_set "$branch" launch_answered_set \
|
|
1980
|
+
"$(top_level_answered_pairs "$worktree/$state_rel/clarifications/$branch")"
|
|
1981
|
+
|
|
1932
1982
|
# Spawn ONE detached subshell that runs the agent IN THE FOREGROUND and then
|
|
1933
1983
|
# classifies the exit from its REAL exit code. The agent must be a CHILD of
|
|
1934
1984
|
# this subshell — not a sibling of a separate monitor — or that code is
|
|
@@ -2168,11 +2218,13 @@ max_question_index() {
|
|
|
2168
2218
|
#
|
|
2169
2219
|
# Consume-then-archive contract: on a resume the watcher LEAVES the whole
|
|
2170
2220
|
# answered set at the TOP LEVEL so the re-launched engine can self-detect and
|
|
2171
|
-
# consume it
|
|
2172
|
-
#
|
|
2173
|
-
#
|
|
2174
|
-
#
|
|
2175
|
-
#
|
|
2221
|
+
# consume it, and a re-entering engine consumes EVERY top-level answered pair.
|
|
2222
|
+
# So the archived set is every pair answered when the session launched — the
|
|
2223
|
+
# union of `resumed_for_index` and `launch_answered_set` — archived only AFTER
|
|
2224
|
+
# that session exits, and never on a pause exit. That is what stops a pair the
|
|
2225
|
+
# engine already read from triggering another resume, without emptying the
|
|
2226
|
+
# paths the re-entering engine reads; a pair written mid-session is in neither
|
|
2227
|
+
# set and stays.
|
|
2176
2228
|
classify_run_exit() {
|
|
2177
2229
|
local branch="$1" worktree="$2" log_path="$3" rc="$4"
|
|
2178
2230
|
|
|
@@ -2226,7 +2278,9 @@ classify_run_exit() {
|
|
|
2226
2278
|
reason=overload
|
|
2227
2279
|
fi
|
|
2228
2280
|
fi
|
|
2229
|
-
|
|
2281
|
+
# pause_note_stale is cleared here, not in a shell global: this runs in the
|
|
2282
|
+
# launch subshell, and only a registry write reaches run_job.
|
|
2283
|
+
registry_set "$branch" pause_reason "$reason" status paused pause_note_stale ""
|
|
2230
2284
|
log "run '$branch' paused (PAUSE honored, reason $reason) — rc=$rc"
|
|
2231
2285
|
if [ "$reason" = "user" ]; then
|
|
2232
2286
|
notify paused "$branch" "$log_path" "paused as you asked — run /autonomous-sdlc-harness:branch-resume $branch to continue; $(hr_github_resume_route "$branch" "$(registry_get "$branch" engine)")"
|
|
@@ -2239,23 +2293,25 @@ classify_run_exit() {
|
|
|
2239
2293
|
return 0
|
|
2240
2294
|
fi
|
|
2241
2295
|
|
|
2242
|
-
#
|
|
2243
|
-
#
|
|
2244
|
-
#
|
|
2296
|
+
# This exit was NOT a pause, handled above, so the session consumed every pair
|
|
2297
|
+
# it launched with: the union of `resumed_for_index` and `launch_answered_set`.
|
|
2298
|
+
# Archive exactly that union before classifying, so none of it is ever
|
|
2245
2299
|
# reprocessed; nothing else is archived. A legacy single-index value is a
|
|
2246
2300
|
# one-element list.
|
|
2247
|
-
local consumed_set consumed_n
|
|
2248
|
-
consumed_set="$(registry_get "$branch" resumed_for_index)"
|
|
2249
|
-
if [ -n "$consumed_set" ]; then
|
|
2301
|
+
local consumed_set consumed_n archived=" "
|
|
2302
|
+
consumed_set="$(registry_get "$branch" resumed_for_index) $(registry_get "$branch" launch_answered_set)"
|
|
2303
|
+
if [ -n "${consumed_set// /}" ]; then
|
|
2250
2304
|
# An empty clar_dir means the state directory was unresolvable above; the
|
|
2251
|
-
#
|
|
2305
|
+
# fields are still cleared, because leaving them set would make the next exit
|
|
2252
2306
|
# try to archive pairs whose location is no better known than it is now.
|
|
2253
2307
|
if [ -n "$clar_dir" ]; then
|
|
2254
2308
|
for consumed_n in $consumed_set; do
|
|
2309
|
+
case "$archived" in *" $consumed_n "*) continue ;; esac
|
|
2310
|
+
archived="${archived}${consumed_n} "
|
|
2255
2311
|
archive_answered_pair "$clar_dir" "$consumed_n"
|
|
2256
2312
|
done
|
|
2257
2313
|
fi
|
|
2258
|
-
registry_set "$branch" resumed_for_index ""
|
|
2314
|
+
registry_set "$branch" resumed_for_index "" launch_answered_set ""
|
|
2259
2315
|
fi
|
|
2260
2316
|
|
|
2261
2317
|
local parked=0
|
|
@@ -2399,6 +2455,32 @@ park_answered_set() {
|
|
|
2399
2455
|
printf '%s\n' "${answered_set% }"
|
|
2400
2456
|
}
|
|
2401
2457
|
|
|
2458
|
+
# top_level_answered_pairs <clar_dir>
|
|
2459
|
+
#
|
|
2460
|
+
# Every top-level index with both files, space-separated and numerically sorted,
|
|
2461
|
+
# whether or not another top-level question is still unanswered; prints nothing
|
|
2462
|
+
# when there is none. spawn_engine records it at every launch. Each
|
|
2463
|
+
# index is printed as its file name spells it, because archive_answered_pair
|
|
2464
|
+
# rebuilds the file names from it; `sort -n` orders a zero-padded one in base 10.
|
|
2465
|
+
top_level_answered_pairs() {
|
|
2466
|
+
local clar_dir="$1" q n answered_list="" answered_set
|
|
2467
|
+
for q in "$clar_dir"/question_*.md; do
|
|
2468
|
+
[ -e "$q" ] || continue
|
|
2469
|
+
n="${q##*/}"
|
|
2470
|
+
n="${n#question_}"
|
|
2471
|
+
n="${n%.md}"
|
|
2472
|
+
case "$n" in
|
|
2473
|
+
'' | *[!0-9]*) continue ;;
|
|
2474
|
+
esac
|
|
2475
|
+
[ -f "$clar_dir/answer_${n}.md" ] || continue
|
|
2476
|
+
answered_list="${answered_list}${n}
|
|
2477
|
+
"
|
|
2478
|
+
done
|
|
2479
|
+
[ -n "$answered_list" ] || return 0
|
|
2480
|
+
answered_set="$(printf '%s' "$answered_list" | sort -n | tr '\n' ' ')"
|
|
2481
|
+
printf '%s\n' "${answered_set% }"
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2402
2484
|
# begin_park_resume <branch> <clar_dir> <answered_set>
|
|
2403
2485
|
#
|
|
2404
2486
|
# The registry half of a park resume, shared by resume_parked_run and job mode.
|
|
@@ -4060,6 +4142,7 @@ run_job() {
|
|
|
4060
4142
|
local branch="$1" engine="$2" resume="$3"
|
|
4061
4143
|
local worktree="$MAIN_REPO" log_path="$LOGS_DIR/$branch.log"
|
|
4062
4144
|
local state_rel state_abs clar_dir remote_status key value answered_set="" prev_reason=""
|
|
4145
|
+
local prev_status="" prev_engine="" aside_rc
|
|
4063
4146
|
|
|
4064
4147
|
JOB_START_EPOCH="$(job_int "${HARNESS_JOB_STARTED_EPOCH:-}")" || JOB_START_EPOCH="$(date +%s)"
|
|
4065
4148
|
state_rel="$(run_state_dir "$worktree")" || fatal "job: the state directory in '$worktree' is unresolvable"
|
|
@@ -4083,8 +4166,11 @@ run_job() {
|
|
|
4083
4166
|
registry_set "$branch" auto_resumes 0
|
|
4084
4167
|
registry_set "$branch" pause_reason ""
|
|
4085
4168
|
registry_set "$branch" control_polled_at ""
|
|
4169
|
+
registry_set "$branch" pause_note_stale ""
|
|
4086
4170
|
if [ -f "$remote_status" ]; then
|
|
4087
4171
|
prev_reason="$(hr_remote_status_get "$remote_status" pause_reason)" || prev_reason=""
|
|
4172
|
+
prev_status="$(hr_remote_status_get "$remote_status" status)" || prev_status=""
|
|
4173
|
+
prev_engine="$(hr_remote_status_get "$remote_status" engine)" || prev_engine=""
|
|
4088
4174
|
for key in park_loop_cycles resume_max_question_index stall_restarts; do
|
|
4089
4175
|
value="$(hr_remote_status_get "$remote_status" "$key")" && registry_set "$branch" "$key" "$value"
|
|
4090
4176
|
done
|
|
@@ -4095,6 +4181,25 @@ run_job() {
|
|
|
4095
4181
|
fi
|
|
4096
4182
|
fi
|
|
4097
4183
|
|
|
4184
|
+
# The pause note belongs to the run that wrote it (the header's JOB MODE
|
|
4185
|
+
# block). Two decisions, deliberately independent: the clause reads the
|
|
4186
|
+
# restored status alone, so a run stopped before it ever paused is never
|
|
4187
|
+
# pointed at a note; the move governs only a note that exists.
|
|
4188
|
+
if [ "$resume" = "pause" ] && { [ "$prev_status" != "paused" ] || [ "$prev_engine" != "$engine" ]; }; then
|
|
4189
|
+
registry_set "$branch" pause_note_stale 1
|
|
4190
|
+
log "job: '$branch' resumes after a job that did not pause for engine $engine (prev_status '${prev_status}', prev_engine '${prev_engine}') — no pause note"
|
|
4191
|
+
fi
|
|
4192
|
+
if ! { [ "$resume" = "pause" ] && [ "$prev_status" = "paused" ] && [ "$prev_engine" = "$engine" ]; } \
|
|
4193
|
+
&& [ -f "$state_abs/$HR_REMOTE_PAUSE_FILE" ]; then
|
|
4194
|
+
aside_rc=0
|
|
4195
|
+
hr_remote_move_aside "$worktree" "$HR_REMOTE_PAUSE_FILE" || aside_rc=$?
|
|
4196
|
+
case "$aside_rc" in
|
|
4197
|
+
0) log "job: '$branch' carried a $HR_REMOTE_PAUSE_FILE that is not this run's (resume $resume, prev_status '${prev_status}', prev_engine '${prev_engine}', engine $engine) — moved aside to $HR_REMOTE_ASIDE" ;;
|
|
4198
|
+
3) ;;
|
|
4199
|
+
*) log "job: WARNING — could not move the carried $HR_REMOTE_PAUSE_FILE of '$branch' aside (hr_remote_move_aside exit $aside_rc); continuing" ;;
|
|
4200
|
+
esac
|
|
4201
|
+
fi
|
|
4202
|
+
|
|
4098
4203
|
registry_set "$branch" engine "$engine"
|
|
4099
4204
|
registry_set "$branch" worktree "$worktree"
|
|
4100
4205
|
registry_set "$branch" log_path "$log_path"
|
|
@@ -79,9 +79,12 @@
|
|
|
79
79
|
# files under the eight planning paths `hr_remote_planning_paths` assigns;
|
|
80
80
|
# outside it, only the caller-named `<out_dir>` (its `planning/` included)
|
|
81
81
|
# of `hr_remote_bundle_write` and the caller-named `<out_json>`
|
|
82
|
-
# of `hr_remote_status_write`.
|
|
83
|
-
#
|
|
84
|
-
#
|
|
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.
|
|
85
88
|
# 4. THE ARTIFACT PLACEMENT writes one artifact into a working copy. Fence:
|
|
86
89
|
# the caller-named `<worktree>/<rel>`, its parent directories and that
|
|
87
90
|
# path's index entry, plus whatever the two caller-named wrappers do.
|
|
@@ -95,7 +98,7 @@
|
|
|
95
98
|
#
|
|
96
99
|
# A caller that calls no `hr_lane_*`, `hr_registry_init`, `hr_registry_set`,
|
|
97
100
|
# `hr_remote_record_init`, `hr_registry_lock`, `hr_registry_unlock`, `hr_remote_status_write`,
|
|
98
|
-
# `hr_remote_bundle_write`, `hr_remote_bundle_restore`, `hr_place_artifact`,
|
|
101
|
+
# `hr_remote_bundle_write`, `hr_remote_bundle_restore`, `hr_remote_move_aside`, `hr_place_artifact`,
|
|
99
102
|
# `hr_commit_placed` or `hr_push_landed` function still gets a library that only reads. The
|
|
100
103
|
# lane's ceilings are the only environment values here that carry policy, because
|
|
101
104
|
# the lane is machine-scoped and has no configuration key to carry them; each is
|
|
@@ -211,7 +214,8 @@
|
|
|
211
214
|
# `HR_LANE_OWNER_AT` and `HR_LANE_BROKEN_OWNER`, and the remote state bundle's
|
|
212
215
|
# names, which `hr_remote_names_var` assigns, with `HR_REMOTE_PLANNING_PATHS`
|
|
213
216
|
# (`hr_remote_planning_paths`) and `HR_REMOTE_PLANNING_PLACED` /
|
|
214
|
-
# `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
|
|
215
219
|
# before it is read by the function that owns it, so an inherited value from a
|
|
216
220
|
# parent process is overwritten rather than believed.
|
|
217
221
|
#
|
|
@@ -2179,6 +2183,45 @@ EOF
|
|
|
2179
2183
|
return 0
|
|
2180
2184
|
}
|
|
2181
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
|
+
|
|
2182
2225
|
# hr_remote_bundle_restore <bundle_dir> <root> <branch> <mode>
|
|
2183
2226
|
#
|
|
2184
2227
|
# <mode> `job` places the clarification directory, `PAUSE_PROGRESS.md`, the
|
|
@@ -2195,20 +2238,18 @@ EOF
|
|
|
2195
2238
|
# stay `0` in `mirror` mode.
|
|
2196
2239
|
#
|
|
2197
2240
|
# THE CLARIFICATION DIRECTORY IS REPLACED WHOLESALE, AND NOTHING IS DELETED. An
|
|
2198
|
-
# existing target is moved aside
|
|
2199
|
-
#
|
|
2200
|
-
#
|
|
2201
|
-
#
|
|
2202
|
-
#
|
|
2203
|
-
#
|
|
2204
|
-
#
|
|
2205
|
-
#
|
|
2206
|
-
#
|
|
2207
|
-
# a schema other than `HR_REMOTE_STATE_SCHEMA`, or a `branch` other than
|
|
2208
|
-
# <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.
|
|
2209
2250
|
hr_remote_bundle_restore() {
|
|
2210
2251
|
local bundle="${1-}" root="${2-}" branch="${3-}" mode="${4-}"
|
|
2211
|
-
local state base named target
|
|
2252
|
+
local state base named target tmp pdir file rel p inset
|
|
2212
2253
|
HR_REMOTE_PLANNING_PLACED=0
|
|
2213
2254
|
HR_REMOTE_PLANNING_KEPT=0
|
|
2214
2255
|
[ -n "$bundle" ] && [ -n "$root" ] && [ -n "$branch" ] || return 1
|
|
@@ -2226,16 +2267,7 @@ hr_remote_bundle_restore() {
|
|
|
2226
2267
|
if [ -d "$bundle/$HR_REMOTE_CLARIFY_DIR/$branch" ]; then
|
|
2227
2268
|
target="$base/$HR_REMOTE_CLARIFY_DIR/$branch"
|
|
2228
2269
|
if [ -e "$target" ]; then
|
|
2229
|
-
|
|
2230
|
-
aside="$base/$HR_REMOTE_SUPERSEDED_DIR/$epoch"
|
|
2231
|
-
n=0
|
|
2232
|
-
while [ -e "$aside/$HR_REMOTE_CLARIFY_DIR/$branch" ]; do
|
|
2233
|
-
n=$((n + 1))
|
|
2234
|
-
aside="$base/$HR_REMOTE_SUPERSEDED_DIR/$epoch-$n"
|
|
2235
|
-
done
|
|
2236
|
-
aside="$aside/$HR_REMOTE_CLARIFY_DIR/$branch"
|
|
2237
|
-
mkdir -p "${aside%/*}" 2>/dev/null || return 1
|
|
2238
|
-
mv "$target" "$aside" 2>/dev/null || return 1
|
|
2270
|
+
hr_remote_move_aside "$root" "$HR_REMOTE_CLARIFY_DIR/$branch" || return 1
|
|
2239
2271
|
fi
|
|
2240
2272
|
mkdir -p "${target%/*}" 2>/dev/null || return 1
|
|
2241
2273
|
cp -R "$bundle/$HR_REMOTE_CLARIFY_DIR/$branch" "$target" 2>/dev/null || return 1
|