autonomous-sdlc-harness 0.4.2 → 0.6.0

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.
Files changed (41) hide show
  1. package/dist/commands/init.js +113 -16
  2. package/dist/commands/init.js.map +1 -1
  3. package/dist/config/check.js +28 -6
  4. package/dist/config/check.js.map +1 -1
  5. package/dist/config/model.js +64 -5
  6. package/dist/config/model.js.map +1 -1
  7. package/dist/core/pluginIdentity.js +2 -0
  8. package/dist/core/pluginIdentity.js.map +1 -1
  9. package/dist/core/writer.js +10 -5
  10. package/dist/core/writer.js.map +1 -1
  11. package/dist/core/yamlScalar.js +14 -0
  12. package/dist/core/yamlScalar.js.map +1 -0
  13. package/dist/doctor/checks.js +454 -23
  14. package/dist/doctor/checks.js.map +1 -1
  15. package/dist/generators/githubWorkflows.js +125 -20
  16. package/dist/generators/githubWorkflows.js.map +1 -1
  17. package/dist/generators/repoRoot.js +17 -8
  18. package/dist/generators/repoRoot.js.map +1 -1
  19. package/dist/remote/githubActions.js +110 -7
  20. package/dist/remote/githubActions.js.map +1 -1
  21. package/dist/retrieval/pythonBackend.js +114 -0
  22. package/dist/retrieval/pythonBackend.js.map +1 -0
  23. package/dist/retrieval/setup.js +8 -0
  24. package/dist/retrieval/setup.js.map +1 -1
  25. package/package.json +1 -1
  26. package/templates/README.md +1 -1
  27. package/templates/github/workflows/harness-control.yml +184 -0
  28. package/templates/github/workflows/harness-resume.yml +9 -0
  29. package/templates/github/workflows/harness-run.yml +127 -12
  30. package/templates/github/workflows/harness-trigger.yml +144 -0
  31. package/templates/repo/gitignore +5 -0
  32. package/templates/scripts/README.md +1 -1
  33. package/templates/scripts/autonomous-watcher.sh +121 -249
  34. package/templates/scripts/create-worktree.sh +52 -10
  35. package/templates/scripts/docs-search-server.sh +88 -17
  36. package/templates/scripts/lib/harness-run-lib.sh +614 -14
  37. package/templates/scripts/remote-run.sh +3684 -144
  38. package/templates/scripts/scratch-run.sh +54 -73
  39. package/templates/state-dir/README-root.md +1 -1
  40. package/templates/state-dir/scratch/README.md +4 -2
  41. package/templates/state-dir/user_reviews/README.md +2 -2
@@ -42,16 +42,14 @@
42
42
  #
43
43
  # THE REFUSAL IS THE WHOLE OF THE SAFETY — over WHICH FILE, and over nothing
44
44
  # else, per the paragraph above. This script executes what it is given, so the
45
- # only thing standing between it and an arbitrary FILE is the path test below: the argument is rejected outright if it carries `..` or any
46
- # character outside `A-Za-z0-9._/-`, and what is left must resolve strictly
47
- # inside `<repo_root>/<state_dir>/scratch/`. A relative argument resolves against
48
- # `<repo_root>`; an absolute one is compared as given. Both sides of the
49
- # comparison are resolved PHYSICALLY (`cd … && pwd -P`), so a symlinked directory
50
- # planted inside the scratch tree cannot name a target outside it, and a target
51
- # that is itself a symlink is refused rather than followed. The `..` test rejects
52
- # the two characters anywhere in the argument rather than only a whole segment —
53
- # strictly stronger, and free, because nothing in that directory depends on a
54
- # file's name.
45
+ # only thing standing between it and an arbitrary FILE is the path test: the
46
+ # argument must resolve strictly inside `<repo_root>/<state_dir>/scratch/`, and a
47
+ # `..`, a character outside `A-Za-z0-9._/-`, the scratch directory itself or a
48
+ # symlinked target is refused. A relative argument resolves against
49
+ # `<repo_root>`; an absolute one is compared as given. The test is
50
+ # `hr_scratch_path_var` in `lib/harness-run-lib.sh`, shared with
51
+ # `remote-run.sh discard`; the refusal reasons, and why each is refused, are the
52
+ # ones that function names.
55
53
  #
56
54
  # THE INTERPRETER COMES FROM THE FILE'S OWN EXTENSION. The table is below, and it
57
55
  # is the only place it is stated. NOT from the detected preset, on two grounds
