autonomous-sdlc-harness 0.1.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 (171) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +7 -0
  3. package/README.md +24 -0
  4. package/dist/cli.js +194 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/config.js +561 -0
  7. package/dist/commands/config.js.map +1 -0
  8. package/dist/commands/daemon.js +791 -0
  9. package/dist/commands/daemon.js.map +1 -0
  10. package/dist/commands/doctor.js +336 -0
  11. package/dist/commands/doctor.js.map +1 -0
  12. package/dist/commands/init.js +2023 -0
  13. package/dist/commands/init.js.map +1 -0
  14. package/dist/commands/registry.js +42 -0
  15. package/dist/commands/registry.js.map +1 -0
  16. package/dist/config/check.js +505 -0
  17. package/dist/config/check.js.map +1 -0
  18. package/dist/config/io.js +177 -0
  19. package/dist/config/io.js.map +1 -0
  20. package/dist/config/model.js +406 -0
  21. package/dist/config/model.js.map +1 -0
  22. package/dist/core/errors.js +71 -0
  23. package/dist/core/errors.js.map +1 -0
  24. package/dist/core/git.js +537 -0
  25. package/dist/core/git.js.map +1 -0
  26. package/dist/core/json.js +125 -0
  27. package/dist/core/json.js.map +1 -0
  28. package/dist/core/layerCoverage.js +141 -0
  29. package/dist/core/layerCoverage.js.map +1 -0
  30. package/dist/core/layerGapRemedy.js +62 -0
  31. package/dist/core/layerGapRemedy.js.map +1 -0
  32. package/dist/core/nameList.js +23 -0
  33. package/dist/core/nameList.js.map +1 -0
  34. package/dist/core/paths.js +153 -0
  35. package/dist/core/paths.js.map +1 -0
  36. package/dist/core/prompt.js +206 -0
  37. package/dist/core/prompt.js.map +1 -0
  38. package/dist/core/repoPaths.js +55 -0
  39. package/dist/core/repoPaths.js.map +1 -0
  40. package/dist/core/report.js +150 -0
  41. package/dist/core/report.js.map +1 -0
  42. package/dist/core/templating.js +88 -0
  43. package/dist/core/templating.js.map +1 -0
  44. package/dist/core/writer.js +479 -0
  45. package/dist/core/writer.js.map +1 -0
  46. package/dist/daemon/backend.js +180 -0
  47. package/dist/daemon/backend.js.map +1 -0
  48. package/dist/daemon/units.js +380 -0
  49. package/dist/daemon/units.js.map +1 -0
  50. package/dist/detect/nestedApplication.js +79 -0
  51. package/dist/detect/nestedApplication.js.map +1 -0
  52. package/dist/detect/presets.js +2033 -0
  53. package/dist/detect/presets.js.map +1 -0
  54. package/dist/detect/signals.js +1368 -0
  55. package/dist/detect/signals.js.map +1 -0
  56. package/dist/doctor/checks.js +3530 -0
  57. package/dist/doctor/checks.js.map +1 -0
  58. package/dist/generators/claudeContext.js +588 -0
  59. package/dist/generators/claudeContext.js.map +1 -0
  60. package/dist/generators/githooks.js +446 -0
  61. package/dist/generators/githooks.js.map +1 -0
  62. package/dist/generators/harnessConfig.js +632 -0
  63. package/dist/generators/harnessConfig.js.map +1 -0
  64. package/dist/generators/notifications.js +191 -0
  65. package/dist/generators/notifications.js.map +1 -0
  66. package/dist/generators/outerLoopScripts.js +165 -0
  67. package/dist/generators/outerLoopScripts.js.map +1 -0
  68. package/dist/generators/permissionProfile.js +1172 -0
  69. package/dist/generators/permissionProfile.js.map +1 -0
  70. package/dist/generators/projectSettings.js +322 -0
  71. package/dist/generators/projectSettings.js.map +1 -0
  72. package/dist/generators/repoRoot.js +417 -0
  73. package/dist/generators/repoRoot.js.map +1 -0
  74. package/dist/generators/scripts.js +557 -0
  75. package/dist/generators/scripts.js.map +1 -0
  76. package/dist/generators/stateDir.js +221 -0
  77. package/dist/generators/stateDir.js.map +1 -0
  78. package/dist/machine/paths.js +111 -0
  79. package/dist/machine/paths.js.map +1 -0
  80. package/dist/machine/plugins.js +224 -0
  81. package/dist/machine/plugins.js.map +1 -0
  82. package/dist/machine/registry.js +330 -0
  83. package/dist/machine/registry.js.map +1 -0
  84. package/package.json +23 -0
  85. package/scripts/README.md +13 -0
  86. package/scripts/daemon/launchd.plist.template +59 -0
  87. package/scripts/daemon/systemd.service.template +58 -0
  88. package/templates/README.md +15 -0
  89. package/templates/claude/CLAUDE.md +54 -0
  90. package/templates/claude/README.md +5 -0
  91. package/templates/claude/context/api.md +29 -0
  92. package/templates/claude/context/conventions.md +23 -0
  93. package/templates/claude/context/data-layer.md +28 -0
  94. package/templates/claude/context/data-storage.md +29 -0
  95. package/templates/claude/context/docs-catalog.md +29 -0
  96. package/templates/claude/context/domain.md +28 -0
  97. package/templates/claude/context/layer.md +20 -0
  98. package/templates/claude/context/module.md +30 -0
  99. package/templates/claude/context/package.md +29 -0
  100. package/templates/claude/context/presentation.md +32 -0
  101. package/templates/claude/context/state-slices.md +28 -0
  102. package/templates/claude/context/tests.md +28 -0
  103. package/templates/claude/harness-task-offer.md +58 -0
  104. package/templates/claude/push-notify.env.example +21 -0
  105. package/templates/claude/qa-accounts.env.example +38 -0
  106. package/templates/claude/qa_test_scenarios.md +110 -0
  107. package/templates/claude/settings.autonomous.json +93 -0
  108. package/templates/claude/settings.autonomous.qa.json +36 -0
  109. package/templates/githooks/README.md +3 -0
  110. package/templates/githooks/pre-push +72 -0
  111. package/templates/repo/README.md +3 -0
  112. package/templates/repo/gitattributes +16 -0
  113. package/templates/repo/gitignore +61 -0
  114. package/templates/repo/gitignore.qa +25 -0
  115. package/templates/repo/mcp.json +17 -0
  116. package/templates/scripts/README.md +5 -0
  117. package/templates/scripts/autonomous-format-stream.sh +95 -0
  118. package/templates/scripts/autonomous-notify.sh +337 -0
  119. package/templates/scripts/autonomous-watcher.sh +3087 -0
  120. package/templates/scripts/cleanup-merged-worktrees.sh +327 -0
  121. package/templates/scripts/commit-on-branch.sh +288 -0
  122. package/templates/scripts/create-worktree.sh +360 -0
  123. package/templates/scripts/deploy.sh +47 -0
  124. package/templates/scripts/lib/harness-run-lib.sh +1481 -0
  125. package/templates/scripts/push-branch.sh +140 -0
  126. package/templates/scripts/refresh-branch.sh +244 -0
  127. package/templates/scripts/restart-watcher.sh +401 -0
  128. package/templates/scripts/scratch-run.sh +302 -0
  129. package/templates/scripts/setup-worktree.sh +262 -0
  130. package/templates/scripts/start-dev-server.sh +99 -0
  131. package/templates/scripts/test.sh +50 -0
  132. package/templates/scripts/typecheck.sh +50 -0
  133. package/templates/state-dir/README-root.md +13 -0
  134. package/templates/state-dir/README.md +9 -0
  135. package/templates/state-dir/architecture_branch_review_point_reviews/README.md +9 -0
  136. package/templates/state-dir/architecture_branch_reviews/README.md +9 -0
  137. package/templates/state-dir/architecture_reviews/README.md +9 -0
  138. package/templates/state-dir/architecture_user_review_reviews/README.md +9 -0
  139. package/templates/state-dir/autonomous_inbox/README.md +9 -0
  140. package/templates/state-dir/autonomous_logs/README.md +9 -0
  141. package/templates/state-dir/branch_statistics/README.md +9 -0
  142. package/templates/state-dir/business_parity_branch_review_point_reviews/README.md +9 -0
  143. package/templates/state-dir/business_parity_branch_reviews/README.md +9 -0
  144. package/templates/state-dir/business_parity_reviews/README.md +9 -0
  145. package/templates/state-dir/business_parity_user_review_reviews/README.md +9 -0
  146. package/templates/state-dir/clarification_digests/README.md +9 -0
  147. package/templates/state-dir/clarifications/README.md +9 -0
  148. package/templates/state-dir/code_reviews/README.md +9 -0
  149. package/templates/state-dir/dispatch_additions/README.md +19 -0
  150. package/templates/state-dir/docs_catalog/README.md +9 -0
  151. package/templates/state-dir/flow_progress/README.md +9 -0
  152. package/templates/state-dir/improvement_observations/README.md +19 -0
  153. package/templates/state-dir/improvement_suggestions.md +29 -0
  154. package/templates/state-dir/lessons.md +23 -0
  155. package/templates/state-dir/qa_review_point_reviews/README.md +9 -0
  156. package/templates/state-dir/qa_reviews/README.md +9 -0
  157. package/templates/state-dir/review_plan_point_reviews/README.md +9 -0
  158. package/templates/state-dir/review_plan_reviews/README.md +9 -0
  159. package/templates/state-dir/scratch/README.md +11 -0
  160. package/templates/state-dir/skeptic_review_plan_reviews/README.md +9 -0
  161. package/templates/state-dir/skeptic_review_point_reviews/README.md +9 -0
  162. package/templates/state-dir/skeptic_reviews/README.md +9 -0
  163. package/templates/state-dir/story_plans/README.md +9 -0
  164. package/templates/state-dir/task_plan_point_reviews/README.md +9 -0
  165. package/templates/state-dir/task_plan_reviews/README.md +9 -0
  166. package/templates/state-dir/task_plans/README.md +9 -0
  167. package/templates/state-dir/task_prompts/README.md +9 -0
  168. package/templates/state-dir/ui_test_plan_reviews/README.md +9 -0
  169. package/templates/state-dir/ui_test_plans/README.md +9 -0
  170. package/templates/state-dir/user_review_fix_plan_point_reviews/README.md +9 -0
  171. package/templates/state-dir/user_reviews/README.md +9 -0
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env bash
2
+ # scratch-run.sh — run ONE file that lives under `<state_dir>/scratch/`, in the
3
+ # interpreter that file's own extension names, and refuse every other path.
4
+ #
5
+ # WHAT IT IS FOR. The two things a dispatched agent otherwise has no permitted
6
+ # route to do: a LANGUAGE PROBE — run a few lines in the project's own language
7
+ # before the code that would answer the question exists — and a MUTATION CHECK —
8
+ # break an implementation on purpose to prove a new test fails. Without a route
9
+ # the agent either reaches for an interpreter and is refused, or silently
10
+ # downgrades a claim it meant to verify from *executed* to *reasoned*.
11
+ #
12
+ # WHY A WRAPPER EXISTS AT ALL. Same reason as its siblings: an invocation of a
13
+ # script path under the configured scripts directory is allow-listed literally in
14
+ # three forms by the generated permission profile (this file's row in
15
+ # `cli/src/generators/outerLoopScripts.ts` carries `agentInvocable: true`) and is
16
+ # auto-allowed by the script-allowlist guard — a script under that directory
17
+ # whose basename is not on the guard's deny list, in a command carrying none of
18
+ # `$(…)`, a backtick, `|`, `<`, a braced form other than a bare `${IDENT}`, or a
19
+ # `>` that is neither a descriptor duplication (`2>&1`, `>&2`, `2>&-`) nor a
20
+ # redirection to the literal `/dev/null`. A bare `python3 …` command line is
21
+ # matched statically by neither and draws a prompt an unattended run cannot
22
+ # answer. This basename is deliberately NOT on that deny list, and its absence
23
+ # there is the decision that makes the guard route cover this script from the
24
+ # first run, whether or not the profile has been regenerated.
25
+ #
26
+ # WHAT THIS DOES NOT CONTAIN. The first argument is a path and every remaining
27
+ # argument is forwarded to the file, so nothing here takes an inline program — no
28
+ # `-c`, no `-e`, no `bash -c`. That fence is over WHICH FILE RUNS and over nothing
29
+ # the file does. A probe is a program the agent composed one tool call earlier and
30
+ # it runs with the session's own privileges, so everything the generated profile's
31
+ # `deny` floor and its `ask` list, and the script-allowlist guard's
32
+ # `DENY_SCRIPT_BASENAMES`, withhold from a COMMAND STRING is reachable from inside
33
+ # one — directly, or one `subprocess` / `child_process` call further on. Do not
34
+ # read the file/string distinction as a capability boundary: it costs an extra
35
+ # tool call. The layer that still holds is the `pre-push` git hook, because git
36
+ # runs it however git was invoked, subprocess included — the ranking
37
+ # `plugin/docs/AUTONOMOUS_FLOW_WHITEBOARD.md`'s protected-branch bullet states.
38
+ # The two mitigations that do apply are narrower than containment and are named as
39
+ # what they are: `<state_dir>/scratch/` is gitignored by its contents, so nothing
40
+ # run from here reaches a commit, and the agent instructions require a MUTATION
41
+ # CHECK to be reverted before the task's own verification runs.
42
+ #
43
+ # THE REFUSAL IS THE WHOLE OF THE SAFETY — over WHICH FILE, and over nothing
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.
55
+ #
56
+ # THE INTERPRETER COMES FROM THE FILE'S OWN EXTENSION. The table is below, and it
57
+ # is the only place it is stated. NOT from the detected preset, on two grounds
58
+ # that both survive reading `DETECTION_PRESET_NAMES` (`cli/src/config/model.ts`).
59
+ # A preset covers the repository's LAYOUT rather than its toolchain: some names in
60
+ # `DETECTION_PRESET_NAMES` imply a language and others imply none, and which
61
+ # language a repository is actually probed in is settled by `commandFamilies`
62
+ # (`cli/src/detect/presets.ts`), not by the preset. So a preset-derived entry
63
+ # reads the wrong field — silent wherever the preset names no toolchain, and
64
+ # wrong wherever a probe's language is not the one the preset implies. And it
65
+ # would be the wrong SHAPE regardless: a preset-derived profile entry
66
+ # is a `Bash(<interpreter>:*)` grant over every command line that interpreter can
67
+ # be handed, which is the breadth the profile `_README`'s package-manager
68
+ # paragraph refuses — while this table grants nothing at all, the reachability
69
+ # coming from this script's own row. NOT from a configuration key either: the
70
+ # family that answered IS recorded, as `detection.commandFamily`, and this table
71
+ # is still not derived from it — a family id answers which toolchain answered for
72
+ # the command keys, not what interpreter a scratch file needs, and a
73
+ # family-derived entry would be the same over-broad `Bash(<interpreter>:*)` grant
74
+ # that paragraph refuses. An extension the table
75
+ # does not carry is refused by name, listing the ones that are — never guessed,
76
+ # and never run under `sh`.
77
+ #
78
+ # WHAT THAT COSTS, STATED RATHER THAN LEFT TO BE DISCOVERED. The table is a CLOSED
79
+ # seven-extension set with NO ADOPTER EXTENSION POINT: no configuration key, no
80
+ # flag, and the file ships fixed. Its `<extension>:<interpreter>` shape is a SINGLE
81
+ # token run as `exec "$interpreter" "$target" "$@"`, so it cannot express a
82
+ # two-word toolchain invocation — `go run`, `cargo`, `dotnet script`. THAT is what
83
+ # bounds the set, and it bounds it by TOOLCHAIN rather than by adoption: the
84
+ # stacks with no probe route here are the ones whose file is not run by a single
85
+ # token — Go, Rust, .NET, Swift, the JVM and Android, C++. Every language a
86
+ # `commandFamilies` function (`cli/src/detect/presets.ts`) covers that DOES run a
87
+ # source file as `<token> <file>` is carried: Node, Python, Ruby, Dart and PHP.
88
+ # Where an adoption's language is not one this table carries there is no probe
89
+ # route here at all, and what an agent has instead is the evidence downgrade
90
+ # `plugin/agents/layer-implementer.md` requires of it (its *An evidence downgrade
91
+ # is recorded* paragraph) — the claim recorded as reasoned rather than executed.
92
+ # Widening the set to a two-word toolchain is a decision for a later round,
93
+ # carried as a residual in `docs/outer-loop-verification.md` §4.
94
+ #
95
+ # THE RUN MUST NEVER SEE A FILE FROM HERE IN A COMMIT. `<state_dir>/scratch/` is
96
+ # ignored by its CONTENTS — the managed block in the generated `.gitignore`, with
97
+ # a negation for that directory's own README — so a probe cannot be committed by
98
+ # accident. The other half of that duty belongs to the agent rather than to this
99
+ # script: a MUTATION CHECK is REVERTED before the task's own verification runs,
100
+ # because the committer will see that suite and it has to be clean.
101
+ #
102
+ # IT IS DELIBERATELY NOT A WRAPPER. It carries no adopter command line — it is
103
+ # not a `WRAPPER_SCRIPTS` row, answers to no `commands.*` key, and has no
104
+ # `{{command}}` for `init` to inline. Like every outer-loop script it ships fixed
105
+ # and reads `harness.config.json` at run time.
106
+ #
107
+ # IT PRINTS NOTHING OF ITS OWN ON THE RUN PATH. The file's stdout and stderr are
108
+ # the answer, and a caller reading a probe's output must not have to filter a
109
+ # wrapper's verdict line out of it — the opposite of the project-command
110
+ # wrappers, whose one verdict line is their contract. Every refusal, and only a
111
+ # refusal, prints `scratch-run.sh: …` on stderr.
112
+ #
113
+ # Usage: scratch-run.sh <file-under-state-dir-scratch> [<arg>...]
114
+ # <file> the file to run; relative to <repo_root>, or absolute
115
+ # <arg>... forwarded to that file, unchanged
116
+ #
117
+ # Exit map a caller can switch on. A run passes the file's OWN status through
118
+ # untouched, so the refusal codes cannot be exclusive — a probe is free to exit
119
+ # 65. The reliable signal that NOTHING ran is the `scratch-run.sh:` line on
120
+ # stderr; the codes are for telling one refusal from another:
121
+ #
122
+ # 0-N the file ran; this is its own exit status, unmodified
123
+ # 64 usage error — no file argument
124
+ # 65 path refused — a `..`, a character outside `A-Za-z0-9._/-`, a symlink,
125
+ # or a target that does not resolve inside <state_dir>/scratch/
126
+ # 66 the extension has no interpreter in the table below
127
+ # 67 the file is not there, or is not a readable regular file
128
+ # 68 the run-time context could not be established — the shared library is
129
+ # unreadable, this is not a git repository, `harness.config.json` could
130
+ # not be resolved, or the scratch directory has never been materialized
131
+ # 69 the interpreter the extension names is not on PATH
132
+ #
133
+ # REPRO — reproduce any decision by hand, against a throwaway fixture:
134
+ #
135
+ # d=$(mktemp -d); git -C "$d" init -q -b feat_x
136
+ # printf '%s' '{"version":1,"defaultBranch":"main","stateDir":"sdlc-harness/","layers":[],"commands":{}}' > "$d/harness.config.json"
137
+ # mkdir -p "$d/sdlc-harness/scratch"
138
+ # printf 'import sys\nprint("ok", sys.argv[1:])\n' > "$d/sdlc-harness/scratch/probe.py"
139
+ # cp -R <scripts_dir>/. "$d/scripts/" # this file and its lib/
140
+ #
141
+ # the probe (cd "$d" && scripts/scratch-run.sh sdlc-harness/scratch/probe.py a b)
142
+ # -> prints `ok ['a', 'b']`, exit 0
143
+ # its own status a probe ending `sys.exit(7)` -> exit 7
144
+ # absolute form scripts/scratch-run.sh "$d/sdlc-harness/scratch/probe.py" -> runs
145
+ # outside scripts/scratch-run.sh harness.config.json -> exit 65
146
+ # traversal scripts/scratch-run.sh sdlc-harness/scratch/../../harness.config.json
147
+ # -> exit 65
148
+ # unknown ext mv probe.py probe.pl; same call -> exit 66
149
+ # shell probe mv probe.py probe.sh; same call -> exit 66
150
+ # absent file scripts/scratch-run.sh sdlc-harness/scratch/nope.py -> exit 67
151
+ # unresolvable printf 'x' > "$d/harness.config.json"; same call -> exit 68
152
+
153
+ # Deliberately no `-e`: every refusal below is an explicit `exit`, and an
154
+ # implicit one would skip the message that says what was refused and why.
155
+ set -uo pipefail
156
+
157
+ # --- THE INTERPRETER TABLE, stated once: `<extension>:<interpreter>`, space
158
+ # separated. Both readers below derive from this one string — the lookup, and
159
+ # the refusal message that lists what is supported — so there is no second list
160
+ # to keep in step with it. Matching is exact and case-sensitive: `.PY` is an
161
+ # extension this table does not carry, and a guess is worse than a refusal.
162
+ #
163
+ # THE ONE EXTENSION THIS TABLE OMITS ON PURPOSE IS `.sh`. Not for containment —
164
+ # WHAT THIS DOES NOT CONTAIN above says why a `.py` probe reaches a shell one
165
+ # `subprocess` call further on — but because it is the row reachable by fewer
166
+ # routes than the rest, buying nothing the others do not. The script-allowlist
167
+ # guard requires EVERY `.sh` token in a command to resolve under the scripts
168
+ # directory, so `bash <scripts_dir>/scratch-run.sh <state_dir>/scratch/probe.sh`
169
+ # carries a second one that does not, and falls through to the permission system:
170
+ # it would run on the profile entry alone, drawing a prompt on an adoption whose
171
+ # guard route covers every other row. A shell probe is refused by name here, with
172
+ # the table's supported extensions in the message.
173
+ SCRATCH_INTERPRETERS='py:python3 js:node mjs:node cjs:node rb:ruby dart:dart php:php'
174
+
175
+ # The one directory under `<state_dir>` a file may be run out of. Spelled once
176
+ # here, mirroring the `scratch` row of `STATE_DIR_ENTRIES` in
177
+ # `cli/src/generators/stateDir.ts`.
178
+ SCRATCH_SUBDIR='scratch'
179
+
180
+ # The library is reached by a path computed from this script's own location — no
181
+ # session root and no runtime-substituted token is assumed.
182
+ hr_lib="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/harness-run-lib.sh"
183
+ if [ ! -r "$hr_lib" ]; then
184
+ echo "scratch-run.sh: cannot read '$hr_lib' — refusing to run anything" >&2
185
+ exit 68
186
+ fi
187
+ # shellcheck source=lib/harness-run-lib.sh
188
+ . "$hr_lib"
189
+
190
+ if [ "$#" -lt 1 ] || [ -z "${1-}" ]; then
191
+ echo "scratch-run.sh: no file argument" >&2
192
+ echo " usage: scratch-run.sh <file-under-state-dir-scratch> [<arg>...]" >&2
193
+ exit 64
194
+ fi
195
+ requested="$1"
196
+
197
+ # --- The path tests, on the argument AS GIVEN, before anything is resolved.
198
+ case "$requested" in
199
+ *..*)
200
+ echo "scratch-run.sh: '$requested' carries '..' — refusing, a scratch path names no parent" >&2
201
+ exit 65
202
+ ;;
203
+ esac
204
+ case "$requested" in
205
+ *[!A-Za-z0-9._/-]*)
206
+ echo "scratch-run.sh: '$requested' carries a character outside A-Za-z0-9._/- — refusing" >&2
207
+ exit 65
208
+ ;;
209
+ esac
210
+
211
+ # The repository is THIS FILE's own checkout, never the caller's directory: the
212
+ # three forms the permission profile emits include a sibling worktree's own copy,
213
+ # and each copy must answer for the checkout it belongs to whatever directory the
214
+ # caller stood in.
215
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
216
+ repo_root="$(hr_repo_root "$script_dir")"
217
+ if [ -z "$repo_root" ]; then
218
+ echo "scratch-run.sh: '$script_dir' is not inside a git repository — refusing to run anything" >&2
219
+ exit 68
220
+ fi
221
+
222
+ # `<repo_root>/<state_dir>/scratch`, from this repository's configuration at run
223
+ # time. The library folds "not adopted" and "unreadable" into one non-zero
224
+ # answer, and both are closed here: a configuration that cannot be read is a
225
+ # scratch directory that cannot be located.
226
+ scratch_dir="$(hr_state_path "$repo_root" "$SCRATCH_SUBDIR")"
227
+ if [ -z "$scratch_dir" ]; then
228
+ echo "scratch-run.sh: cannot resolve '$repo_root/harness.config.json' — refusing to run anything" >&2
229
+ echo " (no configuration there, invalid JSON, more than one document, no defaultBranch, or jq missing/older than 1.5)" >&2
230
+ exit 68
231
+ fi
232
+
233
+ scratch_real="$(cd "$scratch_dir" 2>/dev/null && pwd -P)"
234
+ if [ -z "$scratch_real" ]; then
235
+ echo "scratch-run.sh: '$scratch_dir' does not exist — re-run 'init' to materialize the state tree" >&2
236
+ exit 68
237
+ fi
238
+
239
+ # A relative argument resolves against <repo_root>; an absolute one is taken as
240
+ # given. Neither can carry `..` by the test above.
241
+ case "$requested" in
242
+ /*) candidate="$requested" ;;
243
+ *) candidate="${repo_root%/}/$requested" ;;
244
+ esac
245
+
246
+ candidate_dir="$(cd "$(dirname "$candidate")" 2>/dev/null && pwd -P)"
247
+ if [ -z "$candidate_dir" ]; then
248
+ echo "scratch-run.sh: '$requested' names no existing directory under '$repo_root'" >&2
249
+ exit 67
250
+ fi
251
+
252
+ # STRICTLY INSIDE: the scratch directory itself or a directory beneath it, on the
253
+ # physical path of both sides, so a symlinked directory in the tree cannot widen
254
+ # the fence.
255
+ case "$candidate_dir" in
256
+ "$scratch_real" | "$scratch_real"/*) ;;
257
+ *)
258
+ echo "scratch-run.sh: '$requested' resolves to '$candidate_dir', outside '$scratch_real' — refusing" >&2
259
+ echo " a file is runnable here only from <state_dir>/$SCRATCH_SUBDIR/ (see that directory's README.md)" >&2
260
+ exit 65
261
+ ;;
262
+ esac
263
+
264
+ target="$candidate_dir/$(basename "$candidate")"
265
+ if [ -L "$target" ]; then
266
+ echo "scratch-run.sh: '$requested' is a symlink — refusing, its target is outside this script's judgement" >&2
267
+ exit 65
268
+ fi
269
+ if [ ! -f "$target" ] || [ ! -r "$target" ]; then
270
+ echo "scratch-run.sh: '$target' is not a readable regular file" >&2
271
+ exit 67
272
+ fi
273
+
274
+ # --- The interpreter, from the extension and from nothing else.
275
+ base="$(basename "$target")"
276
+ case "$base" in
277
+ ?*.*) extension="${base##*.}" ;;
278
+ *) extension="" ;;
279
+ esac
280
+
281
+ interpreter=""
282
+ supported=""
283
+ for entry in $SCRATCH_INTERPRETERS; do
284
+ if [ "${entry%%:*}" = "$extension" ]; then interpreter="${entry#*:}"; fi
285
+ if [ -z "$supported" ]; then supported=".${entry%%:*}"; else supported="$supported .${entry%%:*}"; fi
286
+ done
287
+
288
+ if [ -z "$interpreter" ]; then
289
+ echo "scratch-run.sh: '$base' has no interpreter for extension '${extension:-<none>}'" >&2
290
+ echo " the table carries: $supported" >&2
291
+ exit 66
292
+ fi
293
+
294
+ if ! command -v "$interpreter" >/dev/null 2>&1; then
295
+ echo "scratch-run.sh: '$interpreter' (for .$extension) is not on PATH — nothing was run" >&2
296
+ exit 69
297
+ fi
298
+
299
+ # The file, then the caller's remaining arguments, and nothing else. `exec` is
300
+ # what makes this script's exit status the file's own, with nothing added.
301
+ shift
302
+ exec "$interpreter" "$target" "$@"
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env bash
2
+ # setup-worktree.sh — bootstrap the checkout this script sits in, so a run can
3
+ # start in it: link the machine-local files a fresh working copy cannot carry in
4
+ # git, install the committed pre-push backstop for THIS working copy, install
5
+ # dependencies and build.
6
+ #
7
+ # WHO RUNS IT. create-worktree.sh, immediately after it adds a working copy —
8
+ # and a person, by hand, on a checkout that predates a configuration change.
9
+ # Every step is idempotent, so re-running it is the supported repair. It is not
10
+ # meant to be run by a dispatched agent: it is not agent-invocable, it has no
11
+ # entry in the generated permission profile, and it should not get one — it
12
+ # installs dependencies and writes git configuration, which are not decisions an
13
+ # agent takes. That is a CALLING CONVENTION, NOT A GATE. Its basename is not on
14
+ # the script-allowlist guard's `DENY_SCRIPT_BASENAMES`, and that guard grants
15
+ # independently of the profile, so an agent that invokes this path under
16
+ # `scriptsDir` is auto-allowed today — but only in a PLAIN invocation: that
17
+ # guard's construct scan withholds the allow, silently, from any command
18
+ # carrying `$(…)`, a backtick, `|`, `<`, a braced form other than a bare
19
+ # `${IDENT}`, or a `>` that is neither a descriptor duplication (`2>&1`, `>&2`,
20
+ # `2>&-`) nor a redirection to the literal `/dev/null`. Adding the basename to
21
+ # `DENY_SCRIPT_BASENAMES` is what would make the convention enforced.
22
+ #
23
+ # THE FIVE STEPS, AND WHAT EACH DOES WHEN ITS INPUT IS ABSENT.
24
+ #
25
+ # 1. Version manager. A stack that pins its toolchain through a version
26
+ # manager needs that manager's init file sourced before anything installs.
27
+ # Point `HARNESS_VERSION_MANAGER_INIT` at that file (in the watcher's
28
+ # environment, or in the operator's shell profile) and it is sourced here.
29
+ # Unset, or naming a file that is not readable, is a SILENT no-op: most
30
+ # projects have no such file and a note about it every run is noise.
31
+ # 2. The machine-local client env file. When the MAIN checkout has
32
+ # `<appDir>/.env` and this checkout has none, it is SYMLINKED — one file,
33
+ # one place, and gitignored on both sides by the managed `.gitignore`
34
+ # block's `<appDir>/.env` rule, which is what leaves a freshly bootstrapped
35
+ # working copy showing nothing at all in `git status --porcelain`. There is
36
+ # no configuration key for it, and `clientEnvPrefix` is not one (it is
37
+ # review vocabulary — the prefix a build tool requires before exposing a
38
+ # variable to a client bundle — not a path), so the existence test IS the
39
+ # condition. An adopter whose stack needs a different machine-local file
40
+ # adds it here: this script is theirs from `init` on, and a re-run never
41
+ # overwrites it.
42
+ # 3. The test-account credentials at `qa.credentialsPath`, symlinked from the
43
+ # main checkout by the same rule. NEVER copied: the file is sensitive and
44
+ # gitignored, and a symlink keeps the secret in one place and keeps the
45
+ # working copy clean. No key configured means nothing is attempted.
46
+ # 4. The pre-push backstop at `githooksDir`, installed for this working copy
47
+ # alone (see the note at the step itself). A missing hook file is a skip
48
+ # with one line, so a checkout that predates the hook still bootstraps.
49
+ # 5. `commands.depInstall`, then `commands.build`, each run from the
50
+ # repository root. An UNSET key is skipped with one printed line rather
51
+ # than being an error — a project with no build step is ordinary. A build
52
+ # is skipped when the dependency install failed, because a build on a
53
+ # failed install only produces a second, less clear failure.
54
+ #
55
+ # THE REFERENCE TOOLCHAIN IS NOT BOOTSTRAPPED HERE. A project that mirrors a
56
+ # reference implementation lists that implementation's own commands in
57
+ # `parity.toolchainCommands`, which the review phase runs and reports deferred
58
+ # when they are unavailable in an isolated working copy. Installing them is not
59
+ # part of preparing a working copy, and doing it here would make every bootstrap
60
+ # depend on a toolchain most steps never use.
61
+ #
62
+ # WHY NO `set -e`, AND WHY ITS CALLER HAS ONE. Each step's outcome is decided
63
+ # here — skip, warn, or fail — and `-e` would turn the ordinary "this key is not
64
+ # configured" into an abort. Failures are accumulated instead and reported in
65
+ # the exit status, which is what create-worktree.sh's `set -e` then acts on: a
66
+ # working copy whose dependencies did not install is not pushed as if it had.
67
+ #
68
+ # FAIL CLOSED ON A CONFIGURATION IT CANNOT READ. Every path and command below is
69
+ # configured, and none has a remembered fallback, so an absent or unreadable
70
+ # `harness.config.json` is a refusal that changes nothing — not a bootstrap that
71
+ # guesses where the hooks are.
72
+ #
73
+ # Usage: setup-worktree.sh
74
+ # No arguments. The checkout is this script's own location, not $PWD, because
75
+ # the caller runs it from wherever it happens to be.
76
+ #
77
+ # Exit map:
78
+ #
79
+ # 0 every step ran or was deliberately skipped
80
+ # 1 refusal (the library or the configuration could not be read), or a step
81
+ # that was meant to run failed — the message above the exit says which
82
+ #
83
+ # REPRO — reproduce any decision by hand, against a throwaway fixture:
84
+ #
85
+ # w=$(mktemp -d); d="$w/demo"; git init -q -b trunk "$d"; mkdir -p "$d/githooks"
86
+ # printf '#!/usr/bin/env bash\nexit 0\n' > "$d/githooks/pre-push"
87
+ # printf '%s' '{"version":1,"defaultBranch":"trunk","stateDir":"sdlc-harness/","githooksDir":"githooks","layers":[],"commands":{"depInstall":"printf x > deps.marker"}}' > "$d/harness.config.json"
88
+ # mkdir -p "$d/scripts/lib" # copy this script + lib/ there
89
+ #
90
+ # ordinary bash "$d/scripts/setup-worktree.sh"
91
+ # -> exit 0; "$d/deps.marker" exists; the build step prints its
92
+ # skip line; `git -C "$d" config --worktree core.hooksPath`
93
+ # is "$d/githooks"
94
+ # no hook file rm "$d/githooks/pre-push"; re-run -> exit 0 with a skip line
95
+ # env symlink printf 'K=v\n' > "$(git -C "$d" worktree list --porcelain |
96
+ # head -1 | sed 's/^worktree //')/.env"
97
+ # -> a sibling working copy's .env is a SYMLINK to that file;
98
+ # one that already has its own .env is left untouched
99
+ # unresolvable printf 'x' > "$d/harness.config.json"; re-run -> exit 1,
100
+ # nothing installed and no git configuration written
101
+
102
+ set -uo pipefail
103
+
104
+ hr_lib="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/harness-run-lib.sh"
105
+ if [ ! -r "$hr_lib" ]; then
106
+ echo "setup-worktree.sh: cannot read '$hr_lib' — refusing to bootstrap" >&2
107
+ exit 1
108
+ fi
109
+ # shellcheck source=lib/harness-run-lib.sh
110
+ . "$hr_lib"
111
+
112
+ # The checkout being bootstrapped is where THIS FILE lives — never $PWD, which
113
+ # belongs to whoever invoked it.
114
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
115
+ top="$(hr_repo_root "$script_dir")" || top=""
116
+ if [ -z "$top" ]; then
117
+ echo "setup-worktree.sh: '$script_dir' is not inside a git repository — refusing to bootstrap" >&2
118
+ exit 1
119
+ fi
120
+
121
+ # Warm the library's cache once, unsubstituted, and refuse here rather than
122
+ # letting each reader below fail separately with its own message.
123
+ cfg_status=0
124
+ hr_config_load "$top" || cfg_status=$?
125
+ if [ "$cfg_status" -ne 0 ]; then
126
+ echo "setup-worktree.sh: cannot resolve '$top/harness.config.json' — refusing to bootstrap" >&2
127
+ echo " (no configuration there, invalid JSON, more than one document, no defaultBranch, or jq missing/older than 1.5)" >&2
128
+ exit 1
129
+ fi
130
+
131
+ # The main checkout is where every machine-local file has its single source: a
132
+ # working copy created by git carries only tracked content.
133
+ main_repo="$(hr_main_repo "$top")" || main_repo=""
134
+ [ -n "$main_repo" ] || main_repo="$top"
135
+
136
+ status=0
137
+
138
+ # --- 1. Version manager, if this machine uses one. -------------------------
139
+ vm_init="${HARNESS_VERSION_MANAGER_INIT:-}"
140
+ if [ -n "$vm_init" ] && [ -r "$vm_init" ]; then
141
+ echo "setup-worktree.sh: sourcing version-manager init '$vm_init'"
142
+ # shellcheck source=/dev/null
143
+ . "$vm_init" || echo "setup-worktree.sh: version-manager init returned non-zero — continuing" >&2
144
+ fi
145
+
146
+ # --- 2. The machine-local client env file. ---------------------------------
147
+ # `appDir` is repo-relative and defaults to `.`; joining it as `<root>/.` would
148
+ # work but reads badly in every message below, so normalize it once.
149
+ app_dir="$(hr_app_dir "$top")" || app_dir="."
150
+ if [ "$app_dir" = "." ]; then
151
+ app_here="$top"
152
+ app_main="$main_repo"
153
+ else
154
+ app_here="$top/$app_dir"
155
+ app_main="$main_repo/$app_dir"
156
+ fi
157
+
158
+ if [ -e "$app_here/.env" ]; then
159
+ : # this checkout has its own; never overwrite it
160
+ elif [ -f "$app_main/.env" ]; then
161
+ if ln -s "$app_main/.env" "$app_here/.env"; then
162
+ echo "setup-worktree.sh: linked $app_here/.env -> $app_main/.env"
163
+ else
164
+ echo "setup-worktree.sh: could not link $app_here/.env — continuing" >&2
165
+ fi
166
+ fi
167
+
168
+ # --- 3. The test-account credentials, linked and never copied. -------------
169
+ qa_creds="$(hr_qa_creds_path "$top")" || qa_creds=""
170
+ if [ -n "$qa_creds" ]; then
171
+ if [ ! -e "$top/$qa_creds" ] && [ -f "$main_repo/$qa_creds" ]; then
172
+ mkdir -p "$(dirname "$top/$qa_creds")"
173
+ if ln -s "$main_repo/$qa_creds" "$top/$qa_creds"; then
174
+ echo "setup-worktree.sh: linked $top/$qa_creds -> $main_repo/$qa_creds"
175
+ else
176
+ echo "setup-worktree.sh: could not link $top/$qa_creds — continuing" >&2
177
+ fi
178
+ fi
179
+ fi
180
+
181
+ # --- 4. The pre-push backstop, for THIS working copy. ----------------------
182
+ # All working copies of a repository share one `.git/config`, so a plain
183
+ # `git config core.hooksPath` would write a single value every one of them
184
+ # reads — while each has its own committed hooks directory at a DIFFERENT
185
+ # absolute path. The value written here is therefore ABSOLUTE and
186
+ # WORKTREE-SCOPED, so each working copy resolves to its own tracked hook.
187
+ # Idempotent: both the `config` writes and the `chmod` are no-ops on a re-run.
188
+ hooks_rel="$(hr_githooks_dir "$top")" || hooks_rel=""
189
+ hooks_dir="$top/$hooks_rel"
190
+ if [ -n "$hooks_rel" ] && [ -f "$hooks_dir/pre-push" ]; then
191
+ chmod +x "$hooks_dir/pre-push" || true
192
+ # NOTE: `extensions.worktreeConfig true` is a one-way, REPOSITORY-GLOBAL
193
+ # switch — once enabled it changes config resolution for EVERY working copy
194
+ # sharing this `.git`, including older ones that have not re-run this script.
195
+ # It is safe here because `core.hooksPath` is the only worktree-scoped value
196
+ # and every active working copy re-runs this script; keep that invariant if
197
+ # you add another `--worktree` setting.
198
+ if git -C "$top" config extensions.worktreeConfig true &&
199
+ git -C "$top" config --worktree core.hooksPath "$hooks_dir"; then
200
+ echo "setup-worktree.sh: pre-push hook installed (core.hooksPath -> $hooks_dir)"
201
+ else
202
+ echo "setup-worktree.sh: could not point core.hooksPath at $hooks_dir — this working copy has no pre-push backstop" >&2
203
+ status=1
204
+ fi
205
+ else
206
+ echo "setup-worktree.sh: no pre-push hook at $hooks_dir — skipping hook install" >&2
207
+ fi
208
+
209
+ # --- 5. Dependencies, then the build. --------------------------------------
210
+ # Run one configured command from the repository root. `commands.*` holds a raw
211
+ # command LINE (the wrapper form is only used for the keys that have wrappers),
212
+ # so it is evaluated — in a subshell, so the `cd` and anything the command sets
213
+ # stay inside it, and in THIS shell's `eval` rather than a fresh `bash -c` so a
214
+ # version manager's shell function from step 1 is still in scope.
215
+ run_configured() {
216
+ local key="$1" label="$2" cmd cmd_status
217
+ cmd="$(hr_command "$top" "$key")"
218
+ cmd_status=$?
219
+ # The unresolvable case is tested FIRST: it also prints nothing, so folding it
220
+ # into the empty-value test would report a configuration this script could not
221
+ # read as an ordinary project with no such step.
222
+ if [ "$cmd_status" -eq 2 ]; then
223
+ echo "setup-worktree.sh: cannot resolve commands.$key — skipping $label" >&2
224
+ return 1
225
+ fi
226
+ if [ "$cmd_status" -ne 0 ] || [ -z "$cmd" ]; then
227
+ echo "setup-worktree.sh: commands.$key is not set — skipping $label"
228
+ return 0
229
+ fi
230
+ echo "setup-worktree.sh: $label: $cmd"
231
+ (
232
+ cd "$top" || exit 1
233
+ eval "$cmd"
234
+ )
235
+ cmd_status=$?
236
+ if [ "$cmd_status" -ne 0 ]; then
237
+ echo "setup-worktree.sh: $label failed (exit $cmd_status)" >&2
238
+ return 1
239
+ fi
240
+ return 0
241
+ }
242
+
243
+ deps_ok=1
244
+ if ! run_configured depInstall "dependency install"; then
245
+ deps_ok=0
246
+ status=1
247
+ fi
248
+
249
+ if [ "$deps_ok" -eq 1 ]; then
250
+ if ! run_configured build "build"; then
251
+ status=1
252
+ fi
253
+ else
254
+ echo "setup-worktree.sh: skipping build — the dependency install did not succeed" >&2
255
+ fi
256
+
257
+ if [ "$status" -eq 0 ]; then
258
+ echo "setup-worktree.sh: bootstrap complete for $top"
259
+ else
260
+ echo "setup-worktree.sh: bootstrap finished with failures for $top (see above)" >&2
261
+ fi
262
+ exit "$status"
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env bash
2
+ # Written by `autonomous-sdlc-harness init`, and yours from there on: edit it freely, a re-run
3
+ # keeps your copy. It is what `commands.devServer` invokes: it starts the development server in
4
+ # the background, from the repository root, and prints the pid and the log path a caller needs.
5
+ #
6
+ # Usage: start-dev-server.sh [port] [argument...]
7
+ # The port is optional and must be numeric when it is given; anything else is a usage error, so a
8
+ # mistyped port can never be passed through as a flag. It reaches the command on one channel only,
9
+ # exported as HARNESS_DEV_SERVER_PORT: it is shifted off before the remaining arguments are
10
+ # forwarded, so the raw command line below is never handed the port positionally. A server that
11
+ # takes its port only as an argument has to be given that variable on the raw line below.
12
+ #
13
+ # This wrapper may run a build to completion before it launches, on the pre-start line below; that
14
+ # line is yours to edit like every other, and `:` there means there is nothing to run.
15
+ #
16
+ # Nothing here inspects the port. A stack whose server silently falls through to port+1 when the
17
+ # port is taken has to be told not to, and this wrapper is where you tell it: add its strict-port
18
+ # flag to the raw command line below, or make the command that line runs fail on a taken port. A
19
+ # QA run that quietly moved to another port drives a different run's server.
20
+
21
+ # Deliberately no `-e`, for the same reason as the other wrappers. No captured status either:
22
+ # a development server does not exit while it is doing its job.
23
+ set -uo pipefail
24
+
25
+ # Exit with a diagnostic and no pid line: nothing is running, so there is nothing to report. A
26
+ # function rather than an inline `echo … && exit 1`, because the command line below is handed the
27
+ # caller's arguments and `exit` refuses extra ones — `init` writes a call to it as that line when
28
+ # no command line resolved.
29
+ harness_fail() {
30
+ echo "$1" >&2
31
+ exit 1
32
+ }
33
+
34
+ port="${1:-}"
35
+ if [ -n "$port" ]; then
36
+ case "$port" in
37
+ *[!0-9]*)
38
+ harness_fail "{{name}}: usage: start-dev-server.sh [port] (port must be numeric, got '${port}')"
39
+ ;;
40
+ esac
41
+ shift
42
+ export HARNESS_DEV_SERVER_PORT="$port"
43
+ fi
44
+
45
+ # Anchor to the repository root, derived from this script's own location and never from the
46
+ # caller's directory: `commands.*` are command lines run from the repository root and every path
47
+ # in `harness.config.json` is repo-relative. `git -C` on the script's own directory answers with
48
+ # the checkout this copy belongs to, so all three forms the permission profile emits — the
49
+ # repo-relative one, its absolute twin and a sibling worktree's own copy — run at the root of the
50
+ # checkout they were invoked out of, whatever directory the caller stood in.
51
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
52
+ repo_root="$(git -C "$script_dir" rev-parse --show-toplevel 2>/dev/null)"
53
+ if [ -z "$repo_root" ] || ! cd "$repo_root"; then
54
+ harness_fail "{{name}}: could not resolve a repository root from '${script_dir}' (is git on PATH?)"
55
+ fi
56
+
57
+ # A step that has to finish before the server is launched — for a server that serves what was last
58
+ # compiled, the project's own build. Synchronous and above the launch deliberately: the line below
59
+ # is backgrounded and its `$!` is what a caller kills, and a build chained into it with `&&` would
60
+ # make that pid a subshell the kill does not reach past. `:` when there is nothing to run.
61
+ {{prestart}} || harness_fail "{{name}}: the pre-start step failed, so the server was not launched"
62
+
63
+ # $TMPDIR ends in a trailing slash on macOS; strip it so the path carries no doubled separator.
64
+ # With no port the log is the `-default` one, which is also the only case two runs can share.
65
+ tmp_dir="${TMPDIR:-/tmp}"
66
+ tmp_dir="${tmp_dir%/}"
67
+ log_file="${tmp_dir}/harness-dev-server-${port:-default}.log"
68
+
69
+ # Backgrounded rather than `exec`ed, which is the one behaviour a QA phase cannot do without: it
70
+ # needs this shell back to poll the port and it needs a pid of its own to stop the server with,
71
+ # and it needs somewhere to read a startup failure from — which is why both streams go to the log.
72
+ {{command}} "$@" >"$log_file" 2>&1 &
73
+ pid=$!
74
+
75
+ # The only failure this script reports: the launch itself did not happen. A server that started
76
+ # but is not answering yet is success — see the readiness note below.
77
+ #
78
+ # Liveness is the test, not `[ -z "$pid" ]`: `$!` is assigned the moment the job is backgrounded,
79
+ # before the command has been resolved, so it is set even for a command that does not exist and an
80
+ # emptiness test can never fire. The brief settle is what makes the liveness test mean anything —
81
+ # immediately after the `&` the child has not been reaped yet and `kill -0` succeeds for a command
82
+ # that was never found. It is NOT a readiness wait: readiness belongs to the poll named at the foot
83
+ # of this script, and this only has to outlast a command that dies on the spot. Lengthen it if your
84
+ # command forks through a launcher slow enough to be missed here.
85
+ sleep 0.3
86
+ if ! kill -0 "$pid" 2>/dev/null; then
87
+ harness_fail "{{name}}: the development server exited immediately — see $log_file"
88
+ fi
89
+
90
+ # The one wrapper with no PASS/FAIL verdict, because a long-running process has no verdict to
91
+ # give. It reports what a caller has to hold instead: the pid to check liveness and to stop, and
92
+ # the log to read a failed start out of.
93
+ echo "STARTING: {{name}} (pid $pid)"
94
+ echo "LOG: $log_file"
95
+
96
+ # Readiness is deliberately not probed here. The poll already ships as a plugin asset —
97
+ # `scripts/poll-dev-server.sh`, which the QA phase runs against the port once this returns — and a
98
+ # second implementation of it inside a wrapper an adopter edits is how the two come to disagree.
99
+ exit 0