@jenga-ai/agent 3.0.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
@@ -97,6 +97,38 @@ Neither layer inspects a skill's *contents*; both defend the invocation-matching
97
97
  | Message is a general coding or project question | Answer directly |
98
98
  | Ambiguous — could be skill or free-form | Prefer the skill; open and execute its `SKILL.md` rather than describing it |
99
99
 
100
+ ### Sub-Agent Delegation (`prefered_agent`)
101
+
102
+ Some skills declare a `metadata.prefered_agent: <agent_name>` field in their `SKILL.md`
103
+ frontmatter. This names a sub-agent persona — a file under `agents/` (`scrum-master`,
104
+ `developer`, `tester`) — that should execute the skill, the same delegation Claude Code performs
105
+ natively for the same skill via root `CLAUDE.md`'s "Skill Frontmatter" section.
106
+
107
+ **Copilot CLI has its own native custom-agent-loading mechanism for this.** The `--agent <name>`
108
+ CLI flag and the interactive `/agent [name]` command both load a custom agent-definition file
109
+ discovered from `.github/agents/*.md` or `.claude/agents/*.md`, keyed by that file's frontmatter
110
+ `name:` field — not its filename. Jenga's `agents/*.md` personas are mirrored into both of those
111
+ paths by `/self-sync` (`skills/self-sync/scripts/run.js`); cite `.github/agents/` as the
112
+ canonical path for this purpose, parallel to how "How Jenga Works" above cites `.agents/skills/`
113
+ as canonical for skills. `.agents/agents/` is also mirrored (for reasons unrelated to Copilot)
114
+ but is a **confirmed Copilot-discovery dead-end** — the native agent loader does not read it;
115
+ never rely on it when resolving a `prefered_agent`.
116
+
117
+ When a matched skill's frontmatter has `metadata.prefered_agent: <agent_name>`:
118
+
119
+ 1. Before executing the skill's instructions, load that persona by invoking `--agent
120
+ <agent_name>` (if starting a new `copilot` invocation) or the interactive `/agent
121
+ <agent_name>` command (if already in an interactive session).
122
+ 2. Then proceed with the skill's instructions exactly as written, per the "Skill Routing"
123
+ section above — the loaded agent persona governs *how* the skill executes, not *whether* it
124
+ does, and does not change which `SKILL.md` file gets opened or the allow-list check that
125
+ precedes it.
126
+ 3. If a skill's frontmatter has no `prefered_agent` field, execute it directly with no agent
127
+ switch.
128
+
129
+ Valid `<agent_name>` values match the agent definitions under `agents/` (frontmatter `name:`
130
+ field, not the filename): `scrum-master`, `developer`, `tester`.
131
+
100
132
  ### Available Skills
101
133
 
102
134
  {{SKILL_LIST}}