@@ -123,7 +121,8 @@
123
121
  # 0-N the file ran; this is its own exit status, unmodified
124
122
  # 64 usage error — no file argument
125
123
  # 65 path refused — a `..`, a character outside `A-Za-z0-9._/-`, a symlink,
126
- # or a target that does not resolve inside <state_dir>/scratch/
124
+ # the scratch directory itself, or a target that does not resolve
125
+ # inside <state_dir>/scratch/
127
126
  # 66 the extension has no interpreter in the table below
128
127
  # 67 the file is not there, or is not a readable regular file
129
128
  # 68 the run-time context could not be established — the shared library is
@@ -146,6 +145,7 @@
146
145
  # outside scripts/scratch-run.sh harness.config.json -> exit 65
147
146
  # traversal scripts/scratch-run.sh sdlc-harness/scratch/../../harness.config.json
148
147
  # -> exit 65
148
+ # itself scripts/scratch-run.sh sdlc-harness/scratch/. -> exit 65
149
149
  # unknown ext mv probe.py probe.pl; same call -> exit 66
150
150
  # shell probe mv probe.py probe.sh; same call -> exit 66
151
151
  # absent file scripts/scratch-run.sh sdlc-harness/scratch/nope.py -> exit 67
@@ -173,11 +173,6 @@ set -uo pipefail
173
173
  # the table's supported extensions in the message.
174
174
  SCRATCH_INTERPRETERS='py:python3 js:node mjs:node cjs:node rb:ruby dart:dart php:php'
175
175
 
176
- # The one directory under `<state_dir>` a file may be run out of. Spelled once
177
- # here, mirroring the `scratch` row of `STATE_DIR_ENTRIES` in
178
- # `cli/src/generators/stateDir.ts`.
179
- SCRATCH_SUBDIR='scratch'
180
-
181
176
  # The library is reached by a path computed from this script's own location — no
182
177
  # session root and no runtime-substituted token is assumed.
183
178
  hr_lib="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/harness-run-lib.sh"
@@ -195,20 +190,6 @@ if [ "$#" -lt 1 ] || [ -z "${1-}" ]; then
195
190
  fi
196
191
  requested="$1"
197
192
 
198
- # --- The path tests, on the argument AS GIVEN, before anything is resolved.
199
- case "$requested" in
200
- *..*)
201
- echo "scratch-run.sh: '$requested' carries '..' — refusing, a scratch path names no parent" >&2
202
- exit 65
203
- ;;
204
- esac
205
- case "$requested" in
206
- *[!A-Za-z0-9._/-]*)
207
- echo "scratch-run.sh: '$requested' carries a character outside A-Za-z0-9._/- — refusing" >&2
208
- exit 65
209
- ;;
210
- esac
211
-
212
193
  # The repository is THIS FILE's own checkout, never the caller's directory: the
213
194
  # three forms the permission profile emits include a sibling worktree's own copy,
214
195
  # and each copy must answer for the checkout it belongs to whatever directory the
@@ -220,53 +201,53 @@ if [ -z "$repo_root" ]; then
220
201
  exit 68
221
202
  fi
222
203
 
