autonomous-sdlc-harness 0.1.0 → 0.2.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.
- package/README.md +4 -3
- package/dist/commands/docs.js +219 -0
- package/dist/commands/docs.js.map +1 -0
- package/dist/commands/doctor.js +5 -5
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.js +138 -44
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/registry.js +2 -0
- package/dist/commands/registry.js.map +1 -1
- package/dist/config/check.js +25 -5
- package/dist/config/check.js.map +1 -1
- package/dist/config/model.js +18 -1
- package/dist/config/model.js.map +1 -1
- package/dist/core/layerGapRemedy.js +3 -2
- package/dist/core/layerGapRemedy.js.map +1 -1
- package/dist/core/pluginIdentity.js +33 -0
- package/dist/core/pluginIdentity.js.map +1 -0
- package/dist/core/report.js +9 -0
- package/dist/core/report.js.map +1 -1
- package/dist/core/writer.js +22 -5
- package/dist/core/writer.js.map +1 -1
- package/dist/detect/presets.js +35 -26
- package/dist/detect/presets.js.map +1 -1
- package/dist/detect/signals.js +13 -9
- package/dist/detect/signals.js.map +1 -1
- package/dist/doctor/checks.js +141 -11
- package/dist/doctor/checks.js.map +1 -1
- package/dist/generators/claudeContext.js +4 -5
- package/dist/generators/claudeContext.js.map +1 -1
- package/dist/generators/harnessConfig.js +13 -5
- package/dist/generators/harnessConfig.js.map +1 -1
- package/dist/generators/outerLoopScripts.js +13 -0
- package/dist/generators/outerLoopScripts.js.map +1 -1
- package/dist/generators/permissionProfile.js +51 -9
- package/dist/generators/permissionProfile.js.map +1 -1
- package/dist/generators/projectSettings.js +5 -15
- package/dist/generators/projectSettings.js.map +1 -1
- package/dist/generators/repoRoot.js +130 -23
- package/dist/generators/repoRoot.js.map +1 -1
- package/dist/generators/scripts.js +4 -1
- package/dist/generators/scripts.js.map +1 -1
- package/dist/machine/paths.js +16 -4
- package/dist/machine/paths.js.map +1 -1
- package/dist/machine/plugins.js +2 -1
- package/dist/machine/plugins.js.map +1 -1
- package/dist/retrieval/chunk.js +158 -0
- package/dist/retrieval/chunk.js.map +1 -0
- package/dist/retrieval/corpus.js +75 -0
- package/dist/retrieval/corpus.js.map +1 -0
- package/dist/retrieval/models.js +175 -0
- package/dist/retrieval/models.js.map +1 -0
- package/dist/retrieval/queryLog.js +68 -0
- package/dist/retrieval/queryLog.js.map +1 -0
- package/dist/retrieval/refresh.js +55 -0
- package/dist/retrieval/refresh.js.map +1 -0
- package/dist/retrieval/runtime.js +171 -0
- package/dist/retrieval/runtime.js.map +1 -0
- package/dist/retrieval/search.js +118 -0
- package/dist/retrieval/search.js.map +1 -0
- package/dist/retrieval/server.js +197 -0
- package/dist/retrieval/server.js.map +1 -0
- package/dist/retrieval/session.js +41 -0
- package/dist/retrieval/session.js.map +1 -0
- package/dist/retrieval/setup.js +120 -0
- package/dist/retrieval/setup.js.map +1 -0
- package/dist/retrieval/store.js +170 -0
- package/dist/retrieval/store.js.map +1 -0
- package/package.json +22 -3
- package/templates/claude/CLAUDE.md +4 -4
- package/templates/claude/README.md +3 -1
- package/templates/claude/settings.autonomous.json +1 -1
- package/templates/claude/settings.autonomous.retrieval.json +9 -0
- package/templates/repo/README.md +2 -0
- package/templates/repo/gitignore +1 -0
- package/templates/repo/gitignore.retrieval +2 -0
- package/templates/repo/mcp.retrieval.json +11 -0
- package/templates/scripts/README.md +1 -1
- package/templates/scripts/autonomous-notify.sh +10 -4
- package/templates/scripts/autonomous-watcher.sh +336 -110
- package/templates/scripts/cleanup-merged-worktrees.sh +126 -8
- package/templates/scripts/docs-search-server.sh +64 -0
- package/templates/scripts/lib/harness-run-lib.sh +19 -3
- package/templates/scripts/restart-watcher.sh +4 -3
- package/templates/state-dir/business_parity_reviews/README.md +1 -1
- package/templates/state-dir/clarification_digests/README.md +1 -1
- package/templates/state-dir/clarifications/README.md +4 -4
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
# checkout's own directory;
|
|
38
38
|
# * a branch with an ACTIVE RUN — one whose record in the run registry
|
|
39
39
|
# (`<state_dir>/autonomous_logs/registry.json`, shaped
|
|
40
|
-
# `{"runs": {"<branch>": {"status": …}}}`) says `running`, `parked
|
|
41
|
-
# `paused`. All
|
|
42
|
-
# parked run is waiting for a clarification answer
|
|
43
|
-
# RESUME sentinel, and `worktree remove --force` would discard the pause
|
|
40
|
+
# `{"runs": {"<branch>": {"status": …}}}`) says `running`, `parked`,
|
|
41
|
+
# `park_loop` or `paused`. All four own a working copy the watcher will come
|
|
42
|
+
# back to: a parked run is waiting for a clarification answer, a `park_loop`
|
|
43
|
+
# one for an operator to clear it, and a paused one for a RESUME sentinel, and `worktree remove --force` would discard the pause
|
|
44
44
|
# note the resumed engine is pointed at.
|
|
45
45
|
#
|
|
46
46
|
# CAVEAT: `: gone` + force-delete also catches an ABANDONED branch whose remote
|
|
@@ -89,6 +89,13 @@
|
|
|
89
89
|
# Usage: cleanup-merged-worktrees.sh [--dry-run]
|
|
90
90
|
# --dry-run report what would be removed; remove nothing
|
|
91
91
|
#
|
|
92
|
+
# Environment:
|
|
93
|
+
# HARNESS_FETCH_TIMEOUT seconds to allow the `fetch --prune` before it is
|
|
94
|
+
# killed and the round skipped (default 60, clamped to
|
|
95
|
+
# 1..3600; anything else falls back to the default).
|
|
96
|
+
# The bound exists because this sweep runs inside the
|
|
97
|
+
# watcher's loop — see the paragraph on it below.
|
|
98
|
+
#
|
|
92
99
|
# Exit map a caller can switch on:
|
|
93
100
|
#
|
|
94
101
|
# 0 the sweep ran, or refused and deleted nothing (the message says which)
|
|
@@ -120,7 +127,7 @@
|
|
|
120
127
|
# printf '%s' '{"runs":{"feat_gone":{"status":"running"}}}' \
|
|
121
128
|
# > "$d/sdlc-harness/autonomous_logs/registry.json"
|
|
122
129
|
# -> the skip line for feat_gone; nothing deleted (same with
|
|
123
|
-
# "parked" and "paused")
|
|
130
|
+
# "parked", "park_loop" and "paused")
|
|
124
131
|
# unresolvable printf 'x' > "$d/harness.config.json"
|
|
125
132
|
# -> one line, exit 0, nothing deleted
|
|
126
133
|
# offline git -C "$d" remote set-url origin /nonexistent.git
|
|
@@ -206,17 +213,128 @@ if [ -e "$registry" ]; then
|
|
|
206
213
|
# document without it is a registry this cannot enumerate, so `jq` fails and
|
|
207
214
|
# the refusal below fires — which is the point: reading such a file as "no
|
|
208
215
|
# active runs" would delete a branch a run is still working on.
|
|
209
|
-
if ! active="$(jq -r '.runs | to_entries[] | select(.value.status == "running" or .value.status == "parked" or .value.status == "paused") | .key' "$registry" 2>/dev/null)"; then
|
|
216
|
+
if ! active="$(jq -r '.runs | to_entries[] | select(.value.status == "running" or .value.status == "parked" or .value.status == "park_loop" or .value.status == "paused") | .key' "$registry" 2>/dev/null)"; then
|
|
210
217
|
echo "cleanup-merged-worktrees.sh: '$registry' could not be read as a run registry — refusing to delete anything"
|
|
211
218
|
exit 0
|
|
212
219
|
fi
|
|
213
220
|
fi
|
|
214
221
|
|
|
222
|
+
# Run one command with a WALL-CLOCK CEILING, and report a timeout as 124 the way
|
|
223
|
+
# GNU `timeout` does. Written out here rather than taken from `timeout`, which is
|
|
224
|
+
# not on a stock macOS, nor from lib/harness-run-lib.sh, which an adopter's
|
|
225
|
+
# `scriptsDir` may be carrying at an older revision than this file — a sweep that
|
|
226
|
+
# dies on an unbound function is a worse failure than the hang it closes.
|
|
227
|
+
#
|
|
228
|
+
# The command runs in the BACKGROUND and is polled, because `wait` alone cannot be
|
|
229
|
+
# bounded in POSIX shell. On expiry the child gets a TERM, then a KILL two seconds
|
|
230
|
+
# later if it is still there, so it is asked before it is forced.
|
|
231
|
+
#
|
|
232
|
+
# THE SIGNAL GOES TO THE PROCESS GROUP, NOT THE CHILD — `kill -TERM -$pid`, with
|
|
233
|
+
# the negative pid — and `set -m` is what makes that possible, by giving the
|
|
234
|
+
# background job a process group of its own whose id is that pid. Signalling the
|
|
235
|
+
# child alone is not enough and was measured not to be: `git fetch` spawns the
|
|
236
|
+
# transport (`ssh`, `git-remote-https`) as its own child, and that grandchild
|
|
237
|
+
# INHERITS THIS SCRIPT'S STDOUT. Kill only git and the transport survives, holding
|
|
238
|
+
# the write end of the pipe the watcher is reading, so the watcher blocks on a
|
|
239
|
+
# sweep that already printed its verdict — the original hang, moved rather than
|
|
240
|
+
# closed. Against a fetch stalled for 3 seconds under a 3-second ceiling, the
|
|
241
|
+
# child-only version returned its message on time and did not let the script exit
|
|
242
|
+
# for 144 seconds; the group kill ends it in the 2 the escalation costs.
|
|
243
|
+
#
|
|
244
|
+
# Job control is restored to whatever it was, since a script that leaves `-m` on
|
|
245
|
+
# behind it changes how every later background command here reports.
|
|
246
|
+
#
|
|
247
|
+
# run_bounded <seconds> <command> [args…]
|
|
248
|
+
# NOT GROUPED WHEN A PERSON IS WATCHING. `set -m` puts the child in a process
|
|
249
|
+
# group that is not the terminal's foreground one, so a child that reads the
|
|
250
|
+
# CONTROLLING TERMINAL is stopped with SIGTTIN instead of being answered — and
|
|
251
|
+
# `ssh` reads a key passphrase from `/dev/tty` directly, which `GIT_TERMINAL_PROMPT=0`
|
|
252
|
+
# does not cover because it is not git asking. `kill -0` keeps succeeding on a
|
|
253
|
+
# stopped process, so a by-hand sweep on a passphrase-protected key would burn the
|
|
254
|
+
# whole ceiling and report a network failure that never happened. The header's
|
|
255
|
+
# "WHO RUNS IT" names a person by hand as a supported caller, so that path keeps
|
|
256
|
+
# the child in this script's own group and signals the child alone: the pipe a
|
|
257
|
+
# surviving transport could hold is a terminal the person is looking at, and they
|
|
258
|
+
# can end it themselves. The watcher is daemonised with no controlling terminal,
|
|
259
|
+
# so it takes the grouped path, which is the one the orphaned-transport hang
|
|
260
|
+
# needed.
|
|
261
|
+
run_bounded() {
|
|
262
|
+
local limit="$1" pid waited=0 job_control=0 grouped=1
|
|
263
|
+
shift
|
|
264
|
+
if { : > /dev/tty; } 2>/dev/null; then grouped=0; fi
|
|
265
|
+
case "$-" in *m*) job_control=1 ;; esac
|
|
266
|
+
[ "$grouped" -eq 1 ] && set -m
|
|
267
|
+
"$@" &
|
|
268
|
+
pid=$!
|
|
269
|
+
[ "$job_control" -eq 1 ] || set +m
|
|
270
|
+
while kill -0 "$pid" 2>/dev/null; do
|
|
271
|
+
if [ "$waited" -ge "$limit" ]; then
|
|
272
|
+
if [ "$grouped" -eq 1 ]; then
|
|
273
|
+
kill -TERM "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || :
|
|
274
|
+
sleep 2
|
|
275
|
+
# Only while the group is still there. Once it is gone the negative form
|
|
276
|
+
# fails, and retrying the bare pid would aim an unconditional SIGKILL at a
|
|
277
|
+
# number the kernel may already have handed to somebody else.
|
|
278
|
+
kill -0 "-$pid" 2>/dev/null && { kill -KILL "-$pid" 2>/dev/null || :; }
|
|
279
|
+
else
|
|
280
|
+
kill -TERM "$pid" 2>/dev/null || :
|
|
281
|
+
sleep 2
|
|
282
|
+
kill -0 "$pid" 2>/dev/null && { kill -KILL "$pid" 2>/dev/null || :; }
|
|
283
|
+
fi
|
|
284
|
+
wait "$pid" 2>/dev/null || :
|
|
285
|
+
return 124
|
|
286
|
+
fi
|
|
287
|
+
sleep 1
|
|
288
|
+
waited=$((waited + 1))
|
|
289
|
+
done
|
|
290
|
+
wait "$pid"
|
|
291
|
+
}
|
|
292
|
+
|
|
215
293
|
# Refresh remote-tracking refs so a deleted upstream becomes `[gone]`. A failed
|
|
216
294
|
# fetch (offline, or an unreachable remote) skips this round rather than sweeping
|
|
217
295
|
# on stale tracking information, which would report every branch as gone.
|
|
218
|
-
|
|
219
|
-
|
|
296
|
+
#
|
|
297
|
+
# BOUNDED, BECAUSE THIS SWEEP RUNS INSIDE THE WATCHER'S LOOP.
|
|
298
|
+
# `autonomous-watcher.sh` calls this script synchronously, so a fetch that HANGS
|
|
299
|
+
# rather than fails stalls the whole loop — no inbox pass, no drop picked up,
|
|
300
|
+
# nothing written to `watcher.log`, and the watcher still alive and healthy-looking
|
|
301
|
+
# in `ps`. The paragraph above anticipated a fetch that FAILS and skips the round;
|
|
302
|
+
# a stale TCP connection left behind by a network change or a laptop sleep never
|
|
303
|
+
# fails, and an unbounded `git fetch` sits in it indefinitely. The ceiling turns
|
|
304
|
+
# that second mode into the first, which this script already handles.
|
|
305
|
+
#
|
|
306
|
+
# `GIT_TERMINAL_PROMPT=0` closes the other hang of the same shape: a credential
|
|
307
|
+
# prompt, on a remote whose authentication has lapsed, blocks on a terminal that
|
|
308
|
+
# no unattended run is watching.
|
|
309
|
+
#
|
|
310
|
+
# SIXTY SECONDS, AND OVERRIDABLE. The default is generous next to a healthy fetch
|
|
311
|
+
# of a repository this size and short next to `CLEANUP_INTERVAL_SECS` (300), the
|
|
312
|
+
# interval the watcher runs this sweep on, so a hung remote costs a fraction of
|
|
313
|
+
# that round rather than the loop. It is NOT short next to `POLL_INTERVAL_SECS`
|
|
314
|
+
# (15): a sweep that spends the whole ceiling does hold the loop for it, which is
|
|
315
|
+
# the price of bounding the fetch at all and the reason the ceiling is not larger.
|
|
316
|
+
# `HARNESS_FETCH_TIMEOUT` raises it for an adopter on a link where 60 s is not
|
|
317
|
+
# generous, and lowers it for the test that drives this ceiling.
|
|
318
|
+
#
|
|
319
|
+
# VALIDATED BY VALUE, NOT BY SHAPE. An earlier form of this clause tested the
|
|
320
|
+
# string for non-digits, which let through two values the sentence below calls
|
|
321
|
+
# impossible. A 19-digit number is all digits, and `[ "$waited" -ge <huge> ]` does
|
|
322
|
+
# not compare false — it ERRORS, and a non-zero status reads as false, so the
|
|
323
|
+
# timeout branch never fires and the poll loop runs forever: unbounded, which is
|
|
324
|
+
# the one thing this knob must not express. `00` is all digits and is not the
|
|
325
|
+
# string `0`, and `[ 0 -ge 00 ]` is true on the first iteration, so every sweep
|
|
326
|
+
# kills its own fetch before it starts, blames the network, and never cleans up
|
|
327
|
+
# another branch. Comparing the VALUE closes both: `[ ]` returns non-zero for an
|
|
328
|
+
# unparseable or out-of-range string as readily as for a number that fails the
|
|
329
|
+
# test, and the default is what survives either way. The upper clamp is what keeps
|
|
330
|
+
# a fat-fingered value from expressing "wait forever" in seconds.
|
|
331
|
+
fetch_timeout="${HARNESS_FETCH_TIMEOUT:-60}"
|
|
332
|
+
if ! { [ "$fetch_timeout" -ge 1 ] && [ "$fetch_timeout" -le 3600 ]; } 2>/dev/null; then
|
|
333
|
+
fetch_timeout=60
|
|
334
|
+
fi
|
|
335
|
+
|
|
336
|
+
if ! GIT_TERMINAL_PROMPT=0 run_bounded "$fetch_timeout" git -C "$main_repo" fetch --prune --quiet 2>/dev/null; then
|
|
337
|
+
echo "cleanup-merged-worktrees.sh: fetch --prune failed (offline, unreachable or timed out); skipping this round"
|
|
220
338
|
exit 0
|
|
221
339
|
fi
|
|
222
340
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# docs-search-server.sh — start the docs-retrieval MCP server for the checkout
|
|
3
|
+
# this script sits in, by `exec`ing the machine-shared retrieval runtime's
|
|
4
|
+
# `docs serve`.
|
|
5
|
+
#
|
|
6
|
+
# WHO RUNS IT. The agent runner, from the `harness-docs` entry `init` writes into
|
|
7
|
+
# `.mcp.json` when `docs.retrieval` is on — never a dispatched agent's Bash call,
|
|
8
|
+
# so its row in `cli/src/generators/outerLoopScripts.ts` is
|
|
9
|
+
# `agentInvocable: false` and the generated permission profile carries no entry
|
|
10
|
+
# for it.
|
|
11
|
+
#
|
|
12
|
+
# STDOUT BELONGS TO THE MCP TRANSPORT. Nothing here may print to stdout before
|
|
13
|
+
# the `exec`: a stray byte there is a malformed frame for the client. Every
|
|
14
|
+
# diagnostic goes to stderr.
|
|
15
|
+
#
|
|
16
|
+
# THE RUNTIME IS THE ONLY THING THIS RUNS, with no fallback to any other
|
|
17
|
+
# installation: a committed `.mcp.json` cannot know where an adopter's own CLI
|
|
18
|
+
# lives. `doctor`'s `retrieval-dependencies` check and `init`'s install both key
|
|
19
|
+
# on the same entry file this script tests. `PATH` is settled through the
|
|
20
|
+
# library's fallback list before the `exec`, because the starter is the agent
|
|
21
|
+
# runner rather than a login shell.
|
|
22
|
+
#
|
|
23
|
+
# MIRRORS — a change to any owner below is an edit here too:
|
|
24
|
+
# `retrieval/runtime` and `node_modules/autonomous-sdlc-harness/dist/cli.js`
|
|
25
|
+
# mirror `RETRIEVAL_CACHE_DIRNAME`, `RETRIEVAL_RUNTIME_DIRNAME` and
|
|
26
|
+
# `RUNTIME_CLI_RELATIVE` in `cli/src/retrieval/runtime.ts`;
|
|
27
|
+
# `hr_cache_dir` mirrors `machineCacheDir()` in `cli/src/machine/paths.ts`.
|
|
28
|
+
#
|
|
29
|
+
# Exit contract:
|
|
30
|
+
# 1 no runtime entry at the resolved path, no cache directory to resolve
|
|
31
|
+
# it under, or no repository at this script's location; stderr names
|
|
32
|
+
# which
|
|
33
|
+
# N otherwise `docs serve`'s own status, through `exec`; a failure to
|
|
34
|
+
# source the library exits non-zero before that, on stderr only
|
|
35
|
+
|
|
36
|
+
set -euo pipefail
|
|
37
|
+
|
|
38
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
39
|
+
# shellcheck source=lib/harness-run-lib.sh
|
|
40
|
+
. "$script_dir/lib/harness-run-lib.sh"
|
|
41
|
+
|
|
42
|
+
# The agent runner starts this script, and a runner launched from a desktop session carries a
|
|
43
|
+
# minimal PATH. The library appends the usual locations without promoting any of them, which is the
|
|
44
|
+
# same bootstrap autonomous-watcher.sh runs for the same reason.
|
|
45
|
+
PATH="$(hr_path_with_fallbacks)"
|
|
46
|
+
export PATH
|
|
47
|
+
|
|
48
|
+
if ! root="$(hr_repo_root "$script_dir")"; then
|
|
49
|
+
echo "docs-search-server: $script_dir is not inside a git repository, so there is no checkout to serve" >&2
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
if ! cache_dir="$(hr_cache_dir)"; then
|
|
54
|
+
echo "docs-search-server: neither XDG_CACHE_HOME nor HOME is set, so the retrieval runtime cannot be located" >&2
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
entry="$cache_dir/retrieval/runtime/node_modules/autonomous-sdlc-harness/dist/cli.js"
|
|
58
|
+
|
|
59
|
+
if [ ! -f "$entry" ]; then
|
|
60
|
+
echo "docs-search-server: no retrieval runtime at $entry; run \`npx autonomous-sdlc-harness init\` in a repository with docs.retrieval on" >&2
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
exec node "$entry" docs serve --cwd "$root"
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
# an `hr_lane_*` function still gets a library that only reads. The lane's
|
|
53
53
|
# ceilings are the only environment values here that carry policy, because the
|
|
54
54
|
# lane is machine-scoped and has no configuration key to carry them; each is
|
|
55
|
-
# named where it is used. `XDG_STATE_HOME`, `XDG_CONFIG_HOME`, `
|
|
56
|
-
# are also read, as location anchors only, and `PATH` is read by
|
|
55
|
+
# named where it is used. `XDG_STATE_HOME`, `XDG_CONFIG_HOME`, `XDG_CACHE_HOME`,
|
|
56
|
+
# `HOME` and `PWD` are also read, as location anchors only, and `PATH` is read by
|
|
57
57
|
# `hr_path_with_fallbacks` alone — as that function's input, which it prints back
|
|
58
58
|
# transformed and never assigns.
|
|
59
59
|
#
|
|
@@ -196,6 +196,8 @@
|
|
|
196
196
|
# anchors hr_main_repo "$root"; hr_work_root "$root"
|
|
197
197
|
# hr_worktree_dir "$root" feat/x; hr_repo_slug "$root"
|
|
198
198
|
# hr_state_path "$root" autonomous_logs/registry.json
|
|
199
|
+
# machine dirs ( XDG_CACHE_HOME= hr_cache_dir ) -> $HOME/.cache/autonomous-sdlc-harness
|
|
200
|
+
# ( XDG_CACHE_HOME=/x/ hr_cache_dir ) -> /x/autonomous-sdlc-harness
|
|
199
201
|
# the PATH policy run each in a SUBSHELL, so your own PATH is untouched:
|
|
200
202
|
# ( PATH="$HOME/.rbenv/shims:/usr/bin:/bin"
|
|
201
203
|
# hr_path_with_fallbacks )
|
|
@@ -966,6 +968,20 @@ hr_machine_config_dir() {
|
|
|
966
968
|
printf '%s/autonomous-sdlc-harness\n' "${base%/}"
|
|
967
969
|
}
|
|
968
970
|
|
|
971
|
+
# The machine-local cache directory, holding the shared docs-retrieval runtime.
|
|
972
|
+
# Mirrors `machineCacheDir()` in `cli/src/machine/paths.ts`: the variable when
|
|
973
|
+
# set and non-empty, else `$HOME/.cache`, one trailing slash stripped. Return 1
|
|
974
|
+
# when there is no home to anchor it to.
|
|
975
|
+
hr_cache_dir() {
|
|
976
|
+
local base="${XDG_CACHE_HOME-}"
|
|
977
|
+
[ -n "$base" ] || base="${HOME-}/.cache"
|
|
978
|
+
case "$base" in
|
|
979
|
+
/.cache) return 1 ;;
|
|
980
|
+
esac
|
|
981
|
+
[ -n "$base" ] || return 1
|
|
982
|
+
printf '%s/autonomous-sdlc-harness\n' "${base%/}"
|
|
983
|
+
}
|
|
984
|
+
|
|
969
985
|
# The push-notification credential files, in RESOLUTION ORDER, one per line and
|
|
970
986
|
# whether or not each exists — the caller sources the first that does:
|
|
971
987
|
#
|
|
@@ -1068,7 +1084,7 @@ hr_push_env_files() {
|
|
|
1068
1084
|
# THE THREE CEILINGS ARE THE ONLY ENVIRONMENT VALUES THAT CARRY POLICY HERE. The
|
|
1069
1085
|
# file's other environment reads are location anchors, not policy:
|
|
1070
1086
|
# `XDG_STATE_HOME` and `HOME` in `hr_lane_dir`, `XDG_CONFIG_HOME` and `HOME` in
|
|
1071
|
-
# `hr_machine_config_dir`, `PWD` in `hr_repo_root` and `hr_main_repo`. The
|
|
1087
|
+
# `hr_machine_config_dir`, `XDG_CACHE_HOME` and `HOME` in `hr_cache_dir`, `PWD` in `hr_repo_root` and `hr_main_repo`. The
|
|
1072
1088
|
# ceilings are machine-scoped policy with no configuration key:
|
|
1073
1089
|
#
|
|
1074
1090
|
# HR_LANE_STATE_MAX_AGE_SECS 21600 when a PUBLISHED RECORD THAT NAMED NO
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
#
|
|
53
53
|
# * the run registry (`<state_dir>/autonomous_logs/registry.json`, shaped
|
|
54
54
|
# `{"runs": {"<branch>": {"status": …}}}`) is the SOURCE OF TRUTH: a record
|
|
55
|
-
# whose status is `running` or `
|
|
55
|
+
# whose status is `running`, `parked` or `park_loop` is a run in flight;
|
|
56
56
|
# * a process probe for the agent binary — `${HARNESS_AGENT_CLI:-claude}`, the
|
|
57
57
|
# same variable the watcher launches through — is a BACKSTOP, for a run OF
|
|
58
58
|
# THIS PROJECT whose record has not been written yet or was written by a
|
|
@@ -119,7 +119,8 @@
|
|
|
119
119
|
#
|
|
120
120
|
# in flight printf '%s' '{"runs":{"feat_x":{"status":"running"}}}' > "$reg"
|
|
121
121
|
# run -> the in-flight report, exit 2, and no
|
|
122
|
-
# "$w/calls" at all (same for "parked"
|
|
122
|
+
# "$w/calls" at all (same for "parked"
|
|
123
|
+
# and "park_loop")
|
|
123
124
|
# forced run --force -> the same report plus the restarting line,
|
|
124
125
|
# and "$w/calls" holds `daemon stop` then
|
|
125
126
|
# `daemon install`, in that order
|
|
@@ -257,7 +258,7 @@ else
|
|
|
257
258
|
# document that has no such wrapper is a registry this cannot enumerate, so
|
|
258
259
|
# `jq` fails and the refusal below fires. Reading such a file as "no active
|
|
259
260
|
# runs" is the one misreading that costs a run.
|
|
260
|
-
elif ! active="$(jq -r '.runs | to_entries[] | select(.value.status == "running" or .value.status == "parked") | "\(.value.status) \(.key)"' "$registry" 2>/dev/null)"; then
|
|
261
|
+
elif ! active="$(jq -r '.runs | to_entries[] | select(.value.status == "running" or .value.status == "parked" or .value.status == "park_loop") | "\(.value.status) \(.key)"' "$registry" 2>/dev/null)"; then
|
|
261
262
|
active=""
|
|
262
263
|
unknown="'$registry' could not be read as a run registry (invalid JSON, or no .runs wrapper)"
|
|
263
264
|
fi
|
|
@@ -6,4 +6,4 @@ The reviewer reads the story index's `## Context` plus every per-task file and j
|
|
|
6
6
|
|
|
7
7
|
The directory exists only while `phases.parity` is `true` in `harness.config.json`: a project with no reference implementation to compare against has no parity gate and will not have it, and turning the phase on and re-running `init` creates it — an absent directory here is a configuration answer, not a missing artifact. While the phase is on, files appear one per failed gate and stop at convergence or at the fifth iteration, where the gate escalates with the latest path. Nothing supersedes an earlier file, and the whole directory is committed with the branch, since no ignore rule reaches it.
|
|
8
8
|
|
|
9
|
-
The mistake worth naming is expecting a plan to pass here on plausible prose. This gate is graded on **citations**: every constant, call name, wire field and boundary comparison the plan describes has to trace to
|
|
9
|
+
The mistake worth naming is expecting a plan to pass here on plausible prose. This gate is graded on **citations**: every constant, call name, wire field and boundary comparison the plan describes has to trace to an anchored source in the reference implementation (its path and symbol, never a line number in their place), and a behaviour the reference has that no per-task file covers is a Must Fix of the same grade as a wrong field. A plan that describes the right behaviour without saying where it came from is the ordinary way a round is lost here.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# clarification_digests/
|
|
2
2
|
|
|
3
|
-
One `<state_dir>/clarification_digests/<branch>.md` per branch, holding one block per
|
|
3
|
+
One `<state_dir>/clarification_digests/<branch>.md` per branch, holding one block per park the runs on that branch stopped on and had answered — one per question file, however many questions it raised: what was asked, the operator's ruling in the words it was given, and whatever that ruling settles beyond the questions that prompted it. One file per **branch**, appended to and never rewritten — each block is headed by the `question_<n>` index of the question file it was derived from, and a later round or a resumed session adds only the blocks whose key is not in it already. Blocks written before a park held several questions — one per one-question file — are left exactly as written, and stay uniquely keyed because that index is never reused.
|
|
4
4
|
|
|
5
5
|
Each block is written by the run's own orchestrator at the end of the run, from the question and answer files in `<state_dir>/clarifications/<branch>/` and its `answered/` archive. Those files are machine-local and go with the working copy — a working copy removed once its branch has merged takes every question and every answer with it, and **this file is the copy that survives**. Its readers are whoever comes to the branch afterwards: a reviewer asking why a decision was taken the way it was, and the human folding a ruling that outlives the branch into the rules the flow reads.
|
|
6
6
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# clarifications/
|
|
2
2
|
|
|
3
|
-
The park-and-ask channel: one question per
|
|
3
|
+
The park-and-ask channel: one question file per park at `<state_dir>/clarifications/<branch>/question_<n>.md`, holding every question that park raises as its own `## Q<k> — <decision needed>` section with `k` counted from 1, and one answer file for the whole park beside it as `answer_<n>.md`. Pairing is by the index `<n>`, which is 1-based and unique across the branch's directory **including its `answered/` archive** — a second park takes the next index above the highest `question_<n>.md` in either location, never merely the next free name at the top level and never overwriting the first, so an archived pair does not free its index for reuse and `answer_3.md` answers `question_3.md` and nothing else. One `<branch>` subdirectory holds one branch's exchange and is created with that branch's first park.
|
|
4
4
|
|
|
5
|
-
A question is written by the agent that reached
|
|
5
|
+
A question file is written by the agent that reached decisions it must not take alone: writing the file and ending the session is what parks the run. The answer is written by the harness's answer command on the operator's behalf, verbatim and beside the question file; it addresses each question by its `Q<k>` label, and a question it leaves unaddressed is asked again by the resumed run, in a new park file. The run daemon resumes the same engine in the same working copy only once every question file at the top level of the branch's directory has its answer, and the re-entering run is what consumes the answers — the daemon never answers a question itself. A park written before this layout, as several one-question files, is resumed by the same rule.
|
|
6
6
|
|
|
7
|
-
The
|
|
7
|
+
The answered pairs stay at the top level of the branch's directory while that resumed session runs, and once it has exited exactly the pairs that resume consumed move into `<branch>/answered/`, so an answer is still where the resumed run looks for it and an already-consumed one is never read a second time. A pair written while the resumed run is going was not consumed by it and stays at the top level for the next resume. Everything the runs write here is machine-local and gitignored; this README is the only committed file in **this** directory. What survives the working copy is the record: at the end of each run the branch's resolved parks are digested into `<state_dir>/clarification_digests/<branch>.md`, one committed block per park, keyed by that same index — what was asked and the operator's ruling in the words it was given. Reusing an index would make two parks indistinguishable there, which is the other reason it is never reused. That sibling directory's own README states the format. The harness's watcher documentation, which ships with it, states the same protocol from the daemon's side.
|
|
8
8
|
|
|
9
|
-
The mistake worth naming is tidying up by hand. Deleting a question, or archiving it into `answered/` before the run that parked on it has been resumed, does not unpark that run —
|
|
9
|
+
The mistake worth naming is tidying up by hand. Deleting a question file, or archiving it into `answered/` before the run that parked on it has been resumed, does not unpark that run — a question file left at the top level is the sole signal that classifies a run as parked, so removing it strands the run instead, waiting on an answer nothing will now deliver.
|