autonomous-sdlc-harness 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
File without changes
@@ -53,8 +53,11 @@ export const WORKFLOW_CONTROL_FILE = 'harness-control.yml';
53
53
  export const WORKFLOW_CONTROL_PATH = `${WORKFLOWS_DIR}/${WORKFLOW_CONTROL_FILE}`;
54
54
  /** The first word of a command comment, matched case-insensitively. */
55
55
  export const COMMAND_HANDLE = '@sdlc-harness';
56
- /** The verbs a command comment may carry after {@link COMMAND_HANDLE}. */
57
- export const COMMAND_VERBS = ['answer', 'pause', 'resume', 'stop', 'clear'];
56
+ /**
57
+ * The verbs a command comment may carry after {@link COMMAND_HANDLE}. `status` is read-only: it
58
+ * replies with the run's state and changes no label and dispatches nothing.
59
+ */
60
+ export const COMMAND_VERBS = ['answer', 'pause', 'resume', 'stop', 'clear', 'status'];
58
61
  /**
59
62
  * The prefix of the hidden line every harness comment carries; the whole line is
60
63
  * `<!-- sdlc-harness event=<event> branch=<branch>[ question=<n>] -->`. A comment containing the
@@ -1 +1 @@
1
- {"version":3,"file":"githubActions.js","sourceRoot":"","sources":["../../src/remote/githubActions.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"githubActions.js","sourceRoot":"","sources":["../../src/remote/githubActions.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autonomous-sdlc-harness",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
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",
@@ -1,7 +1,8 @@
1
1
  # harness-control.yml — acts on an autonomous-sdlc-harness command comment
2
- # (`@sdlc-harness answer|pause|resume|stop|clear`) on an issue or pull request,
3
- # and turns a pull-request review requesting changes into the next user-review
4
- # round.
2
+ # (`@sdlc-harness answer|pause|resume|stop|clear|status`) on an issue or pull
3
+ # request, turns a pull-request review requesting changes into the next
4
+ # user-review round, and stops a run when its issue or pull request is closed
5
+ # or its branch is deleted.
5
6
  #
6
7
  # WHO WRITES IT. `autonomous-sdlc-harness init`, only when harness.config.json's
7
8
  # `forge` is `github` and its `execution.target` is `github-actions`. Written
@@ -26,16 +27,29 @@
26
27
  # pull_request_review, type `submitted` only: the round is built from the
27
28
  # review event, which carries the review body and identifies its inline
28
29
  # comments (docs/github-integration-research.md, C1).
30
+ # issues, type `closed` only: a run's issue was closed, which stops the run.
31
+ # pull_request, type `closed` only: a run's pull request was closed or
32
+ # merged, which stops the run. GitHub starts no workflow for activity on
33
+ # a pull request with a merge conflict, so closing a conflicting one
34
+ # stops nothing (docs/github-run-control.md, section 8).
35
+ # delete: a branch or tag was deleted; a deleted run branch stops the run.
29
36
  # Never pull_request_review_comment: one review raises one such event per
30
37
  # inline comment, so listening to it would start one job per comment for a
31
38
  # round the review event already covers (C1).
32
39
  #
33
40
  # THE PREFILTER. The job's `if:` runs it only for a comment that contains
34
41
  # `@sdlc-harness` and not `<!-- sdlc-harness` (the hidden line every harness
35
- # reply carries, so a reply quoting a command is never one), or for a review
36
- # whose state is `changes_requested` on a pull request from this repository.
37
- # Every other comment or review yields a skipped job, which runs no runner and
38
- # bills nothing (docs/remote-execution.md, `### Verified in Gate 12 round 2`).
42
+ # reply carries, so a reply quoting a command is never one); for a review
43
+ # whose state is `changes_requested` on a pull request from this repository;
44
+ # for a closed issue carrying a `sdlc-harness: ` state label, which every
45
+ # harness issue carries from its trigger on; for a closed pull request from
46
+ # this repository; or for a deleted ref whose type is `branch`. Every other
47
+ # event yields a skipped job, which runs no runner and bills nothing
48
+ # (docs/remote-execution.md, `### Verified in Gate 12 round 2`). No label or
49
+ # name marks a run branch at deletion, and none reliably marks a run's pull
50
+ # request before its first lifecycle event, so every branch deletion and
51
+ # every closed same-repository pull request in the repository starts a job,
52
+ # which `control` ends in one line when the branch had no run.
39
53
  # It is a saving, not the authority: `control` re-checks every rule, so an
40
54
  # edited copy of this filter cannot bypass one. That `contains()` compares
41
55
  # case-insensitively is GitHub's documented behaviour, not verified here; a
@@ -43,7 +57,8 @@
43
57
  #
44
58
  # THE PERMISSIONS, declared because the default token may be read-only
45
59
  # (docs/github-integration-research.md, T2):
46
- # contents: write push a review round
60
+ # contents: write push a review round, and the contents read a
61
+ # `stop --branch-gone` makes
47
62
  # actions: write dispatch, pause, stop and cancel runs, and read
48
63
  # runs and their artifacts
49
64
  # issues: write reply on the issue or pull request, and label it
@@ -61,6 +76,8 @@
61
76
  # not skipped, and can name any `runs-on:` label, a self-hosted runner's
62
77
  # included. Such a job gets a read-only token and no secret (C2), and fork
63
78
  # approval policies apply to it.
79
+ # A `pull_request` close job runs the pull request's merge-commit copy of this
80
+ # file too, as a review job does (C2); the `if:` skips a fork's.
64
81
  # A comment on a fork's pull request runs with a write token, and `control`
65
82
  # refuses it.
66
83
  # The checkout is always the default branch, never the pull request's merge
@@ -71,7 +88,8 @@
71
88
  # THE CONCURRENCY GROUP SERIALIZES REVIEW JOBS PER BRANCH, NEVER COMMENT JOBS.
72
89
  # * A comment job's group is its own run id, so it never waits and is never
73
90
  # replaced: a group keeps at most one pending job and cancels an earlier
74
- # pending one, which would drop a command.
91
+ # pending one, which would drop a command. A close or deletion job takes
92
+ # the same run-id group, for the same reason.
75
93
  # * Review jobs on one head branch share `harness-review-<branch>` and run one
76
94
  # at a time, so two reviews submitted together give one round, not two
77
95
  # pushes racing for the same round number.
@@ -86,6 +104,13 @@
86
104
  # harness-run.yml's `collect` job is the one such job today (DECLARED
87
105
  # MIRRORS below).
88
106
  #
107
+ # A REFUSAL IS A SUCCESS. `remote-run.sh control` exits 2 for a refusal it has
108
+ # already replied to; that exit code is the script's contract with every other
109
+ # caller, so the step, not the script, maps it: `|| [ $? -eq 2 ]` turns a
110
+ # replied refusal into a green job with no failure e-mail, while 1, 3 and 4
111
+ # still fail it. Under the step's `bash -e -o pipefail` a failure on the left
112
+ # of `||` does not exit the shell, and `$?` is the script's status.
113
+ #
89
114
 
90
115
  # TWO RULES EVERY EDIT KEEPS.
91
116
  # * Event text and variables reach a shell line only through `env:` and the
@@ -121,13 +146,18 @@
121
146
  # Pin a sha in your copy if you want one.
122
147
 
123
148
  name: harness-control
124
- run-name: harness control ${{ github.event.issue.number || github.event.pull_request.number }}
149
+ run-name: harness control ${{ github.event.issue.number || github.event.pull_request.number || github.event.ref }}
125
150
 
126
151
  on:
127
152
  issue_comment:
128
153
  types: [created]
129
154
  pull_request_review:
130
155
  types: [submitted]
156
+ issues:
157
+ types: [closed]
158
+ pull_request:
159
+ types: [closed]
160
+ delete:
131
161
 
132
162
  permissions:
133
163
  contents: write
@@ -141,7 +171,7 @@ defaults:
141
171
 
142
172
  jobs:
143
173
  control:
144
- if: (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@sdlc-harness') && !contains(github.event.comment.body, '<!-- sdlc-harness')) || (github.event_name == 'pull_request_review' && github.event.review.state == 'changes_requested' && github.event.pull_request.head.repo.full_name == github.repository)
174
+ if: (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@sdlc-harness') && !contains(github.event.comment.body, '<!-- sdlc-harness')) || (github.event_name == 'pull_request_review' && github.event.review.state == 'changes_requested' && github.event.pull_request.head.repo.full_name == github.repository) || (github.event_name == 'issues' && contains(join(github.event.issue.labels.*.name, ','), 'sdlc-harness: ')) || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || (github.event_name == 'delete' && github.event.ref_type == 'branch')
145
175
  runs-on: ${{ vars.HARNESS_RUNNER || 'ubuntu-latest' }}
146
176
  concurrency:
147
177
  group: ${{ github.event_name == 'pull_request_review' && format('harness-review-{0}', github.event.pull_request.head.ref) || format('harness-control-{0}', github.run_id) }}
@@ -180,5 +210,5 @@ jobs:
180
210
  git config user.name "github-actions[bot]"
181
211
  git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
182
212
 
183
- - name: Act on the comment or review
184
- run: bash "$SCRIPTS_DIR/remote-run.sh" control
213
+ - name: Act on the comment, review, close or deletion
214
+ run: bash "$SCRIPTS_DIR/remote-run.sh" control || [ $? -eq 2 ]
@@ -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)
@@ -69,6 +70,18 @@
69
70
  # report step's reason below: scripts older than this file carry no `collect`
70
71
  # verb.
71
72
  #
73
+ # THE REF CHECK. Finding 6 of Gate 12 round 6: GitHub lists a run under the ref
74
+ # it was dispatched from (Use workflow from), so a run or pause dispatched from
75
+ # a ref other than its `branch` input is invisible to every lookup by that
76
+ # branch. When `action` is `run` or `pause` and `github.ref_name` is not
77
+ # `inputs.branch`, the `wrong-ref` job fails, naming the ref to use, and `run`
78
+ # and `collect` are skipped; the jobless `pause` marker gains that failing job
79
+ # only when its ref is wrong. `warm` is exempt because it dispatches on the
80
+ # default branch with that branch as its input, so the two already agree.
81
+ # `stop` is exempt because a deleted branch's stop marker is sent from the
82
+ # default branch, its own ref being gone, and the stop check matches the
83
+ # `harness stop <branch>` title across every branch's runs, under any ref.
84
+ #
72
85
  # ACTION PINS.
73
86
  # actions/checkout@v5
74
87
  # actions/setup-node@v5
@@ -234,8 +247,20 @@ defaults:
234
247
  shell: bash
235
248
 
236
249
  jobs:
250
+ wrong-ref:
251
+ if: (inputs.action == 'run' || inputs.action == 'pause') && github.ref_name != inputs.branch
252
+ runs-on: ${{ vars.HARNESS_RUNNER || 'ubuntu-latest' }}
253
+ steps:
254
+ - name: Refuse a dispatch from another ref
255
+ env:
256
+ IN_REF: ${{ github.ref_name }}
257
+ IN_BRANCH: ${{ inputs.branch }}
258
+ run: |
259
+ 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'."
260
+ exit 1
261
+
237
262
  run:
238
- if: inputs.action == 'run'
263
+ if: inputs.action == 'run' && github.ref_name == inputs.branch
239
264
  runs-on: ${{ vars.HARNESS_RUNNER || 'ubuntu-latest' }}
240
265
  # The self-hosted job limit, 5 days; a hosted job stops at its own limit.
241
266
  timeout-minutes: 7200
@@ -486,7 +511,7 @@ jobs:
486
511
 
487
512
  collect:
488
513
  needs: run
489
- if: ${{ inputs.action == 'run' && !cancelled() }}
514
+ if: ${{ inputs.action == 'run' && !cancelled() && github.ref_name == inputs.branch }}
490
515
  runs-on: ${{ vars.HARNESS_RUNNER || 'ubuntu-latest' }}
491
516
  concurrency:
492
517
  group: harness-review-${{ inputs.branch }}
@@ -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 resume_parked_run
1209
- # and archived then cleared by classify_run_exit, which is
1210
- # the whole of its lifetime — so A NON-EMPTY VALUE ON A
1211
- # `completed` RECORD IS A DEFECT: it means the pairs it
1212
- # names are still sitting unarchived at the top level,
1213
- # where they trigger a resume of a run that already read
1214
- # them. It is NOT a defect on a `paused` record: the pause
1215
- # branch returns before the archival and leaves it set on
1216
- # purpose, because a pause mid park-resume left those
1217
- # answers unconsumed. The pause resume never writes this
1218
- # field — a pause is not an answer.
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
- # execution `github-actions` on a remote record — one
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. Exactly that set is archived only AFTER that resumed engine exits —
2172
- # here, keyed off the `resumed_for_index` list the resume recorded. That is what
2173
- # stops a pair the engine already read from triggering another resume, without
2174
- # emptying the paths the re-entering engine reads; a pair written mid-session is
2175
- # not in the list and stays.
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
- registry_set "$branch" pause_reason "$reason" status paused
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
- # If this exit followed a resume — and was NOT a pause, handled above — every
2243
- # answer in `resumed_for_index` has now been consumed by the re-launched
2244
- # engine. Archive exactly that set before classifying, so none of it is ever
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
- # field is still cleared, because leaving it set would make the next exit
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`. Written only by `hr_remote_status_write`,
83
- # `hr_remote_bundle_write` and `hr_remote_bundle_restore`, and nothing
84
- # there but a writer's own failed temp file is ever removed.
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`). Every one of them is assigned
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 with one `mv` into
2199
- # `autonomous_logs/remote_superseded/<epoch>/clarifications/<branch>` (`<epoch>-<n>`
2200
- # when an earlier restore took that second) before the
2201
- # bundle's copy goes in, so a stale local pair cannot survive and no recursive
2202
- # removal is ever shelled out. A bundle carrying no clarification directory
2203
- # leaves the target alone: in a mirror it may hold an answer not yet relayed.
2204
- #
2205
- # 0 restored; 1 a missing argument, an unknown <mode> or a failed copy; 2 —
2206
- # touching nothing — when the bundle is unrecognised (no readable `status.json`,
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 epoch aside n tmp pdir file rel p inset
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
- epoch=$(date +%s)
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