223
- # `<repo_root>/<state_dir>/scratch`, from this repository's configuration at run
224
- # time. The library folds "not adopted" and "unreadable" into one non-zero
225
- # answer, and both are closed here: a configuration that cannot be read is a
226
- # scratch directory that cannot be located.
227
- scratch_dir="$(hr_state_path "$repo_root" "$SCRATCH_SUBDIR")"
228
- if [ -z "$scratch_dir" ]; then
229
- echo "scratch-run.sh: cannot resolve '$repo_root/harness.config.json' — refusing to run anything" >&2
230
- echo " (no configuration there, invalid JSON, more than one document, no defaultBranch, or jq missing/older than 1.5)" >&2
231
- exit 68
232
- fi
233
-
234
- scratch_real="$(cd "$scratch_dir" 2>/dev/null && pwd -P)"
235
- if [ -z "$scratch_real" ]; then
236
- echo "scratch-run.sh: '$scratch_dir' does not exist — re-run 'init' to materialize the state tree" >&2
237
- exit 68
238
- fi
239
-
240
- # A relative argument resolves against <repo_root>; an absolute one is taken as
241
- # given. Neither can carry `..` by the test above.
242
- case "$requested" in
243
- /*) candidate="$requested" ;;
244
- *) candidate="${repo_root%/}/$requested" ;;
245
- esac
246
-
247
- candidate_dir="$(cd "$(dirname "$candidate")" 2>/dev/null && pwd -P)"
248
- if [ -z "$candidate_dir" ]; then
249
- echo "scratch-run.sh: '$requested' names no existing directory under '$repo_root'" >&2
250
- exit 67
251
- fi
252
-
253
- # STRICTLY INSIDE: the scratch directory itself or a directory beneath it, on the
254
- # physical path of both sides, so a symlinked directory in the tree cannot widen
255
- # the fence.
256
- case "$candidate_dir" in
257
- "$scratch_real" | "$scratch_real"/*) ;;
258
- *)
259
- echo "scratch-run.sh: '$requested' resolves to '$candidate_dir', outside '$scratch_real' — refusing" >&2
260
- echo " a file is runnable here only from <state_dir>/$SCRATCH_SUBDIR/ (see that directory's README.md)" >&2
204
+ # A configuration that cannot be read is a scratch directory that cannot be
205
+ # located: both are closed here.
206
+ hr_scratch_path_var "$repo_root" "$requested"
207
+ case "$HR_SCRATCH_WHY" in
208
+ "") ;;
209
+ dotdot)
210
+ echo "scratch-run.sh: '$requested' carries '..' — refusing, a scratch path names no parent" >&2
211
+ exit 65
212
+ ;;
213
+ charset)
214
+ echo "scratch-run.sh: '$requested' carries a character outside A-Za-z0-9._/- — refusing" >&2
215
+ exit 65
216
+ ;;
217
+ outside)
218
+ echo "scratch-run.sh: '$requested' resolves to '$HR_SCRATCH_PARENT', outside '$HR_SCRATCH_DIR' — refusing" >&2
219
+ echo " a file is runnable here only from <state_dir>/$HR_SCRATCH_SUBDIR/ (see that directory's README.md)" >&2
261
220
  exit 65
262
221
  ;;
222
+ itself)
223
+ echo "scratch-run.sh: '$requested' names the scratch directory itself — refusing" >&2
224
+ exit 65
225
+ ;;
226
+ symlink)
227
+ echo "scratch-run.sh: '$requested' is a symlink — refusing, its target is outside this script's judgement" >&2
228
+ exit 65
229
+ ;;
230
+ no-parent)
231
+ echo "scratch-run.sh: '$requested' names no existing directory under '$repo_root'" >&2
232
+ exit 67
233
+ ;;
234
+ no-config)
235
+ echo "scratch-run.sh: cannot resolve '$repo_root/harness.config.json' — refusing to run anything" >&2
236
+ echo " (no configuration there, invalid JSON, more than one document, no defaultBranch, or jq missing/older than 1.5)" >&2
237
+ exit 68
238
+ ;;
239
+ no-scratch)
240
+ echo "scratch-run.sh: '$(hr_state_path "$repo_root" "$HR_SCRATCH_SUBDIR")' does not exist — re-run 'init' to materialize the state tree" >&2
241
+ exit 68
242
+ ;;
243
+ usage | *)
244
+ echo "scratch-run.sh: no file argument" >&2
245
+ echo " usage: scratch-run.sh <file-under-state-dir-scratch> [<arg>...]" >&2
246
+ exit 64
247
+ ;;
263
248
  esac
264
249
 
265
- target="$candidate_dir/$(basename "$candidate")"
266
- if [ -L "$target" ]; then
267
- echo "scratch-run.sh: '$requested' is a symlink — refusing, its target is outside this script's judgement" >&2
268
- exit 65
269
- fi
250
+ target="$HR_SCRATCH_TARGET"
270
251
  if [ ! -f "$target" ] || [ ! -r "$target" ]; then
271
252
  echo "scratch-run.sh: '$target' is not a readable regular file" >&2
272
253
  exit 67
@@ -2,7 +2,7 @@
2
2
 
3
3
  Everything the delivery flow writes and reads lives here: the task prompt a branch starts from, the plans made for it, the findings of every review round, the progress ledger a resumed run reads back, the end-of-run statistics, and the two long-lived ledgers at the root of this directory. **The tree is committed** — these artifacts are the record of how each branch was planned, reviewed and fixed, and a reviewer or a resumed run reads them out of the repository rather than out of one machine's scratch space.
4
4
 
5
- What git ignores is the machine-local part of it, and it is a short list: the run daemon's per-run transcripts, event logs and run registry under `<state_dir>/autonomous_logs/`; the prompts dropped into `<state_dir>/autonomous_inbox/`; the questions a parked run asks with the answers it is given, under `<state_dir>/clarifications/`; and the throwaway files an agent runs a probe or a mutation check from, under `<state_dir>/scratch/`; and the full output of each test-gate run, one log per round, under `<state_dir>/test_run_logs/`. All five are ignored **by their contents**, so the committed `README.md` in each survives the rule and the directory keeps its contract. Ignored with them are the stop, pause and dispatch-count files a run leaves flat at the root of this directory while it is in flight — a committed `STOP` being the one that would halt every run for everyone who clones, at a step whose own instruction forbids deleting the file.
5
+ What git ignores is the machine-local part of it, and it is a short list: the run daemon's per-run transcripts, event logs and run registry under `<state_dir>/autonomous_logs/`; the prompts dropped into `<state_dir>/autonomous_inbox/`; the questions a parked run asks with the answers it is given, under `<state_dir>/clarifications/`; and the throwaway files an agent runs a probe or a mutation check from, with the directories the branch commands' GitHub route fetches a branch's remote state into, under `<state_dir>/scratch/`; and the full output of each test-gate run, one log per round, under `<state_dir>/test_run_logs/`. All five are ignored **by their contents**, so the committed `README.md` in each survives the rule and the directory keeps its contract. Ignored with them are the stop, pause and dispatch-count files a run leaves flat at the root of this directory while it is in flight — a committed `STOP` being the one that would halt every run for everyone who clones, at a step whose own instruction forbids deleting the file.
6
6
 
7
7
  **Do not rename this directory to a dot-name.** That is this tree's own hard constraint, not a preference. It is tempting to tidy the tree out of sight, and it is the one change that risks unattended operation: this tree has to be writable by an unattended run, and a dot-path is where a host reserves directories an unattended run may not write to — the measured case is the host's own `.claude/**`, where an unattended run completes reporting success with nothing written. The name is configured as `stateDir` in `harness.config.json`, which refuses a dot at the start of any of its path segments rather than trusting which dot-paths a given host reserves — so a value that only reaches a dot-directory by traversal is refused as well as a plainly dot-named one.
8
8
 
@@ -1,8 +1,10 @@
1
1
  # scratch/
2
2
 
3
- A throwaway file an agent writes so that it can **execute** something: a language probe, before the code that would answer the question exists, or a mutation check that breaks an implementation on purpose to prove a new test fails. It is a file in the project's own language, and the interpreter follows from its extension — `probe.py` is run as Python, `probe.mjs` as Node. **Nothing depends on its name.** This is the one directory in the tree where a file's name carries no contract, and the only reason to choose it with any care is that two dispatches in flight at once must not land on the same one.
3
+ A throwaway file an agent writes so that it can **execute** something: a language probe, before the code that would answer the question exists, or a mutation check that breaks an implementation on purpose to prove a new test fails. It is a file in the project's own language, and the interpreter follows from its extension — `probe.py` is run as Python, `probe.mjs` as Node. **Nothing depends on a probe file's name.** A probe is the one file in the tree whose name carries no contract — the fetch directories below are the exception here — and the only reason to choose it with any care is that two dispatches in flight at once must not land on the same one.
4
4
 
5
- A file here is written by the dispatched implementer or reviewer that needs the answer. **Nothing reads it**, and the harness's scratch runner is the route the flow **provides** for executing it — the route to use, not the only one that exists. It lives for the length of one dispatch — the agent that wrote it is the last participant with a reason to care about it — so this directory's steady state is empty. Nothing rotates or archives it, which makes deleting it once the answer is in hand the whole of the housekeeping.
5
+ The GitHub route of `/autonomous-sdlc-harness:branch-pause`, `-resume`, `-answer` and `-user-review` also fetches a branch's remote state into `<state_dir>/scratch/<command>-<branch_fold>/`, and removes that directory with `remote-run.sh discard` on every path. `<branch_fold>` is the branch with every character outside `A-Za-z0-9_-` replaced by `_` — `feat/recent_searches_panel` gives `branch-pause-feat_recent_searches_panel` — so the name is always one path segment `discard` accepts. Two branches that fold to the same name are safe: the second command finds the first's directory, stops on it as a leftover, and never shares or removes it.
6
+
7
+ A probe file here is written by the dispatched implementer or reviewer that needs the answer. **Nothing reads it**, and the harness's scratch runner is the route the flow **provides** for executing it — the route to use, not the only one that exists. It lives for the length of one dispatch — the agent that wrote it is the last participant with a reason to care about it — so this directory's steady state is empty. Nothing rotates or archives it, which makes deleting it once the answer is in hand the whole of the housekeeping.
6
8
 
7
9
  Its contents are **machine-local and gitignored**: the directory is ignored by its contents rather than as a directory, and this README survives by an explicit negation, which makes it the only committed file here.
8
10
 
@@ -2,8 +2,8 @@
2
2
 
3
3
  Two kinds of file under one root, both suffixed by round: the operator's hands-on review of a finished branch at `<branch>_review[_<n>].md`, and the fix plan it produces — a `<branch>_fix_plan[_<N>].md` index plus one `finding_<K>.md` per finding — written by the fix-plan writer and worked through by the implementers, reviewers and committer. Round 1 is unsuffixed and later rounds carry `_2`, `_3`, … A fix plan takes the **same** round number as the review it was written from, and its per-finding folder mirrors its index suffix exactly. So a `<branch>_fix_plan_2.md` found alone is round 2: it was written from `<branch>_review_2.md` in this directory, and its findings sit in `<branch>_fix_plan_2/finding_<K>.md` beside it.
4
4
 
5
- The review is written by hand after the hands-on pass, in the format `${CLAUDE_PLUGIN_ROOT}/samples/sample_user_review.md` fixes — or dropped through the unattended loop, which copies it here under its original name before launching the fix flow. The fix plan is the fix-plan writer's: it discovers the latest review here, derives both output paths from that round, and writes the index to the format `${CLAUDE_PLUGIN_ROOT}/samples/sample_user_review_fix_plan.md` fixes with per-finding files shaped like `${CLAUDE_PLUGIN_ROOT}/samples/sample_user_review_fix_plan/finding_1.md`. The drafted plan is then graded by the plan-review gates before any of its fixes is implemented, and walked item by item by the fix loop.
5
+ The review is written by hand after the hands-on pass, in the format `${CLAUDE_PLUGIN_ROOT}/samples/sample_user_review.md` fixes — or dropped through the unattended loop, which copies it here under its original name before launching the fix flow — or submitted on GitHub as a pull-request review requesting changes on the run's branch, which becomes the next round. Its layout — one section per review — every review requesting changes, and every other review that carries a summary — with a provenance line naming its state, each inline comment with its file, line, commit, author and diff hunk, and a closing marker line — is set by the `control` paragraph of `remote-run.sh`'s header in the configured scripts directory. The fix plan is the fix-plan writer's: it discovers the latest review here, derives both output paths from that round, and writes the index to the format `${CLAUDE_PLUGIN_ROOT}/samples/sample_user_review_fix_plan.md` fixes with per-finding files shaped like `${CLAUDE_PLUGIN_ROOT}/samples/sample_user_review_fix_plan/finding_1.md`. The drafted plan is then graded by the plan-review gates before any of its fixes is implemented, and walked item by item by the fix loop.
6
6
 
7
- The unattended drop deliberately does **not** commit the review — unlike a prompt or a checklist it is not a precondition of the first step, and the fix flow's own first commit stages the review beside the fix-plan index and its per-finding folder. Nothing supersedes either file afterwards: a further round is written beside them under the next suffix, and the accumulated rounds are the branch's hands-on-review history. The whole directory is committed with the branch, since no ignore rule reaches it.
7
+ The unattended drop for a **local** run deliberately does **not** commit the review — unlike a prompt or a checklist it is not a precondition of the first step, and the fix flow's own first commit stages the review beside the fix-plan index and its per-finding folder. A round for a run executing on GitHub — dropped, sent by the local command, or arriving as a pull-request review — is committed as `chore: add user review for <branch>` and pushed before its dispatch, because a job boundary before the fix flow's own commit would lose it. Nothing supersedes either file afterwards: a further round is written beside them under the next suffix, and the accumulated rounds are the branch's hands-on-review history. The whole directory is committed with the branch, since no ignore rule reaches it.
8
8
 
9
9
  The mistake worth naming is guessing the round instead of resolving it. The suffix is the **next free** index in this directory — list it first. A second pass that restarts at the unsuffixed name writes over round 1's record with nothing to flag it, and because that record is already tracked, the overwrite also dirties the tree the flow expects clean. The unattended path refuses a same-name drop whose content differs and tells the operator to use the next suffix; a file placed here by hand gets no such guard.