@jenga-ai/agent 3.1.0 → 3.1.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.
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env bash
2
+ # ---------------------------------------------------------------------------
3
+ # skills/jenga/scripts/run-playbook-step.sh
4
+ #
5
+ # Deterministic step-SEQUENCING state tracker for a confirmed playbook chain (E53_S02_T03). This
6
+ # script does NOT execute a skill step itself — invoking a skill's `SKILL.md` instructions (and,
7
+ # where applicable, loading `agents/<prefered_agent>.md`) is inherently the calling agent's job
8
+ # and cannot live in a shell script. What belongs in a script, per `CLAUDE.md`'s Skill
9
+ # Implementation Principle, is the deterministic bookkeeping around that: which step is current,
10
+ # which have completed, which have failed, and enforcing that a failure halts the sequence
11
+ # permanently with no silent skip-ahead — exactly the story's acceptance criterion for mid-chain
12
+ # failure handling.
13
+ #
14
+ # `/jenga`'s natural-language branch (wired in E53_S02_T04) uses this script as follows, after
15
+ # `render-playbook-confirmation.sh` (E53_S02_T03) returns a `confirmed` result:
16
+ #
17
+ # 1. `init` with the confirmed, ordered step list -> get the first step to invoke.
18
+ # 2. Invoke that step (as `/route`'s Step 6 already does for a single matched skill).
19
+ # 3. `advance <state_file> passed` (step succeeded) or `advance <state_file> failed [note]`
20
+ # (step failed) -> get the next step, a "complete" signal, or (on failure) a halt report.
21
+ # 4. Repeat 2-3 until "complete" or a halt report is returned.
22
+ #
23
+ # ---------------------------------------------------------------------------
24
+ # USAGE
25
+ # ---------------------------------------------------------------------------
26
+ # run-playbook-step.sh init "<playbook_id>" "<name>" "<comma-separated confirmed step names>"
27
+ # Starts a new run. Creates a state file tracking the ordered step list, a current-step
28
+ # pointer (starts at the first step), and empty completed/failed lists. Emits the first
29
+ # step's info as JSON on stdout and a `STATE_FILE:` path on stderr.
30
+ #
31
+ # run-playbook-step.sh advance <state_file> passed
32
+ # Records the CURRENT step as completed and advances the pointer. If more steps remain,
33
+ # emits the next step's info as JSON (same shape as `init`'s stdout). If that was the last
34
+ # step, emits a completion report instead (see OUTPUT SCHEMA) and removes the state file.
35
+ #
36
+ # run-playbook-step.sh advance <state_file> failed ["<note>"]
37
+ # Records the CURRENT step as failed (optionally with a free-text note) and halts the
38
+ # sequence PERMANENTLY — the state file is marked `halted: true` rather than removed, so a
39
+ # further `advance` call against it is rejected (see EXIT CODES). Emits a halt report (see
40
+ # OUTPUT SCHEMA) listing completed / failed / never-run steps.
41
+ #
42
+ # ---------------------------------------------------------------------------
43
+ # OUTPUT SCHEMA
44
+ # ---------------------------------------------------------------------------
45
+ # `init` and a `passed` `advance` call that has more steps remaining both emit, on stdout:
46
+ #
47
+ # {"status": "step_ready", "step": "<skill name>", "step_index": 2, "total_steps": 5}
48
+ #
49
+ # A `passed` `advance` call on the FINAL step emits, on stdout (state file removed):
50
+ #
51
+ # {"status": "complete", "playbook_id": "...", "name": "...",
52
+ # "completed": ["<step1>", "<step2>", ...]}
53
+ #
54
+ # A `failed` `advance` call emits, on stdout (state file retained, marked halted):
55
+ #
56
+ # {"status": "halted", "playbook_id": "...", "name": "...",
57
+ # "completed": ["<step1>", ...], "failed_step": "<stepN>", "failed_note": "<note or null>",
58
+ # "never_run": ["<stepN+1>", ...]}
59
+ #
60
+ # Nothing else is ever written to stdout — errors/warnings go to stderr only.
61
+ #
62
+ # ---------------------------------------------------------------------------
63
+ # EXIT CODES
64
+ # ---------------------------------------------------------------------------
65
+ # 0 `init` succeeded; OR `advance passed` succeeded (whether it returned the next step or a
66
+ # "complete" report); OR `advance failed` succeeded in recording the halt (a "halted" report
67
+ # IS the expected, successful outcome of this call — exit 0, not an error)
68
+ # 2 usage error (missing/malformed arguments, unrecognized outcome word), or a real setup
69
+ # problem (python3 unavailable, state file missing/corrupt)
70
+ # 3 `advance` called against a state file already marked `halted: true` from a prior `failed`
71
+ # call — rejected outright rather than silently resuming; this is the "no skip-ahead, no
72
+ # silent resumption after a halt" guard the story's acceptance criteria require
73
+ #
74
+ # ---------------------------------------------------------------------------
75
+
76
+ set -euo pipefail
77
+
78
+ if ! command -v python3 >/dev/null 2>&1; then
79
+ echo "Error: python3 is required by run-playbook-step.sh" >&2
80
+ exit 2
81
+ fi
82
+
83
+ if [ $# -lt 1 ]; then
84
+ echo "Usage:" >&2
85
+ echo " run-playbook-step.sh init \"<playbook_id>\" \"<name>\" \"<comma-separated confirmed step names>\"" >&2
86
+ echo " run-playbook-step.sh advance <state_file> passed" >&2
87
+ echo " run-playbook-step.sh advance <state_file> failed [\"<note>\"]" >&2
88
+ exit 2
89
+ fi
90
+
91
+ SUBCOMMAND="$1"
92
+ shift
93
+
94
+ if [ "$SUBCOMMAND" = "init" ]; then
95
+ if [ $# -ne 3 ]; then
96
+ echo 'Usage: run-playbook-step.sh init "<playbook_id>" "<name>" "<comma-separated steps>"' >&2
97
+ exit 2
98
+ fi
99
+ PLAYBOOK_ID="$1"
100
+ PLAYBOOK_NAME="$2"
101
+ RAW_STEPS="$3"
102
+
103
+ if [ -z "${RAW_STEPS// /}" ]; then
104
+ echo "Error: no steps given to run-playbook-step.sh init" >&2
105
+ exit 2
106
+ fi
107
+
108
+ STATE_FILE="$(mktemp -t jenga-playbook-run-XXXXXX.json)"
109
+
110
+ PY_SCRIPT="$(mktemp -t run-playbook-step-init-XXXXXX.py)"
111
+ trap 'rm -f "$PY_SCRIPT"' EXIT
112
+
113
+ cat > "$PY_SCRIPT" <<'PY'
114
+ import json
115
+ import sys
116
+ from datetime import datetime, timezone
117
+
118
+ state_file_path = sys.argv[1]
119
+ playbook_id = sys.argv[2]
120
+ playbook_name = sys.argv[3]
121
+ raw_steps = sys.argv[4]
122
+
123
+ steps = [s.strip() for s in raw_steps.split(",") if s.strip() != ""]
124
+ if not steps:
125
+ print("Error: no valid step names parsed from the given list", file=sys.stderr)
126
+ sys.exit(2)
127
+
128
+ state = {
129
+ "version": 1,
130
+ "created_at": datetime.now(timezone.utc).isoformat(),
131
+ "playbook_id": playbook_id,
132
+ "playbook_name": playbook_name,
133
+ "steps": steps,
134
+ "current_index": 0,
135
+ "completed": [],
136
+ "halted": False,
137
+ "failed_step": None,
138
+ "failed_note": None,
139
+ }
140
+
141
+ with open(state_file_path, "w", encoding="utf-8") as f:
142
+ json.dump(state, f, indent=2)
143
+ f.write("\n")
144
+
145
+ print(json.dumps({
146
+ "status": "step_ready",
147
+ "step": steps[0],
148
+ "step_index": 1,
149
+ "total_steps": len(steps),
150
+ }))
151
+ print(f"STATE_FILE: {state_file_path}", file=sys.stderr)
152
+ PY
153
+
154
+ python3 "$PY_SCRIPT" "$STATE_FILE" "$PLAYBOOK_ID" "$PLAYBOOK_NAME" "$RAW_STEPS"
155
+ exit 0
156
+
157
+ elif [ "$SUBCOMMAND" = "advance" ]; then
158
+ if [ $# -lt 2 ] || [ $# -gt 3 ]; then
159
+ echo 'Usage: run-playbook-step.sh advance <state_file> passed|failed ["<note>"]' >&2
160
+ exit 2
161
+ fi
162
+ STATE_FILE="$1"
163
+ OUTCOME="$2"
164
+ NOTE="${3:-}"
165
+
166
+ if [ "$OUTCOME" != "passed" ] && [ "$OUTCOME" != "failed" ]; then
167
+ echo "Error: outcome must be 'passed' or 'failed', got '$OUTCOME'" >&2
168
+ exit 2
169
+ fi
170
+
171
+ if [ ! -f "$STATE_FILE" ]; then
172
+ echo "Error: state file not found at $STATE_FILE" >&2
173
+ echo "The playbook run may have expired (e.g. temp dir was cleared), or already completed." >&2
174
+ exit 2
175
+ fi
176
+
177
+ PY_SCRIPT="$(mktemp -t run-playbook-step-advance-XXXXXX.py)"
178
+ trap 'rm -f "$PY_SCRIPT"' EXIT
179
+
180
+ cat > "$PY_SCRIPT" <<'PY'
181
+ import json
182
+ import os
183
+ import sys
184
+
185
+ state_file_path = sys.argv[1]
186
+ outcome = sys.argv[2]
187
+ note = sys.argv[3] if len(sys.argv) > 3 and sys.argv[3] != "" else None
188
+
189
+ try:
190
+ with open(state_file_path, encoding="utf-8") as f:
191
+ state = json.load(f)
192
+ except Exception as e:
193
+ print(f"Error: could not read/parse state file at {state_file_path}: {e}", file=sys.stderr)
194
+ sys.exit(2)
195
+
196
+ if state.get("halted"):
197
+ print(
198
+ f"Error: this playbook run already halted on step '{state.get('failed_step')}'. "
199
+ "A further advance() call is rejected -- no silent resumption after a halt. "
200
+ "Start a new run with run-playbook-step.sh init if you want to retry.",
201
+ file=sys.stderr,
202
+ )
203
+ sys.exit(3)
204
+
205
+ steps = state["steps"]
206
+ idx = state["current_index"]
207
+
208
+ if idx >= len(steps):
209
+ print(f"Error: state file at {state_file_path} has no current step (already complete).", file=sys.stderr)
210
+ sys.exit(2)
211
+
212
+ current_step = steps[idx]
213
+
214
+ if outcome == "failed":
215
+ state["halted"] = True
216
+ state["failed_step"] = current_step
217
+ state["failed_note"] = note
218
+ never_run = steps[idx + 1:]
219
+
220
+ with open(state_file_path, "w", encoding="utf-8") as f:
221
+ json.dump(state, f, indent=2)
222
+ f.write("\n")
223
+
224
+ print(json.dumps({
225
+ "status": "halted",
226
+ "playbook_id": state["playbook_id"],
227
+ "name": state["playbook_name"],
228
+ "completed": state["completed"],
229
+ "failed_step": current_step,
230
+ "failed_note": note,
231
+ "never_run": never_run,
232
+ }))
233
+ sys.exit(0)
234
+
235
+ # outcome == "passed"
236
+ state["completed"].append(current_step)
237
+ state["current_index"] = idx + 1
238
+
239
+ if state["current_index"] >= len(steps):
240
+ result = {
241
+ "status": "complete",
242
+ "playbook_id": state["playbook_id"],
243
+ "name": state["playbook_name"],
244
+ "completed": state["completed"],
245
+ }
246
+ try:
247
+ os.remove(state_file_path)
248
+ except OSError:
249
+ pass
250
+ print(json.dumps(result))
251
+ sys.exit(0)
252
+
253
+ with open(state_file_path, "w", encoding="utf-8") as f:
254
+ json.dump(state, f, indent=2)
255
+ f.write("\n")
256
+
257
+ next_step = steps[state["current_index"]]
258
+ print(json.dumps({
259
+ "status": "step_ready",
260
+ "step": next_step,
261
+ "step_index": state["current_index"] + 1,
262
+ "total_steps": len(steps),
263
+ }))
264
+ print(f"STATE_FILE: {state_file_path}", file=sys.stderr)
265
+ PY
266
+
267
+ python3 "$PY_SCRIPT" "$STATE_FILE" "$OUTCOME" "$NOTE"
268
+ exit $?
269
+
270
+ else
271
+ echo "Error: unrecognized subcommand '$SUBCOMMAND' (expected 'init' or 'advance')" >&2
272
+ exit 2
273
+ fi