autonomous-sdlc-harness 0.2.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cli.js +0 -0
- package/dist/commands/docs.js +2 -1
- package/dist/commands/docs.js.map +1 -1
- package/dist/commands/doctor.js +56 -6
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.js +107 -20
- package/dist/commands/init.js.map +1 -1
- package/dist/config/check.js +11 -4
- package/dist/config/check.js.map +1 -1
- package/dist/config/model.js +25 -0
- package/dist/config/model.js.map +1 -1
- package/dist/core/defaultBranchPush.js +26 -0
- package/dist/core/defaultBranchPush.js.map +1 -0
- package/dist/core/git.js +61 -0
- package/dist/core/git.js.map +1 -1
- package/dist/core/paths.js +22 -2
- package/dist/core/paths.js.map +1 -1
- package/dist/core/prompt.js +6 -2
- package/dist/core/prompt.js.map +1 -1
- package/dist/core/writer.js +2 -0
- package/dist/core/writer.js.map +1 -1
- package/dist/doctor/checks.js +482 -73
- package/dist/doctor/checks.js.map +1 -1
- package/dist/generators/githubWorkflows.js +66 -0
- package/dist/generators/githubWorkflows.js.map +1 -0
- package/dist/generators/notifications.js +105 -19
- package/dist/generators/notifications.js.map +1 -1
- package/dist/generators/outerLoopScripts.js +17 -3
- package/dist/generators/outerLoopScripts.js.map +1 -1
- package/dist/generators/permissionProfile.js +141 -21
- package/dist/generators/permissionProfile.js.map +1 -1
- package/dist/generators/projectSettings.js +2 -1
- package/dist/generators/projectSettings.js.map +1 -1
- package/dist/generators/repoRoot.js +23 -4
- package/dist/generators/repoRoot.js.map +1 -1
- package/dist/generators/stateDir.js +9 -3
- package/dist/generators/stateDir.js.map +1 -1
- package/dist/machine/paths.js +4 -1
- package/dist/machine/paths.js.map +1 -1
- package/dist/remote/githubActions.js +86 -0
- package/dist/remote/githubActions.js.map +1 -0
- package/dist/retrieval/queryLog.js +6 -4
- package/dist/retrieval/queryLog.js.map +1 -1
- package/dist/retrieval/runtime.js +46 -33
- package/dist/retrieval/runtime.js.map +1 -1
- package/dist/retrieval/search.js +21 -10
- package/dist/retrieval/search.js.map +1 -1
- package/dist/retrieval/server.js +1 -1
- package/dist/retrieval/setup.js +2 -1
- package/dist/retrieval/setup.js.map +1 -1
- package/package.json +2 -2
- package/templates/README.md +3 -2
- package/templates/claude/context/conventions.md +1 -1
- package/templates/claude/context/layer.md +1 -1
- package/templates/claude/push-notify.env.example +7 -2
- package/templates/claude/settings.autonomous.json +1 -1
- package/templates/github/workflows/harness-resume.yml +124 -0
- package/templates/github/workflows/harness-run.yml +446 -0
- package/templates/repo/gitignore +11 -1
- package/templates/scripts/README.md +1 -1
- package/templates/scripts/autonomous-watcher.sh +1296 -169
- package/templates/scripts/flow-walker.sh +629 -0
- package/templates/scripts/flows/task_plan_writing.graph.json +192 -0
- package/templates/scripts/lib/flow-walker-gates.sh +165 -0
- package/templates/scripts/lib/harness-run-lib.sh +595 -18
- package/templates/scripts/remote-run.sh +1784 -0
- package/templates/scripts/restart-watcher.sh +21 -1
- package/templates/scripts/run-test-suite.sh +182 -0
- package/templates/scripts/scratch-run.sh +2 -1
- package/templates/state-dir/README-root.md +1 -1
- package/templates/state-dir/autonomous_logs/README.md +2 -2
- package/templates/state-dir/improvement_observations/README.md +2 -0
- package/templates/state-dir/scratch/README.md +1 -1
- package/templates/state-dir/test_fix_plan_reviews/README.md +9 -0
- package/templates/state-dir/test_fix_plans/README.md +9 -0
- package/templates/state-dir/test_fix_point_reviews/README.md +9 -0
- package/templates/state-dir/test_run_logs/README.md +11 -0
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# flow-walker.sh — the next step of a declared flow, read off its graph instead
|
|
3
|
+
# of worked out from prose: given the flow, the branch and the outcome of the
|
|
4
|
+
# node that just ran, print the next action in one fixed form.
|
|
5
|
+
#
|
|
6
|
+
# THE WALKER ROUTES; THE ORCHESTRATOR JUDGES. It owns the next node, the shared
|
|
7
|
+
# counter and its cap, both skip semantics, each reviewer's `iteration:`
|
|
8
|
+
# argument and the escalation payload, and it rejects an outcome the pending
|
|
9
|
+
# node does not declare. Which outcome a return carries, and what a binding
|
|
10
|
+
# resolves to, stay the orchestrator's.
|
|
11
|
+
#
|
|
12
|
+
# THE SAFETY CONTRACT IS DELIBERATELY NOT THE WALKER'S. The STOP check, the
|
|
13
|
+
# `.dispatch_counter` increment, the PAUSE check and the halt messages stay
|
|
14
|
+
# orchestrator steps, so the heartbeat carries the literal slot
|
|
15
|
+
# `(#<total_dispatches>)` for the orchestrator to fill. A binding is printed by
|
|
16
|
+
# NAME and never resolved; no dispatch prompt is composed — only the values the
|
|
17
|
+
# core's dispatch block lacks (`arg.iteration`, `arg.findings_file`) and the
|
|
18
|
+
# prompt variant.
|
|
19
|
+
#
|
|
20
|
+
# USAGE — each call one literal command from the repository root:
|
|
21
|
+
#
|
|
22
|
+
# bash <scripts_dir>/flow-walker.sh start --flow <flow> --branch <branch> [--entry <node>] [--skipped <ids|none>]
|
|
23
|
+
# bash <scripts_dir>/flow-walker.sh next --flow <flow> --branch <branch> --outcome <outcome> [--findings <path>] [--skipped <ids|none>]
|
|
24
|
+
# bash <scripts_dir>/flow-walker.sh current --flow <flow> --branch <branch>
|
|
25
|
+
#
|
|
26
|
+
# `current` is read-only: it re-prints the saved pending action byte for
|
|
27
|
+
# byte, and is how a planning (re-)entry finds a saved walk to continue. Its
|
|
28
|
+
# output names the saved `awaiting`: `action: dispatch` is `dispatch`,
|
|
29
|
+
# `binding: <ask>` is `answer`, any other `binding:` (`<escalate>`,
|
|
30
|
+
# `<terminal_handoff>`) is `done` — a finished walk, re-printed but continued
|
|
31
|
+
# by nothing: `next` refuses it with exit 1.
|
|
32
|
+
#
|
|
33
|
+
# <outcome> a key of the pending node's `outcomes`, or `answered` while an
|
|
34
|
+
# `<ask>` is pending. `FAIL` requires `--findings`.
|
|
35
|
+
# --skipped the run mode's skipped ids; read only when no flow-progress
|
|
36
|
+
# ledger exists, and re-read on every arrival at a gated node.
|
|
37
|
+
# The graph is `<this script's dir>/flows/<flow>.graph.json`; the root is a
|
|
38
|
+
# bare `git rev-parse --show-toplevel` in the working directory.
|
|
39
|
+
#
|
|
40
|
+
# OUTPUT — stdout, `key: value` lines, in this order:
|
|
41
|
+
#
|
|
42
|
+
# dispatch action: dispatch / node: / agent: / prompt: (writer nodes) /
|
|
43
|
+
# arg.iteration: (reviewer nodes: the next free index in the node's
|
|
44
|
+
# findings folder, never the counter) / arg.findings_file: (with
|
|
45
|
+
# `prompt: revision`) / skip: <node> <skipped|passed-by-exclusion>
|
|
46
|
+
# and ledger: <id> lines for what the walk passed through /
|
|
47
|
+
# heartbeat: <template, {iteration} = the counter> (#<total_dispatches>)
|
|
48
|
+
# binding action: binding / binding: <name> / node: / then
|
|
49
|
+
# <ask> agent: / resume: <node>
|
|
50
|
+
# <escalate> agent: / reason: <cap|error|blocker>, and for
|
|
51
|
+
# a cap: findings_file: / rounds: <node> <count>
|
|
52
|
+
# (a node recorded `skipped` omitted) / evidence
|
|
53
|
+
# lines / summary:
|
|
54
|
+
# <terminal_handoff> report: <key> <value> per recorded key
|
|
55
|
+
# then its skip: / ledger: lines.
|
|
56
|
+
#
|
|
57
|
+
# EXIT CODES
|
|
58
|
+
#
|
|
59
|
+
# 0 an action was printed (and, for start / next, the state written)
|
|
60
|
+
# 1 refusal: bad usage, undeclared outcome, `next` before `start`, `current`
|
|
61
|
+
# with no walk saved (a re-entry's "no saved walk"), a flow or
|
|
62
|
+
# branch other than the state's, unknown flow, formatVersion not 1, FAIL
|
|
63
|
+
# without --findings, no run-mode record, an unresolvable phase flag
|
|
64
|
+
# 2 the configuration, the graph or the state file cannot be resolved or
|
|
65
|
+
# written, or lib/harness-run-lib.sh predates this walker
|
|
66
|
+
# Every non-zero exit prints one `flow-walker: <cause>` line on stderr and
|
|
67
|
+
# leaves the state file byte-identical.
|
|
68
|
+
#
|
|
69
|
+
# STATE — `<state_dir>/.flow_walker_state`, machine-local, flat `key=value`
|
|
70
|
+
# lines: flow, branch, the pending node, `awaiting` (dispatch / answer / done),
|
|
71
|
+
# `counter.<name>`, the prompt variants, the last findings file, the recorded
|
|
72
|
+
# `report.<key>` values, the nodes skipped as `skipped`, and the pending
|
|
73
|
+
# action's printed lines (`out=`), which `current` re-prints unchanged. Only
|
|
74
|
+
# `start` overwrites it and resets every counter to 0 — a fresh write, an
|
|
75
|
+
# extend, a review of a draft and a skip past a loop alike. A re-entry that
|
|
76
|
+
# continues a saved walk (`current`, then `next` or a re-dispatch of the
|
|
77
|
+
# printed action) keeps it, so `counter.iteration`, the prompt variants and
|
|
78
|
+
# the last findings file survive a session boundary. It is a position, never a
|
|
79
|
+
# phase record: the flow-progress ledger stays the only durable record.
|
|
80
|
+
# Replaced by temp file and `mv`.
|
|
81
|
+
#
|
|
82
|
+
# BASH 3.2 AND JQ 1.5 ARE THE FLOORS: no associative array and no array-reading
|
|
83
|
+
# builtin. The graph is flattened by one `jq` per call.
|
|
84
|
+
#
|
|
85
|
+
# REPRO — one PASS-everywhere walk in a throwaway fixture:
|
|
86
|
+
#
|
|
87
|
+
# d=$(mktemp -d); git -C "$d" init -q -b feat_x
|
|
88
|
+
# printf '%s' '{"defaultBranch":"main","stateDir":"sdlc-harness/","phases":{"parity":true,"qa":true}}' > "$d/harness.config.json"
|
|
89
|
+
# mkdir -p "$d/s/lib" "$d/s/flows"
|
|
90
|
+
# cp <scripts_dir>/flow-walker.sh "$d/s/"
|
|
91
|
+
# cp <scripts_dir>/lib/harness-run-lib.sh <scripts_dir>/lib/flow-walker-gates.sh "$d/s/lib/"
|
|
92
|
+
# cp <scripts_dir>/flows/task_plan_writing.graph.json "$d/s/flows/"
|
|
93
|
+
# cd "$d"
|
|
94
|
+
# bash s/flow-walker.sh start --flow task_plan_writing --branch feat_x --skipped none
|
|
95
|
+
# -> dispatch plan_writer, prompt: initial
|
|
96
|
+
# ... next --flow task_plan_writing --branch feat_x --outcome returned --skipped none
|
|
97
|
+
# -> dispatch business_parity_review, arg.iteration: 0
|
|
98
|
+
# ... next ... --outcome PASS --skipped none -> architecture_review, arg.iteration: 0
|
|
99
|
+
# ... next ... --outcome PASS --skipped none -> plan_review, arg.iteration: 0
|
|
100
|
+
# ... next ... --outcome PASS --skipped none -> ledger: P1, ui_writer, prompt: initial
|
|
101
|
+
# ... next ... --outcome returned --skipped none -> ui_review
|
|
102
|
+
# ... next ... --outcome PASS --skipped none -> binding: <terminal_handoff>,
|
|
103
|
+
# four report: lines, ledger: P2
|
|
104
|
+
# ... next ... --outcome PASS -> exit 1: the walk has finished
|
|
105
|
+
|
|
106
|
+
set -uo pipefail
|
|
107
|
+
|
|
108
|
+
fw_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
109
|
+
for fw_lib in "$fw_dir/lib/harness-run-lib.sh" "$fw_dir/lib/flow-walker-gates.sh"; do
|
|
110
|
+
if [ ! -r "$fw_lib" ]; then
|
|
111
|
+
printf 'flow-walker: cannot read %s\n' "$fw_lib" >&2
|
|
112
|
+
exit 2
|
|
113
|
+
fi
|
|
114
|
+
# shellcheck source=/dev/null
|
|
115
|
+
. "$fw_lib"
|
|
116
|
+
done
|
|
117
|
+
# An older `init` keeps an existing lib/harness-run-lib.sh (create-if-absent), and one written
|
|
118
|
+
# before this walker shipped lacks the phase reader every `skipped` gate calls.
|
|
119
|
+
if ! declare -F hr_phase_enabled >/dev/null; then
|
|
120
|
+
printf 'flow-walker: %s predates this walker (it defines no hr_phase_enabled): delete that file and re-run autonomous-sdlc-harness init, which writes the current copy\n' "$fw_dir/lib/harness-run-lib.sh" >&2
|
|
121
|
+
exit 2
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
NL='
|
|
125
|
+
'
|
|
126
|
+
TAB=$'\t'
|
|
127
|
+
|
|
128
|
+
refuse() {
|
|
129
|
+
printf 'flow-walker: %s\n' "$1" >&2
|
|
130
|
+
exit 1
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
fault() {
|
|
134
|
+
printf 'flow-walker: %s\n' "$1" >&2
|
|
135
|
+
exit 2
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# Every lookup below sets a global rather than printing, because `refuse` and
|
|
139
|
+
# `fault` inside a `$(…)` would exit the subshell only.
|
|
140
|
+
|
|
141
|
+
# --- Arguments ------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
[ "$#" -gt 0 ] || refuse "missing subcommand: start, next or current"
|
|
144
|
+
CMD="$1"
|
|
145
|
+
shift
|
|
146
|
+
case "$CMD" in
|
|
147
|
+
start|next|current) ;;
|
|
148
|
+
*) refuse "unknown subcommand '$CMD': expected start, next or current" ;;
|
|
149
|
+
esac
|
|
150
|
+
|
|
151
|
+
A_FLOW=""
|
|
152
|
+
A_BRANCH=""
|
|
153
|
+
A_ENTRY=""
|
|
154
|
+
A_SKIPPED=""
|
|
155
|
+
A_OUTCOME=""
|
|
156
|
+
A_FINDINGS=""
|
|
157
|
+
while [ "$#" -gt 0 ]; do
|
|
158
|
+
flag="$1"
|
|
159
|
+
case "$CMD:$flag" in
|
|
160
|
+
*:--flow|*:--branch|start:--entry|start:--skipped|next:--skipped|next:--outcome|next:--findings) ;;
|
|
161
|
+
*) refuse "'$flag' is not a flag of '$CMD'" ;;
|
|
162
|
+
esac
|
|
163
|
+
[ "$#" -ge 2 ] || refuse "$flag requires a value"
|
|
164
|
+
value="$2"
|
|
165
|
+
shift 2
|
|
166
|
+
case "$value" in
|
|
167
|
+
*"$NL"*) refuse "the value of $flag carries a newline" ;;
|
|
168
|
+
esac
|
|
169
|
+
case "$flag" in
|
|
170
|
+
--flow) A_FLOW="$value" ;;
|
|
171
|
+
--branch) A_BRANCH="$value" ;;
|
|
172
|
+
--entry) A_ENTRY="$value" ;;
|
|
173
|
+
--skipped) A_SKIPPED="$value" ;;
|
|
174
|
+
--outcome) A_OUTCOME="$value" ;;
|
|
175
|
+
--findings) A_FINDINGS="$value" ;;
|
|
176
|
+
esac
|
|
177
|
+
done
|
|
178
|
+
|
|
179
|
+
[ -n "$A_FLOW" ] || refuse "missing required flag --flow"
|
|
180
|
+
[ -n "$A_BRANCH" ] || refuse "missing required flag --branch"
|
|
181
|
+
if [ "$CMD" = next ]; then
|
|
182
|
+
[ -n "$A_OUTCOME" ] || refuse "missing required flag --outcome"
|
|
183
|
+
case "$A_OUTCOME" in
|
|
184
|
+
*[!A-Za-z0-9_]*) refuse "outcome '$A_OUTCOME' is not an outcome name" ;;
|
|
185
|
+
esac
|
|
186
|
+
if [ "$A_OUTCOME" = FAIL ] && [ -z "$A_FINDINGS" ]; then
|
|
187
|
+
refuse "outcome FAIL requires --findings with the reviewer's findings_file"
|
|
188
|
+
fi
|
|
189
|
+
fi
|
|
190
|
+
case "$A_FLOW" in
|
|
191
|
+
*[!a-z0-9_]*) refuse "unknown flow '$A_FLOW'" ;;
|
|
192
|
+
esac
|
|
193
|
+
|
|
194
|
+
# --- Configuration ----------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
ROOT="$(hr_repo_root)" || fault "not inside a git repository"
|
|
197
|
+
hr_config_load "$ROOT"
|
|
198
|
+
case "$?" in
|
|
199
|
+
0) ;;
|
|
200
|
+
1) fault "no harness.config.json at $ROOT" ;;
|
|
201
|
+
*) fault "cannot resolve $ROOT/harness.config.json (invalid JSON, no defaultBranch, or jq missing or older than 1.5)" ;;
|
|
202
|
+
esac
|
|
203
|
+
STATE_DIR="$(hr_state_dir "$ROOT")" || fault "cannot resolve stateDir from $ROOT/harness.config.json"
|
|
204
|
+
STATE_FILE="$ROOT/$STATE_DIR/.flow_walker_state"
|
|
205
|
+
|
|
206
|
+
# --- The graph --------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
GRAPH_FILE="$fw_dir/flows/$A_FLOW.graph.json"
|
|
209
|
+
[ -f "$GRAPH_FILE" ] || refuse "unknown flow '$A_FLOW': no $GRAPH_FILE"
|
|
210
|
+
hr_have_jq || fault "jq is not on PATH"
|
|
211
|
+
|
|
212
|
+
# One line per scalar: `<dotted path>\t<value>`, array indices as path segments.
|
|
213
|
+
G_RAW="$(jq -r 'paths(scalars) as $p | [($p | map(tostring) | join(".")), (getpath($p) | tostring)] | @tsv' "$GRAPH_FILE" 2>/dev/null)" \
|
|
214
|
+
|| fault "cannot parse $GRAPH_FILE"
|
|
215
|
+
G_LINES="$NL$G_RAW$NL"
|
|
216
|
+
|
|
217
|
+
# Set GV to the graph value at <path>; return 1 when absent.
|
|
218
|
+
g_get() {
|
|
219
|
+
local key="$1" rest
|
|
220
|
+
GV=""
|
|
221
|
+
case "$G_LINES" in
|
|
222
|
+
*"$NL$key$TAB"*) ;;
|
|
223
|
+
*) return 1 ;;
|
|
224
|
+
esac
|
|
225
|
+
rest="${G_LINES#*"$NL$key$TAB"}"
|
|
226
|
+
rest="${rest%%"$NL"*}"
|
|
227
|
+
hr_tsv_unescape_var "$rest"
|
|
228
|
+
GV="$HR_CFG_VALUE"
|
|
229
|
+
return 0
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
g_need() {
|
|
233
|
+
g_get "$1" || fault "graph $GRAPH_FILE has no '$1'"
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
g_has_prefix() {
|
|
237
|
+
case "$G_LINES" in
|
|
238
|
+
*"$NL$1"*) return 0 ;;
|
|
239
|
+
esac
|
|
240
|
+
return 1
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
g_get formatVersion || refuse "graph $GRAPH_FILE carries no formatVersion"
|
|
244
|
+
[ "$GV" = 1 ] || refuse "graph $GRAPH_FILE has formatVersion '$GV'; this walker reads 1"
|
|
245
|
+
g_need flow
|
|
246
|
+
[ "$GV" = "$A_FLOW" ] || fault "graph $GRAPH_FILE declares flow '$GV', not '$A_FLOW'"
|
|
247
|
+
|
|
248
|
+
# --- The state store (newline-joined `key=value`) ----------------------------
|
|
249
|
+
|
|
250
|
+
ST_LINES="$NL"
|
|
251
|
+
OUT=""
|
|
252
|
+
PASS=""
|
|
253
|
+
|
|
254
|
+
st_get() {
|
|
255
|
+
local key="$1" rest
|
|
256
|
+
SV=""
|
|
257
|
+
case "$ST_LINES" in
|
|
258
|
+
*"$NL$key="*) ;;
|
|
259
|
+
*) return 1 ;;
|
|
260
|
+
esac
|
|
261
|
+
rest="${ST_LINES#*"$NL$key="}"
|
|
262
|
+
SV="${rest%%"$NL"*}"
|
|
263
|
+
return 0
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
st_set() {
|
|
267
|
+
local key="$1" val="$2" line out="$NL"
|
|
268
|
+
while IFS= read -r line; do
|
|
269
|
+
[ -n "$line" ] || continue
|
|
270
|
+
case "$line" in
|
|
271
|
+
"$key="*) continue ;;
|
|
272
|
+
esac
|
|
273
|
+
out="$out$line$NL"
|
|
274
|
+
done <<EOF
|
|
275
|
+
$ST_LINES
|
|
276
|
+
EOF
|
|
277
|
+
ST_LINES="$out$key=$val$NL"
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
load_state() {
|
|
281
|
+
local line
|
|
282
|
+
[ -f "$STATE_FILE" ] || refuse "no walk in progress at $STATE_FILE: run 'start' first"
|
|
283
|
+
[ -r "$STATE_FILE" ] || fault "cannot read $STATE_FILE"
|
|
284
|
+
while IFS= read -r line; do
|
|
285
|
+
case "$line" in
|
|
286
|
+
out=*) OUT="$OUT${line#out=}$NL" ;;
|
|
287
|
+
*=*) ST_LINES="$ST_LINES$line$NL" ;;
|
|
288
|
+
esac
|
|
289
|
+
done < "$STATE_FILE"
|
|
290
|
+
st_get flow || fault "$STATE_FILE carries no flow"
|
|
291
|
+
[ "$SV" = "$A_FLOW" ] || refuse "the walk in progress is flow '$SV', not '$A_FLOW'"
|
|
292
|
+
st_get branch || fault "$STATE_FILE carries no branch"
|
|
293
|
+
[ "$SV" = "$A_BRANCH" ] || refuse "the walk in progress is for branch '$SV', not '$A_BRANCH'"
|
|
294
|
+
[ -n "$OUT" ] || fault "$STATE_FILE carries no pending action"
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
save_state() {
|
|
298
|
+
local dir tmp line
|
|
299
|
+
dir="${STATE_FILE%/*}"
|
|
300
|
+
[ -d "$dir" ] || mkdir -p "$dir" || fault "cannot create $dir"
|
|
301
|
+
tmp="$STATE_FILE.tmp.$$"
|
|
302
|
+
{
|
|
303
|
+
printf '%s' "${ST_LINES#"$NL"}"
|
|
304
|
+
while IFS= read -r line; do
|
|
305
|
+
if [ -n "$line" ]; then printf 'out=%s\n' "$line"; fi
|
|
306
|
+
done <<EOF
|
|
307
|
+
$OUT
|
|
308
|
+
EOF
|
|
309
|
+
} > "$tmp" 2>/dev/null || { rm -f "$tmp"; fault "cannot write $tmp"; }
|
|
310
|
+
mv -f "$tmp" "$STATE_FILE" 2>/dev/null || { rm -f "$tmp"; fault "cannot replace $STATE_FILE"; }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
# --- Rendering helpers ------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
# SUBST = <string> with every literal <pattern> replaced by <replacement>.
|
|
316
|
+
subst() {
|
|
317
|
+
local s="$1" pat="$2" rep="$3" out=""
|
|
318
|
+
while :; do
|
|
319
|
+
case "$s" in
|
|
320
|
+
*"$pat"*)
|
|
321
|
+
out="$out${s%%"$pat"*}$rep"
|
|
322
|
+
s="${s#*"$pat"}"
|
|
323
|
+
;;
|
|
324
|
+
*) break ;;
|
|
325
|
+
esac
|
|
326
|
+
done
|
|
327
|
+
SUBST="$out$s"
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
# SUBST = <template> with {state_dir} and {branch} rendered, repo-relative.
|
|
331
|
+
render_path() {
|
|
332
|
+
subst "$1" "{state_dir}" "$STATE_DIR"
|
|
333
|
+
subst "$SUBST" "{branch}" "$A_BRANCH"
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
record_reports() {
|
|
337
|
+
local prefix="$1.report." line key
|
|
338
|
+
while IFS= read -r line; do
|
|
339
|
+
case "$line" in
|
|
340
|
+
"$prefix"*)
|
|
341
|
+
key="${line#"$prefix"}"
|
|
342
|
+
key="${key%%"$TAB"*}"
|
|
343
|
+
hr_tsv_unescape_var "${line#*"$TAB"}"
|
|
344
|
+
st_set "report.$key" "$HR_CFG_VALUE"
|
|
345
|
+
;;
|
|
346
|
+
esac
|
|
347
|
+
done <<EOF
|
|
348
|
+
$G_LINES
|
|
349
|
+
EOF
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
add_skipped_node() {
|
|
353
|
+
st_get skipped_nodes
|
|
354
|
+
fw_id_in_list "$1" "$SV" && return 0
|
|
355
|
+
if [ -n "$SV" ]; then st_set skipped_nodes "$SV,$1"; else st_set skipped_nodes "$1"; fi
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
render_dispatch() {
|
|
359
|
+
local node="$1" variant="$2" agent idx hb
|
|
360
|
+
g_need "nodes.$node.agent"
|
|
361
|
+
agent="$GV"
|
|
362
|
+
OUT="action: dispatch${NL}node: $node${NL}agent: $agent$NL"
|
|
363
|
+
st_set node "$node"
|
|
364
|
+
st_set awaiting dispatch
|
|
365
|
+
st_set resume ""
|
|
366
|
+
st_set prompt ""
|
|
367
|
+
if g_get "nodes.$node.prompts.0"; then
|
|
368
|
+
[ -n "$variant" ] || variant=initial
|
|
369
|
+
case "$variant" in
|
|
370
|
+
initial|revision) ;;
|
|
371
|
+
*) fault "graph gives '$node' the unknown prompt variant '$variant'" ;;
|
|
372
|
+
esac
|
|
373
|
+
OUT="${OUT}prompt: $variant$NL"
|
|
374
|
+
st_set prompt "$variant"
|
|
375
|
+
st_set "prompt.$node" "$variant"
|
|
376
|
+
if [ "$variant" = revision ]; then
|
|
377
|
+
st_get findings_file
|
|
378
|
+
[ -n "$SV" ] || fault "a revision of '$node' is due and no findings file is recorded"
|
|
379
|
+
OUT="${OUT}arg.findings_file: $SV$NL"
|
|
380
|
+
fi
|
|
381
|
+
fi
|
|
382
|
+
if g_get "nodes.$node.findingsFolder"; then
|
|
383
|
+
render_path "$GV"
|
|
384
|
+
idx="$(fw_next_review_index "$ROOT/$SUBST")"
|
|
385
|
+
OUT="${OUT}arg.iteration: $idx$NL"
|
|
386
|
+
fi
|
|
387
|
+
OUT="$OUT$PASS"
|
|
388
|
+
g_need "nodes.$node.heartbeat"
|
|
389
|
+
st_get counter.iteration
|
|
390
|
+
subst "$GV" "{iteration}" "$SV"
|
|
391
|
+
hb="$SUBST"
|
|
392
|
+
OUT="${OUT}heartbeat: $hb (#<total_dispatches>)$NL"
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
render_terminal() {
|
|
396
|
+
local node="$1" i=0 key
|
|
397
|
+
g_need "nodes.$node.binding"
|
|
398
|
+
OUT="action: binding${NL}binding: $GV${NL}node: $node$NL"
|
|
399
|
+
while g_get "reports.$i"; do
|
|
400
|
+
key="$GV"
|
|
401
|
+
if st_get "report.$key"; then
|
|
402
|
+
OUT="${OUT}report: $key $SV$NL"
|
|
403
|
+
fi
|
|
404
|
+
i=$((i + 1))
|
|
405
|
+
done
|
|
406
|
+
OUT="$OUT$PASS"
|
|
407
|
+
st_set node "$node"
|
|
408
|
+
st_set awaiting done
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
# Print the binding at graph path <prefix> reached from <node>. <cap> is the
|
|
412
|
+
# cap that fired, for a cap escalation.
|
|
413
|
+
take_binding() {
|
|
414
|
+
local prefix="$1" node="$2" cap="${3-}" binding agent reason i=0 r count ev
|
|
415
|
+
g_need "$prefix.binding"
|
|
416
|
+
binding="$GV"
|
|
417
|
+
g_need "nodes.$node.agent"
|
|
418
|
+
agent="$GV"
|
|
419
|
+
OUT="action: binding${NL}binding: $binding${NL}node: $node${NL}agent: $agent$NL"
|
|
420
|
+
st_set node "$node"
|
|
421
|
+
case "$binding" in
|
|
422
|
+
"<ask>")
|
|
423
|
+
g_need "$prefix.resume"
|
|
424
|
+
OUT="${OUT}resume: $GV$NL"
|
|
425
|
+
st_set resume "$GV"
|
|
426
|
+
st_set awaiting answer
|
|
427
|
+
;;
|
|
428
|
+
"<escalate>")
|
|
429
|
+
g_need "$prefix.reason"
|
|
430
|
+
reason="$GV"
|
|
431
|
+
OUT="${OUT}reason: $reason$NL"
|
|
432
|
+
if [ "$reason" = cap ]; then
|
|
433
|
+
st_get findings_file
|
|
434
|
+
OUT="${OUT}findings_file: $SV$NL"
|
|
435
|
+
st_get skipped_nodes
|
|
436
|
+
local skipped="$SV"
|
|
437
|
+
while g_get "$prefix.roundCounts.$i"; do
|
|
438
|
+
r="$GV"
|
|
439
|
+
i=$((i + 1))
|
|
440
|
+
fw_id_in_list "$r" "$skipped" && continue
|
|
441
|
+
g_need "nodes.$r.findingsFolder"
|
|
442
|
+
render_path "$GV"
|
|
443
|
+
count="$(fw_round_count "$ROOT/$SUBST")"
|
|
444
|
+
OUT="${OUT}rounds: $r $count$NL"
|
|
445
|
+
done
|
|
446
|
+
if g_get "$prefix.evidence"; then
|
|
447
|
+
g_need "paths.$GV"
|
|
448
|
+
render_path "$GV"
|
|
449
|
+
ev="$(fw_index_evidence "$ROOT/$SUBST")"
|
|
450
|
+
[ -z "$ev" ] || OUT="$OUT$ev$NL"
|
|
451
|
+
fi
|
|
452
|
+
if g_get "$prefix.summary"; then
|
|
453
|
+
subst "$GV" "{cap}" "$cap"
|
|
454
|
+
OUT="${OUT}summary: $SUBST$NL"
|
|
455
|
+
fi
|
|
456
|
+
fi
|
|
457
|
+
st_set awaiting done
|
|
458
|
+
;;
|
|
459
|
+
*) fault "graph binding '$binding' at '$prefix' is not <ask> or <escalate>" ;;
|
|
460
|
+
esac
|
|
461
|
+
OUT="$OUT$PASS"
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
# --- Transitions ------------------------------------------------------------
|
|
465
|
+
|
|
466
|
+
# Move to <node>, passing its skip gates, and render what is reached.
|
|
467
|
+
arrive() {
|
|
468
|
+
local node="$1" variant="$2" steps=0 i applied construct phase id list status
|
|
469
|
+
while :; do
|
|
470
|
+
steps=$((steps + 1))
|
|
471
|
+
[ "$steps" -le 64 ] || fault "graph loops through skip gates at '$node'"
|
|
472
|
+
g_get "nodes.$node.kind" || fault "graph has no node '$node'"
|
|
473
|
+
case "$GV" in
|
|
474
|
+
terminal)
|
|
475
|
+
render_terminal "$node"
|
|
476
|
+
return 0
|
|
477
|
+
;;
|
|
478
|
+
dispatch) ;;
|
|
479
|
+
*) fault "node '$node' has unknown kind '$GV'" ;;
|
|
480
|
+
esac
|
|
481
|
+
applied=""
|
|
482
|
+
i=0
|
|
483
|
+
while [ -z "$applied" ] && g_get "nodes.$node.skip.$i.construct"; do
|
|
484
|
+
construct="$GV"
|
|
485
|
+
case "$construct" in
|
|
486
|
+
skipped)
|
|
487
|
+
g_need "nodes.$node.skip.$i.phase"
|
|
488
|
+
phase="$GV"
|
|
489
|
+
fw_phase_on "$ROOT" "$phase"
|
|
490
|
+
status=$?
|
|
491
|
+
case "$status" in
|
|
492
|
+
0) ;;
|
|
493
|
+
1) applied=skipped ;;
|
|
494
|
+
*) refuse "cannot resolve phases.$phase in $ROOT/harness.config.json" ;;
|
|
495
|
+
esac
|
|
496
|
+
;;
|
|
497
|
+
passedByExclusion)
|
|
498
|
+
g_need "nodes.$node.skip.$i.runModeId"
|
|
499
|
+
id="$GV"
|
|
500
|
+
list="$(fw_run_mode_skipped "$ROOT" "$STATE_DIR" "$A_BRANCH" "$A_SKIPPED")"
|
|
501
|
+
status=$?
|
|
502
|
+
case "$status" in
|
|
503
|
+
0) if fw_id_in_list "$id" "$list"; then applied=passed-by-exclusion; fi ;;
|
|
504
|
+
1) refuse "no run-mode record: no flow-progress ledger for '$A_BRANCH' and no --skipped" ;;
|
|
505
|
+
*) refuse "the flow-progress ledger for '$A_BRANCH' has no '- skipped:' line under '## Run mode'" ;;
|
|
506
|
+
esac
|
|
507
|
+
;;
|
|
508
|
+
*) fault "node '$node' has unknown skip construct '$construct'" ;;
|
|
509
|
+
esac
|
|
510
|
+
[ -n "$applied" ] || i=$((i + 1))
|
|
511
|
+
done
|
|
512
|
+
if [ -z "$applied" ]; then
|
|
513
|
+
render_dispatch "$node" "$variant"
|
|
514
|
+
return 0
|
|
515
|
+
fi
|
|
516
|
+
record_reports "nodes.$node.skip.$i"
|
|
517
|
+
PASS="${PASS}skip: $node $applied$NL"
|
|
518
|
+
[ "$applied" != skipped ] || add_skipped_node "$node"
|
|
519
|
+
g_need "nodes.$node.skip.$i.to"
|
|
520
|
+
node="$GV"
|
|
521
|
+
done
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
follow_edge() {
|
|
525
|
+
local edge="$1" node="$2" counter cap n variant=initial
|
|
526
|
+
if g_get "$edge.binding"; then
|
|
527
|
+
take_binding "$edge" "$node"
|
|
528
|
+
return 0
|
|
529
|
+
fi
|
|
530
|
+
if g_get "$edge.reset"; then
|
|
531
|
+
st_set "counter.$GV" 0
|
|
532
|
+
fi
|
|
533
|
+
if g_get "$edge.increment"; then
|
|
534
|
+
counter="$GV"
|
|
535
|
+
st_get "counter.$counter"
|
|
536
|
+
case "$SV" in
|
|
537
|
+
''|*[!0-9]*) fault "$STATE_FILE carries no numeric counter.$counter" ;;
|
|
538
|
+
esac
|
|
539
|
+
n=$((SV + 1))
|
|
540
|
+
st_set "counter.$counter" "$n"
|
|
541
|
+
g_need "counters.$counter.cap"
|
|
542
|
+
cap="$GV"
|
|
543
|
+
if [ "$n" -ge "$cap" ]; then
|
|
544
|
+
g_get "$edge.onCap.binding" || fault "edge '$edge' reaches its cap with no onCap"
|
|
545
|
+
take_binding "$edge.onCap" "$node" "$cap"
|
|
546
|
+
return 0
|
|
547
|
+
fi
|
|
548
|
+
fi
|
|
549
|
+
record_reports "$edge"
|
|
550
|
+
if g_get "$edge.ledger"; then
|
|
551
|
+
PASS="${PASS}ledger: $GV$NL"
|
|
552
|
+
fi
|
|
553
|
+
if g_get "$edge.prompt"; then variant="$GV"; fi
|
|
554
|
+
g_need "$edge.to"
|
|
555
|
+
arrive "$GV" "$variant"
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
# --- Subcommands ------------------------------------------------------------
|
|
559
|
+
|
|
560
|
+
case "$CMD" in
|
|
561
|
+
start)
|
|
562
|
+
g_need start
|
|
563
|
+
entry="$GV"
|
|
564
|
+
if [ -n "$A_ENTRY" ]; then
|
|
565
|
+
i=0
|
|
566
|
+
found=1
|
|
567
|
+
while g_get "entries.$i"; do
|
|
568
|
+
if [ "$GV" = "$A_ENTRY" ]; then found=0; fi
|
|
569
|
+
i=$((i + 1))
|
|
570
|
+
done
|
|
571
|
+
[ "$found" -eq 0 ] || refuse "'$A_ENTRY' is not an entry of flow '$A_FLOW'"
|
|
572
|
+
entry="$A_ENTRY"
|
|
573
|
+
fi
|
|
574
|
+
st_set flow "$A_FLOW"
|
|
575
|
+
st_set branch "$A_BRANCH"
|
|
576
|
+
while IFS= read -r line; do
|
|
577
|
+
case "$line" in
|
|
578
|
+
counters.*.cap"$TAB"*)
|
|
579
|
+
name="${line#counters.}"
|
|
580
|
+
name="${name%%.cap"$TAB"*}"
|
|
581
|
+
st_set "counter.$name" 0
|
|
582
|
+
;;
|
|
583
|
+
esac
|
|
584
|
+
done <<EOF
|
|
585
|
+
$G_LINES
|
|
586
|
+
EOF
|
|
587
|
+
st_set findings_file ""
|
|
588
|
+
st_set skipped_nodes ""
|
|
589
|
+
arrive "$entry" initial
|
|
590
|
+
save_state
|
|
591
|
+
;;
|
|
592
|
+
next)
|
|
593
|
+
load_state
|
|
594
|
+
st_get node
|
|
595
|
+
node="$SV"
|
|
596
|
+
st_get awaiting
|
|
597
|
+
case "$SV" in
|
|
598
|
+
done)
|
|
599
|
+
refuse "the walk finished at '$node'; no outcome is pending (run 'start' to walk again)"
|
|
600
|
+
;;
|
|
601
|
+
answer)
|
|
602
|
+
[ "$A_OUTCOME" = answered ] \
|
|
603
|
+
|| refuse "outcome '$A_OUTCOME' is not declared while an <ask> is pending at '$node'; only 'answered' is"
|
|
604
|
+
[ -z "$A_FINDINGS" ] || st_set findings_file "$A_FINDINGS"
|
|
605
|
+
st_get resume
|
|
606
|
+
resume="$SV"
|
|
607
|
+
[ -n "$resume" ] || fault "$STATE_FILE carries no resume node"
|
|
608
|
+
st_get "prompt.$resume"
|
|
609
|
+
arrive "$resume" "$SV"
|
|
610
|
+
;;
|
|
611
|
+
dispatch)
|
|
612
|
+
g_has_prefix "nodes.$node.outcomes.$A_OUTCOME." \
|
|
613
|
+
|| refuse "outcome '$A_OUTCOME' is not declared by node '$node'"
|
|
614
|
+
[ -z "$A_FINDINGS" ] || st_set findings_file "$A_FINDINGS"
|
|
615
|
+
follow_edge "nodes.$node.outcomes.$A_OUTCOME" "$node"
|
|
616
|
+
;;
|
|
617
|
+
*)
|
|
618
|
+
fault "$STATE_FILE carries an unknown awaiting value '$SV'"
|
|
619
|
+
;;
|
|
620
|
+
esac
|
|
621
|
+
save_state
|
|
622
|
+
;;
|
|
623
|
+
current)
|
|
624
|
+
load_state
|
|
625
|
+
;;
|
|
626
|
+
esac
|
|
627
|
+
|
|
628
|
+
printf '%s' "$OUT"
|
|
629
|
+
exit 0
|