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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autonomous-sdlc-harness",
3
- "version": "0.4.1",
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
- # UNVERIFIED: whether GITHUB_TOKEN with `actions: write` may enable and disable
21
- # a workflow. If it may not, the pausing job's `paused` notification says
22
- # auto-resume is unavailable, and the run waits for
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@v4
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@v4
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. Whether a step's timeout-minutes
76
- # accepts an expression over `runner.environment` could not be checked against
77
- # GitHub's workflow-syntax documentation when this file was written (no network
78
- # access), so the first step computes every budget value from
79
- # `runner.environment` into GITHUB_ENV, and the harness step reads a single
80
- # `env` value. Gate 12 records the real behaviour.
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 run parked or paused longer
92
- # than its retention cannot be answered and loses those counts. 400 is the
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. The cap (rather than a
99
- # rejection) is carried from the action's `@actions/artifact` retention code
100
- # and could not be re-checked against its v4 README when this file was written
101
- # (no network access); Gate 12 records the real behaviour.
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@v4
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@v4
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@v4
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@v4
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@v4
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@v4
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@v4
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 are lost);
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` and the move-aside directory
71
- # `autonomous_logs/remote_superseded/`; outside it, only the caller-named
72
- # `<out_dir>` of `hr_remote_bundle_write` and the caller-named `<out_json>`
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. Every one of them is assigned
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. NOTHING IN THE BUNDLE IS EVER COMMITTED: every file in it is
1333
- # gitignored machine-local state, and the remote-status and move-aside paths sit
1334
- # under `autonomous_logs/`, whose ignore rule already covers them.
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. `run.log`: the user only — `sync` copies it to the main checkout's logs
1341
- # directory itself, and no restore places it.
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. 0 written; 1 a missing argument,
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) and `status.json` (as
1521
- # `autonomous_logs/remote_status.json`); `mirror` places the first two only. 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 and clarification history, and the
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. Then, under --resume answer, it
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, `answer_<n>.md` and the
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 and the clarification history it carried are lost; this job continues from the committed ledger"
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) echo "remote-run.sh: restored the bundle of run $id into $root" ;;
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