autonomous-sdlc-harness 0.4.1 → 0.4.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/package.json +1 -1
- package/templates/github/workflows/harness-resume.yml +21 -5
- package/templates/github/workflows/harness-run.yml +41 -19
- package/templates/scripts/autonomous-watcher.sh +3 -1
- package/templates/scripts/lib/harness-run-lib.sh +121 -15
- package/templates/scripts/remote-run.sh +20 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autonomous-sdlc-harness",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "The outer loop of the autonomous SDLC harness: init, doctor, config and daemon management for the harness Claude Code plugin, plus opt-in local docs retrieval.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "firu-daniel",
|
|
@@ -17,9 +17,12 @@
|
|
|
17
17
|
# That also makes moot GitHub's disabling of scheduled workflows in a public
|
|
18
18
|
# repository after 60 days without activity.
|
|
19
19
|
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
20
|
+
# Gate 12 round 2, on 2026-09-29, verified that `gh workflow disable` succeeds
|
|
21
|
+
# under GITHUB_TOKEN with `actions: write`: a hand-started tick logged
|
|
22
|
+
# `poll: no branch is waiting; disabled harness-resume.yml`, and the workflow's
|
|
23
|
+
# state became `disabled_manually`. UNVERIFIED: the enable, because no job
|
|
24
|
+
# ended on a usage pause. If it is refused, the pausing job's `paused`
|
|
25
|
+
# notification says auto-resume is unavailable, and the run waits for
|
|
23
26
|
# /autonomous-sdlc-harness:branch-resume.
|
|
24
27
|
#
|
|
25
28
|
# THE INTERVAL IS YOURS TO EDIT. On a private repository every tick is billed
|
|
@@ -56,6 +59,19 @@
|
|
|
56
59
|
# HARNESS_REMOTE_STOP, HARNESS_PUSH_URL,
|
|
57
60
|
# POLL_STATE_ARTIFACT_NAME
|
|
58
61
|
# cli/src/config/model.ts DEFAULTS.scriptsDir, DEFAULTS.stateDir
|
|
62
|
+
#
|
|
63
|
+
# ACTION PINS.
|
|
64
|
+
# actions/checkout@v5
|
|
65
|
+
# actions/upload-artifact@v6
|
|
66
|
+
# Each is the lowest major whose own action.yml declares `runs.using: node24`,
|
|
67
|
+
# per a maintainer's lookups of each action's action.yml and release notes on
|
|
68
|
+
# 2026-09-29T08:21Z (UTC). Each of those majors' release notes requires Actions
|
|
69
|
+
# Runner 2.327.1 or newer; a GitHub-hosted runner already has it, and a
|
|
70
|
+
# self-hosted one must run it (docs/remote-execution.md, section 8). A major
|
|
71
|
+
# tag, not a commit sha: both are GitHub's own `actions/` organisation, the
|
|
72
|
+
# owner of the runner and GITHUB_TOKEN this job already trusts, and a major tag
|
|
73
|
+
# takes the action's own patch and security releases where a sha would freeze
|
|
74
|
+
# your copy. Pin shas in your copy if you want them.
|
|
59
75
|
|
|
60
76
|
name: harness-resume
|
|
61
77
|
|
|
@@ -89,7 +105,7 @@ jobs:
|
|
|
89
105
|
HARNESS_PUSH_URL: ${{ secrets.HARNESS_PUSH_URL }}
|
|
90
106
|
steps:
|
|
91
107
|
- name: Check out the default branch
|
|
92
|
-
uses: actions/checkout@
|
|
108
|
+
uses: actions/checkout@v5
|
|
93
109
|
with:
|
|
94
110
|
ref: ${{ github.event.repository.default_branch }}
|
|
95
111
|
|
|
@@ -115,7 +131,7 @@ jobs:
|
|
|
115
131
|
|
|
116
132
|
- name: Upload the poller state
|
|
117
133
|
if: always() && env.POLL_STATE_DIR != ''
|
|
118
|
-
uses: actions/upload-artifact@
|
|
134
|
+
uses: actions/upload-artifact@v6
|
|
119
135
|
with:
|
|
120
136
|
name: harness-poll-state
|
|
121
137
|
path: ${{ env.POLL_STATE_DIR }}
|
|
@@ -38,6 +38,22 @@
|
|
|
38
38
|
# runtime sits in `retrieval/`
|
|
39
39
|
# cli/src/retrieval/runtime.ts RETRIEVAL_CACHE_DIRNAME, that `retrieval/`
|
|
40
40
|
#
|
|
41
|
+
# ACTION PINS.
|
|
42
|
+
# actions/checkout@v5
|
|
43
|
+
# actions/setup-node@v5
|
|
44
|
+
# actions/cache/restore@v5
|
|
45
|
+
# actions/cache@v5
|
|
46
|
+
# actions/upload-artifact@v6
|
|
47
|
+
# Each is the lowest major whose own action.yml declares `runs.using: node24`,
|
|
48
|
+
# per a maintainer's lookups of each action's action.yml and release notes on
|
|
49
|
+
# 2026-09-29T08:21Z (UTC). Each of those majors' release notes requires Actions
|
|
50
|
+
# Runner 2.327.1 or newer; a GitHub-hosted runner already has it, and a
|
|
51
|
+
# self-hosted one must run it (docs/remote-execution.md, section 8). A major
|
|
52
|
+
# tag, not a commit sha: every action here is GitHub's own `actions/`
|
|
53
|
+
# organisation, the owner of the runner and GITHUB_TOKEN this job already
|
|
54
|
+
# trusts, and a major tag takes the action's own patch and security releases
|
|
55
|
+
# where a sha would freeze your copy. Pin shas in your copy if you want them.
|
|
56
|
+
#
|
|
41
57
|
# WHAT IT READS.
|
|
42
58
|
# Secrets: CLAUDE_CODE_OAUTH_TOKEN and/or ANTHROPIC_API_KEY (one is required;
|
|
43
59
|
# billing follows the API key when both are set), HARNESS_GIT_TOKEN (optional:
|
|
@@ -72,12 +88,12 @@
|
|
|
72
88
|
# the step timeout. The job-level timeout-minutes is the self-hosted limit; a
|
|
73
89
|
# hosted job is stopped at its own limit whatever that says.
|
|
74
90
|
#
|
|
75
|
-
# WHY THE TIMEOUT IS COMPUTED INTO GITHUB_ENV.
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
91
|
+
# WHY THE TIMEOUT IS COMPUTED INTO GITHUB_ENV. The first step computes every
|
|
92
|
+
# budget value from `runner.environment` into GITHUB_ENV, so the harness step's
|
|
93
|
+
# timeout-minutes reads a single `env` value rather than an expression over the
|
|
94
|
+
# runner kind. Gate 12 round 2, on 2026-09-29, found every harness-run.yml run
|
|
95
|
+
# accepted and its `Run the harness` step run: the expression-valued
|
|
96
|
+
# timeout-minutes is accepted.
|
|
81
97
|
#
|
|
82
98
|
# THE PLUGIN PIN. `claude plugin marketplace add --help` and
|
|
83
99
|
# `claude plugin install --help` (Claude Code 2.1.282) offer no ref or version:
|
|
@@ -88,17 +104,19 @@
|
|
|
88
104
|
# rendered CLI version.
|
|
89
105
|
#
|
|
90
106
|
# WHY retention-days IS SET. The `harness-state` bundle is the only remote copy
|
|
91
|
-
# of a run's clarifications and carried counts; a
|
|
92
|
-
# than its retention cannot be answered and loses
|
|
107
|
+
# of a run's clarifications, uncommitted planning drafts and carried counts; a
|
|
108
|
+
# run parked or paused longer than its retention cannot be answered and loses
|
|
109
|
+
# those drafts and counts. 400 is the
|
|
93
110
|
# largest retention any repository can configure, and `actions/upload-artifact`
|
|
94
111
|
# caps a larger `retention-days` at the repository's own maximum, so the value
|
|
95
112
|
# means "as long as this repository allows" — the same bound as leaving it
|
|
96
113
|
# unset. It is explicit so that lowering it is a visible edit. The real bound is
|
|
97
114
|
# the repository's Artifact and log retention setting, which
|
|
98
|
-
# `autonomous-sdlc-harness doctor --check-github` reads.
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
115
|
+
# `autonomous-sdlc-harness doctor --check-github` reads. Gate 12 round 2, on
|
|
116
|
+
# 2026-09-29, observed the cap rather than a rejection on `actions/upload-artifact@v4`:
|
|
117
|
+
# the artifact's `expires_at` fell 90 days after its creation, with the
|
|
118
|
+
# repository's artifact-and-log-retention at {"days":90,"maximum_allowed_days":400}.
|
|
119
|
+
# That was v4; the v6 pinned above has not yet been observed by a gate round.
|
|
102
120
|
|
|
103
121
|
name: harness-run
|
|
104
122
|
run-name: harness ${{ inputs.action }} ${{ inputs.branch }}
|
|
@@ -248,7 +266,7 @@ jobs:
|
|
|
248
266
|
|
|
249
267
|
- name: Check out the run's branch
|
|
250
268
|
if: env.HARNESS_STOPPED != '1'
|
|
251
|
-
uses: actions/checkout@
|
|
269
|
+
uses: actions/checkout@v5
|
|
252
270
|
with:
|
|
253
271
|
ref: ${{ inputs.branch }}
|
|
254
272
|
fetch-depth: 0
|
|
@@ -280,9 +298,11 @@ jobs:
|
|
|
280
298
|
|
|
281
299
|
- name: Set up Node
|
|
282
300
|
if: env.HARNESS_STOPPED != '1'
|
|
283
|
-
uses: actions/setup-node@
|
|
301
|
+
uses: actions/setup-node@v5
|
|
284
302
|
with:
|
|
285
303
|
node-version: '22'
|
|
304
|
+
# v5 caches by package.json's packageManager; the job installs nothing through npm's cache, and a lockfile-less repository must not fail this step.
|
|
305
|
+
package-manager-cache: false
|
|
286
306
|
|
|
287
307
|
- name: Install the claude CLI when absent
|
|
288
308
|
if: env.HARNESS_STOPPED != '1'
|
|
@@ -327,7 +347,7 @@ jobs:
|
|
|
327
347
|
|
|
328
348
|
- name: Restore the docs-retrieval cache
|
|
329
349
|
if: env.HARNESS_STOPPED != '1' && steps.config.outputs.retrieval == 'true'
|
|
330
|
-
uses: actions/cache/restore@
|
|
350
|
+
uses: actions/cache/restore@v5
|
|
331
351
|
with:
|
|
332
352
|
path: ${{ env.HARNESS_RETRIEVAL_CACHE }}
|
|
333
353
|
key: harness-retrieval-${{ runner.os }}-${{ env.HARNESS_CLI_VERSION }}
|
|
@@ -377,7 +397,7 @@ jobs:
|
|
|
377
397
|
|
|
378
398
|
- name: Upload the state bundle
|
|
379
399
|
if: always() && env.SCRIPTS_DIR != ''
|
|
380
|
-
uses: actions/upload-artifact@
|
|
400
|
+
uses: actions/upload-artifact@v6
|
|
381
401
|
with:
|
|
382
402
|
name: harness-state
|
|
383
403
|
path: ${{ runner.temp }}/harness-state
|
|
@@ -411,7 +431,7 @@ jobs:
|
|
|
411
431
|
|
|
412
432
|
- name: Check out the dispatched ref
|
|
413
433
|
if: env.HARNESS_STOPPED != '1'
|
|
414
|
-
uses: actions/checkout@
|
|
434
|
+
uses: actions/checkout@v5
|
|
415
435
|
|
|
416
436
|
- name: Read the configuration
|
|
417
437
|
id: config
|
|
@@ -430,13 +450,15 @@ jobs:
|
|
|
430
450
|
|
|
431
451
|
- name: Set up Node
|
|
432
452
|
if: env.HARNESS_STOPPED != '1' && steps.config.outputs.retrieval == 'true'
|
|
433
|
-
uses: actions/setup-node@
|
|
453
|
+
uses: actions/setup-node@v5
|
|
434
454
|
with:
|
|
435
455
|
node-version: '22'
|
|
456
|
+
# As in the run job: no npm cache is used, and a lockfile-less repository must not fail this step.
|
|
457
|
+
package-manager-cache: false
|
|
436
458
|
|
|
437
459
|
- name: Restore and save the docs-retrieval cache
|
|
438
460
|
if: env.HARNESS_STOPPED != '1' && steps.config.outputs.retrieval == 'true'
|
|
439
|
-
uses: actions/cache@
|
|
461
|
+
uses: actions/cache@v5
|
|
440
462
|
with:
|
|
441
463
|
path: ${{ env.HARNESS_RETRIEVAL_CACHE }}
|
|
442
464
|
key: harness-retrieval-${{ runner.os }}-${{ env.HARNESS_CLI_VERSION }}
|
|
@@ -1272,7 +1272,9 @@ notify() {
|
|
|
1272
1272
|
# bundle still says `running`, or a finished run left no
|
|
1273
1273
|
# bundle, and `expired`, a registry-only value it derives
|
|
1274
1274
|
# when a finished run's bundle has expired (the job can no
|
|
1275
|
-
# longer take an answer, and the carried counts
|
|
1275
|
+
# longer take an answer, and the carried counts and any
|
|
1276
|
+
# planning drafts not yet committed that it carried are
|
|
1277
|
+
# lost);
|
|
1276
1278
|
# `status.json` never carries either. Both map to
|
|
1277
1279
|
# `paused` rather than `failed` because a `failed` record
|
|
1278
1280
|
# has no resume path, while the ledger on the branch is
|
|
@@ -67,9 +67,11 @@
|
|
|
67
67
|
# 3. THE REMOTE STATE BUNDLE writes the files its format lists. Fence: inside
|
|
68
68
|
# `<root>/<state_dir>/` (resolved through `hr_state_dir`), only
|
|
69
69
|
# `autonomous_logs/remote_status.json`, `clarifications/<branch>/`,
|
|
70
|
-
# `PAUSE_PROGRESS.md`, `.flow_walker_state
|
|
71
|
-
# `autonomous_logs/remote_superseded
|
|
72
|
-
#
|
|
70
|
+
# `PAUSE_PROGRESS.md`, `.flow_walker_state`, the move-aside directory
|
|
71
|
+
# `autonomous_logs/remote_superseded/` and, only where nothing exists yet,
|
|
72
|
+
# files under the eight planning paths `hr_remote_planning_paths` assigns;
|
|
73
|
+
# outside it, only the caller-named `<out_dir>` (its `planning/` included)
|
|
74
|
+
# of `hr_remote_bundle_write` and the caller-named `<out_json>`
|
|
73
75
|
# of `hr_remote_status_write`. Written only by `hr_remote_status_write`,
|
|
74
76
|
# `hr_remote_bundle_write` and `hr_remote_bundle_restore`, and nothing
|
|
75
77
|
# there but a writer's own failed temp file is ever removed.
|
|
@@ -183,7 +185,9 @@
|
|
|
183
185
|
# `HR_LANE_RANK`, `HR_LANE_STATE`, `HR_LANE_RESUME_AT`, `HR_LANE_OBSERVED_AT`,
|
|
184
186
|
# `HR_LANE_OBSERVED_REPO`, `HR_LANE_OWNER_SLUG`, `HR_LANE_OWNER_PID`,
|
|
185
187
|
# `HR_LANE_OWNER_AT` and `HR_LANE_BROKEN_OWNER`, and the remote state bundle's
|
|
186
|
-
# names, which `hr_remote_names_var` assigns
|
|
188
|
+
# names, which `hr_remote_names_var` assigns, with `HR_REMOTE_PLANNING_PATHS`
|
|
189
|
+
# (`hr_remote_planning_paths`) and `HR_REMOTE_PLANNING_PLACED` /
|
|
190
|
+
# `HR_REMOTE_PLANNING_KEPT` (`hr_remote_bundle_restore`). Every one of them is assigned
|
|
187
191
|
# before it is read by the function that owns it, so an inherited value from a
|
|
188
192
|
# parent process is overwritten rather than believed.
|
|
189
193
|
#
|
|
@@ -1327,18 +1331,43 @@ hr_registry_branches() {
|
|
|
1327
1331
|
# <bundle>/PAUSE_PROGRESS.md when present
|
|
1328
1332
|
# <bundle>/flow_walker_state <state_dir>/.flow_walker_state, WITHOUT its dot
|
|
1329
1333
|
# <bundle>/run.log <state_dir>/autonomous_logs/<branch>.log; never restored
|
|
1334
|
+
# <bundle>/planning/<path> each of these under <state_dir>, when present:
|
|
1335
|
+
# story_plans/<branch>_story_plan.md
|
|
1336
|
+
# task_plans/<branch>
|
|
1337
|
+
# ui_test_plans/<branch>_ui_test_plan.md
|
|
1338
|
+
# ui_test_plans/<branch>
|
|
1339
|
+
# task_plan_reviews/<branch>
|
|
1340
|
+
# business_parity_reviews/<branch>
|
|
1341
|
+
# architecture_reviews/<branch>
|
|
1342
|
+
# ui_test_plan_reviews/<branch>
|
|
1330
1343
|
#
|
|
1331
1344
|
# 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
|
|
1345
|
+
# files by default. THE BUNDLE ITSELF IS NEVER COMMITTED. Every file outside
|
|
1346
|
+
# `planning/` is gitignored machine-local state, and the remote-status and
|
|
1347
|
+
# move-aside paths sit under `autonomous_logs/`, whose ignore rule already
|
|
1348
|
+
# covers them. The files under `planning/` are untracked drafts that the flow
|
|
1349
|
+
# commits itself at its P1/P3 convergence; the bundle only carries them.
|
|
1350
|
+
#
|
|
1351
|
+
# THE PLANNING PATHS ARE A MIRROR of the contracts that write and stage them:
|
|
1352
|
+
# `plugin/instructions/task_plan_writing_instructions_autonomous.md` →
|
|
1353
|
+
# `## Override 3` and `## Override 4` staging lists, and
|
|
1354
|
+
# `<scripts_dir>/flows/task_plan_writing.graph.json` → each node's `findingsFolder`. A path
|
|
1355
|
+
# added there is an edit to `hr_remote_planning_paths`. Only planning is
|
|
1356
|
+
# carried because the walker's one graph is `task_plan_writing.graph.json`;
|
|
1357
|
+
# implementation-phase per-unit review folders never span a pause, which waits
|
|
1358
|
+
# for a clean tracked tree. `HR_REMOTE_STATE_SCHEMA` stays `'1'`: `planning/`
|
|
1359
|
+
# is additive, an older reader ignores it, and a newer reader of an older
|
|
1360
|
+
# bundle finds none.
|
|
1335
1361
|
#
|
|
1336
1362
|
# WHO READS EACH FILE. `status.json`: `remote-run.sh sync` / `status` (into the
|
|
1337
1363
|
# local registry), `continue` / `poll` (the decision, `chain`, the reset time)
|
|
1338
1364
|
# and the next job's seed. The clarification directory and `PAUSE_PROGRESS.md`:
|
|
1339
1365
|
# the next job, and the user's local mirror. The walker state: the next job
|
|
1340
|
-
# only. `
|
|
1341
|
-
#
|
|
1366
|
+
# only. `planning/`: the next job only, never a mirror — an untracked draft left
|
|
1367
|
+
# in the mirror would make its later fast-forward to `origin/<branch>` refuse,
|
|
1368
|
+
# because the draft's own convergence commit adds the same path. `run.log`: the
|
|
1369
|
+
# user only — `sync` copies it to the main checkout's logs directory itself,
|
|
1370
|
+
# and no restore places it.
|
|
1342
1371
|
#
|
|
1343
1372
|
# `status.json` — schema `HR_REMOTE_STATE_SCHEMA`; every value a JSON string:
|
|
1344
1373
|
# schema a reader that does not recognise it treats the bundle as absent
|
|
@@ -1372,6 +1401,27 @@ hr_remote_names_var() {
|
|
|
1372
1401
|
HR_REMOTE_LOGS_DIR='autonomous_logs'
|
|
1373
1402
|
HR_REMOTE_STATUS_SOURCE="$HR_REMOTE_LOGS_DIR/remote_status.json"
|
|
1374
1403
|
HR_REMOTE_SUPERSEDED_DIR="$HR_REMOTE_LOGS_DIR/remote_superseded"
|
|
1404
|
+
HR_REMOTE_PLANNING_DIR='planning'
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
# hr_remote_planning_paths <branch>
|
|
1408
|
+
#
|
|
1409
|
+
# Assigns `HR_REMOTE_PLANNING_PATHS`: the eight planning paths of the format
|
|
1410
|
+
# above, relative to <state_dir>, newline-separated, no trailing slash. 1 with
|
|
1411
|
+
# an empty value for an empty <branch>.
|
|
1412
|
+
hr_remote_planning_paths() {
|
|
1413
|
+
local branch="${1-}"
|
|
1414
|
+
HR_REMOTE_PLANNING_PATHS=''
|
|
1415
|
+
[ -n "$branch" ] || return 1
|
|
1416
|
+
HR_REMOTE_PLANNING_PATHS="story_plans/${branch}_story_plan.md
|
|
1417
|
+
task_plans/$branch
|
|
1418
|
+
ui_test_plans/${branch}_ui_test_plan.md
|
|
1419
|
+
ui_test_plans/$branch
|
|
1420
|
+
task_plan_reviews/$branch
|
|
1421
|
+
business_parity_reviews/$branch
|
|
1422
|
+
architecture_reviews/$branch
|
|
1423
|
+
ui_test_plan_reviews/$branch"
|
|
1424
|
+
return 0
|
|
1375
1425
|
}
|
|
1376
1426
|
|
|
1377
1427
|
# hr_remote_status_write <registry_file> <branch> <out_json> <decision> <detail>
|
|
@@ -1475,10 +1525,12 @@ hr_remote_status_get() {
|
|
|
1475
1525
|
# <root>'s configured state directory. `status.json` is the job's own
|
|
1476
1526
|
# `autonomous_logs/remote_status.json` when present; otherwise it is written
|
|
1477
1527
|
# from <branch>'s registry record with decision `stop`, because a job that never
|
|
1478
|
-
# wrote its status never decided to continue.
|
|
1528
|
+
# wrote its status never decided to continue. Each planning path present is
|
|
1529
|
+
# copied under `planning/`, whether or not the branch tracks it: the restore
|
|
1530
|
+
# never overwrites, so a tracked copy is inert. 0 written; 1 a missing argument,
|
|
1479
1531
|
# a non-empty <out_dir> or a failed copy; 2 <root>'s configuration unresolvable.
|
|
1480
1532
|
hr_remote_bundle_write() {
|
|
1481
|
-
local root="${1-}" branch="${2-}" registry="${3-}" out="${4-}" state base clarify
|
|
1533
|
+
local root="${1-}" branch="${2-}" registry="${3-}" out="${4-}" state base clarify rel dst
|
|
1482
1534
|
[ -n "$root" ] && [ -n "$branch" ] && [ -n "$registry" ] && [ -n "$out" ] || return 1
|
|
1483
1535
|
state=$(hr_state_dir "$root") || return 2
|
|
1484
1536
|
hr_remote_names_var
|
|
@@ -1511,15 +1563,36 @@ hr_remote_bundle_write() {
|
|
|
1511
1563
|
if [ -f "$base/$HR_REMOTE_LOGS_DIR/$branch.log" ]; then
|
|
1512
1564
|
cp "$base/$HR_REMOTE_LOGS_DIR/$branch.log" "$out/$HR_REMOTE_LOG_FILE" 2>/dev/null || return 1
|
|
1513
1565
|
fi
|
|
1566
|
+
hr_remote_planning_paths "$branch" || return 1
|
|
1567
|
+
while IFS= read -r rel; do
|
|
1568
|
+
dst="$out/$HR_REMOTE_PLANNING_DIR/$rel"
|
|
1569
|
+
if [ -f "$base/$rel" ]; then
|
|
1570
|
+
mkdir -p "${dst%/*}" 2>/dev/null || return 1
|
|
1571
|
+
cp "$base/$rel" "$dst" 2>/dev/null || return 1
|
|
1572
|
+
elif [ -d "$base/$rel" ]; then
|
|
1573
|
+
mkdir -p "${dst%/*}" 2>/dev/null || return 1
|
|
1574
|
+
cp -R "$base/$rel" "$dst" 2>/dev/null || return 1
|
|
1575
|
+
fi
|
|
1576
|
+
done <<EOF
|
|
1577
|
+
$HR_REMOTE_PLANNING_PATHS
|
|
1578
|
+
EOF
|
|
1514
1579
|
return 0
|
|
1515
1580
|
}
|
|
1516
1581
|
|
|
1517
1582
|
# hr_remote_bundle_restore <bundle_dir> <root> <branch> <mode>
|
|
1518
1583
|
#
|
|
1519
1584
|
# <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.
|
|
1585
|
+
# walker state (back under its dotted name), the planning drafts and
|
|
1586
|
+
# `status.json` (as `autonomous_logs/remote_status.json`); `mirror` places the
|
|
1587
|
+
# first two only. The run log is placed by neither.
|
|
1588
|
+
#
|
|
1589
|
+
# A PLANNING DRAFT NEVER OVERWRITES. A regular file under `planning/` whose
|
|
1590
|
+
# path lies in `HR_REMOTE_PLANNING_PATHS` and has no `..` segment is placed only
|
|
1591
|
+
# where nothing exists, counted in `HR_REMOTE_PLANNING_PLACED`; one whose target
|
|
1592
|
+
# exists is left byte-identical — the checkout's copy is the branch's committed
|
|
1593
|
+
# record — and counted in `HR_REMOTE_PLANNING_KEPT`. A symlink, a non-regular
|
|
1594
|
+
# entry or a path outside the set counts in neither. Both are `0` at entry and
|
|
1595
|
+
# stay `0` in `mirror` mode.
|
|
1523
1596
|
#
|
|
1524
1597
|
# THE CLARIFICATION DIRECTORY IS REPLACED WHOLESALE, AND NOTHING IS DELETED. An
|
|
1525
1598
|
# existing target is moved aside with one `mv` into
|
|
@@ -1535,7 +1608,9 @@ hr_remote_bundle_write() {
|
|
|
1535
1608
|
# <branch>) or <root>'s configuration is unresolvable.
|
|
1536
1609
|
hr_remote_bundle_restore() {
|
|
1537
1610
|
local bundle="${1-}" root="${2-}" branch="${3-}" mode="${4-}"
|
|
1538
|
-
local state base named target epoch aside n tmp
|
|
1611
|
+
local state base named target epoch aside n tmp pdir file rel p inset
|
|
1612
|
+
HR_REMOTE_PLANNING_PLACED=0
|
|
1613
|
+
HR_REMOTE_PLANNING_KEPT=0
|
|
1539
1614
|
[ -n "$bundle" ] && [ -n "$root" ] && [ -n "$branch" ] || return 1
|
|
1540
1615
|
case "$mode" in
|
|
1541
1616
|
job|mirror) ;;
|
|
@@ -1575,6 +1650,37 @@ hr_remote_bundle_restore() {
|
|
|
1575
1650
|
mkdir -p "$base" 2>/dev/null || return 1
|
|
1576
1651
|
cp "$bundle/$HR_REMOTE_WALKER_FILE" "$base/$HR_REMOTE_WALKER_SOURCE" 2>/dev/null || return 1
|
|
1577
1652
|
fi
|
|
1653
|
+
pdir="$bundle/$HR_REMOTE_PLANNING_DIR"
|
|
1654
|
+
if [ -d "$pdir" ] && [ ! -L "$pdir" ]; then
|
|
1655
|
+
hr_remote_planning_paths "$branch" || return 1
|
|
1656
|
+
# Process substitution, not a pipe: the loop must run in this shell so the
|
|
1657
|
+
# counters and `return 1` reach the caller.
|
|
1658
|
+
while IFS= read -r file; do
|
|
1659
|
+
# Re-tested per line: a name holding a newline arrives split and fails here.
|
|
1660
|
+
[ -f "$file" ] && [ ! -L "$file" ] || continue
|
|
1661
|
+
rel=${file#"$pdir"/}
|
|
1662
|
+
case "/$rel/" in
|
|
1663
|
+
*/../*) continue ;;
|
|
1664
|
+
esac
|
|
1665
|
+
inset=0
|
|
1666
|
+
while IFS= read -r p; do
|
|
1667
|
+
case "$rel" in
|
|
1668
|
+
"$p"|"$p"/*) inset=1; break ;;
|
|
1669
|
+
esac
|
|
1670
|
+
done <<EOF
|
|
1671
|
+
$HR_REMOTE_PLANNING_PATHS
|
|
1672
|
+
EOF
|
|
1673
|
+
[ "$inset" = 1 ] || continue
|
|
1674
|
+
if [ -e "$base/$rel" ] || [ -L "$base/$rel" ]; then
|
|
1675
|
+
HR_REMOTE_PLANNING_KEPT=$((HR_REMOTE_PLANNING_KEPT + 1))
|
|
1676
|
+
continue
|
|
1677
|
+
fi
|
|
1678
|
+
target="$base/$rel"
|
|
1679
|
+
mkdir -p "${target%/*}" 2>/dev/null || return 1
|
|
1680
|
+
cp "$file" "$target" 2>/dev/null || return 1
|
|
1681
|
+
HR_REMOTE_PLANNING_PLACED=$((HR_REMOTE_PLANNING_PLACED + 1))
|
|
1682
|
+
done < <(find "$pdir" -type f 2>/dev/null)
|
|
1683
|
+
fi
|
|
1578
1684
|
mkdir -p "$base/$HR_REMOTE_LOGS_DIR" 2>/dev/null || return 1
|
|
1579
1685
|
tmp=$(mktemp "$base/$HR_REMOTE_STATUS_SOURCE.tmp.XXXXXX" 2>/dev/null) || return 1
|
|
1580
1686
|
if cp "$bundle/$HR_REMOTE_STATUS_FILE" "$tmp" 2>/dev/null \
|
|
@@ -71,11 +71,15 @@
|
|
|
71
71
|
# past a run with none, and stopping at one whose artifact has expired, since
|
|
72
72
|
# an older copy would be staler state. An expired one restores nothing: under
|
|
73
73
|
# --resume answer it exits 2; otherwise it prints a `::warning::` line naming
|
|
74
|
-
# the run, the expiry and the lost counts
|
|
75
|
-
# job continues from the committed ledger. An unexpired one it downloads to `<state_dir>/autonomous_logs/remote_download/<branch>/<id>/`
|
|
74
|
+
# the run, the expiry and the lost counts, clarification history and
|
|
75
|
+
# uncommitted planning drafts, and the job continues from the committed ledger. An unexpired one it downloads to `<state_dir>/autonomous_logs/remote_download/<branch>/<id>/`
|
|
76
76
|
# (skipped when that directory already holds its status.json); and restores it
|
|
77
77
|
# in `job` mode — on every --resume kind, `none` included, because a reused
|
|
78
|
-
# branch keeps its clarification history.
|
|
78
|
+
# branch keeps its clarification history. A job-mode restore also places the
|
|
79
|
+
# bundle's `planning/` drafts at their paths under the state directory, never
|
|
80
|
+
# over a file the checkout already has, and when it placed or kept any prints
|
|
81
|
+
# `placed <n> planning file(s) for <branch>; kept <n> the checkout already
|
|
82
|
+
# carries`. Then, under --resume answer, it
|
|
79
83
|
# writes each `"<n>": "<text>"` entry to `clarifications/<branch>/answer_<n>.md`
|
|
80
84
|
# with the exact bytes, after checking every entry first; and with
|
|
81
85
|
# `HARNESS_INPUT_PARK_LOOP_CLEAR` exactly `true` it sets `park_loop_cycles` to
|
|
@@ -230,7 +234,8 @@
|
|
|
230
234
|
# `mirror` mode into the record's `worktree`, `run.log` copied to the main
|
|
231
235
|
# checkout's `autonomous_logs/<branch>.remote.log`, and `status`,
|
|
232
236
|
# `pause_reason`, `usage_resume_at`, `park_loop_cycles`, `remote_run_id`,
|
|
233
|
-
# `remote_run_url`, `remote_detail` and `remote_synced_at` written
|
|
237
|
+
# `remote_run_url`, `remote_detail` and `remote_synced_at` written. A
|
|
238
|
+
# `mirror` restore places no planning draft
|
|
234
239
|
# 4. no artifact, while some bundle exists (`remote_run_id` is set, or an
|
|
235
240
|
# older finished run carries one): a job that died before its upload.
|
|
236
241
|
# `paused` / `killed`, `remote_run_id` / `remote_run_url` re-pointed at
|
|
@@ -294,7 +299,8 @@
|
|
|
294
299
|
# `<state_dir>/autonomous_logs/remote_download/<branch>/<id>/` and
|
|
295
300
|
# `<branch>.remote.log` in the main checkout, plus the mirror restore
|
|
296
301
|
# `hr_remote_bundle_restore` performs in the record's `worktree`; for
|
|
297
|
-
# `restore`, that download directory, the job restore
|
|
302
|
+
# `restore`, that download directory, the job restore (the planning drafts
|
|
303
|
+
# among it), `answer_<n>.md` and the
|
|
298
304
|
# `park_loop_cycles` rewrite of `remote_status.json`, all in the job's
|
|
299
305
|
# checkout; for `save`, <out_dir> and the step summary; for `poll`, its
|
|
300
306
|
# download directories and `<state_dir>/autonomous_logs/poll_state/previous/`
|
|
@@ -363,7 +369,8 @@
|
|
|
363
369
|
# flow_walker_state) and GITHUB_RUN_ID set to another id:
|
|
364
370
|
# restore bash scripts/remote-run.sh restore feat_x --resume none -> 0; the
|
|
365
371
|
# checkout carries clarifications/feat_x/question_1.md,
|
|
366
|
-
# .flow_walker_state and autonomous_logs/remote_status.json
|
|
372
|
+
# .flow_walker_state and autonomous_logs/remote_status.json, and
|
|
373
|
+
# places the bundle's planning/ drafts where the checkout has none
|
|
367
374
|
# answer HARNESS_INPUT_ANSWERS='{"1":"Use B.\n"}' ... --resume answer -> 0;
|
|
368
375
|
# clarifications/feat_x/answer_1.md holds exactly `Use B.` + newline
|
|
369
376
|
# no question HARNESS_INPUT_ANSWERS='{"2":"x"}' ... --resume answer
|
|
@@ -381,7 +388,7 @@
|
|
|
381
388
|
# -> prints the expired line, "$r" byte-identical
|
|
382
389
|
# save bash scripts/remote-run.sh save feat_x /tmp/b -> 0; /tmp/b holds
|
|
383
390
|
# status.json, clarifications/feat_x/, flow_walker_state (and
|
|
384
|
-
# PAUSE_PROGRESS.md, run.log when present); with
|
|
391
|
+
# PAUSE_PROGRESS.md, run.log, planning/ when present); with
|
|
385
392
|
# GITHUB_STEP_SUMMARY=/tmp/s, /tmp/s gains the status table
|
|
386
393
|
# never started no remote_status.json and no registry: save -> 0, /tmp/b
|
|
387
394
|
# empty
|
|
@@ -1140,7 +1147,7 @@ verb_restore() {
|
|
|
1140
1147
|
if [ "$PREV_RUN_STATE" = expired ]; then
|
|
1141
1148
|
[ "$resume" != answer ] \
|
|
1142
1149
|
|| restore_refuse "the state bundle of run $id expired on $BUNDLE_EXPIRES_AT, so its questions can no longer be answered here: resume from the committed ledger with $RESUME_HINT $branch, or re-drop the task; nothing written"
|
|
1143
|
-
echo "::warning::remote-run.sh: the state bundle of run $id expired on $BUNDLE_EXPIRES_AT: the park-loop, auto-resume and stall counts
|
|
1150
|
+
echo "::warning::remote-run.sh: the state bundle of run $id expired on $BUNDLE_EXPIRES_AT: the park-loop, auto-resume and stall counts, the clarification history and any planning drafts not yet committed that it carried are lost; this job continues from the committed ledger"
|
|
1144
1151
|
elif [ -z "$id" ]; then
|
|
1145
1152
|
[ "$resume" != answer ] \
|
|
1146
1153
|
|| restore_refuse "--resume answer, but no finished run of $branch carries a state bundle; nothing written"
|
|
@@ -1163,7 +1170,11 @@ verb_restore() {
|
|
|
1163
1170
|
fi
|
|
1164
1171
|
hr_remote_bundle_restore "$download" "$root" "$branch" job
|
|
1165
1172
|
case $? in
|
|
1166
|
-
0)
|
|
1173
|
+
0)
|
|
1174
|
+
echo "remote-run.sh: restored the bundle of run $id into $root"
|
|
1175
|
+
[ $((HR_REMOTE_PLANNING_PLACED + HR_REMOTE_PLANNING_KEPT)) -eq 0 ] \
|
|
1176
|
+
|| echo "remote-run.sh: placed $HR_REMOTE_PLANNING_PLACED planning file(s) for $branch; kept $HR_REMOTE_PLANNING_KEPT the checkout already carries"
|
|
1177
|
+
;;
|
|
1167
1178
|
2) restore_refuse "the bundle in '$download' is unrecognised for $branch; nothing restored" ;;
|
|
1168
1179
|
*) restore_fail "restoring '$download' into '$root' failed" ;;
|
|
1169
1180
|
esac
|