@jenga-ai/agent 3.2.0 → 3.4.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 +52 -12
- package/agents/developer.md +16 -1
- package/agents/scrum-master.md +1 -0
- package/bin/jenga.js +10 -0
- package/lib/commands/dashboard.js +92 -0
- package/lib/skill-allow-list.json +6 -2
- package/package.json +21 -2
- package/project/app/api/lib/resolve-project-root.js +120 -0
- package/project/app/api/package.json +16 -0
- package/project/app/api/parsers/architecture.js +72 -0
- package/project/app/api/parsers/board.js +141 -0
- package/project/app/api/parsers/documentation.js +125 -0
- package/project/app/api/parsers/git-log.js +52 -0
- package/project/app/api/parsers/ideas.js +62 -0
- package/project/app/api/parsers/knowledge-graph.js +73 -0
- package/project/app/api/parsers/lib/markdown-dir-reader.js +163 -0
- package/project/app/api/parsers/rapports.js +148 -0
- package/project/app/api/parsers/todo.js +179 -0
- package/project/app/api/response.js +47 -0
- package/project/app/api/routes/architecture.js +23 -0
- package/project/app/api/routes/board.js +46 -0
- package/project/app/api/routes/documentation.js +24 -0
- package/project/app/api/routes/health.js +25 -0
- package/project/app/api/routes/history.js +55 -0
- package/project/app/api/routes/rapports.js +24 -0
- package/project/app/api/scripts/capture-snapshot.js +294 -0
- package/project/app/api/server.js +112 -0
- package/project/app/api/types.js +40 -0
- package/project/app/package.json +21 -0
- package/project/app/ui/dist/assets/index-7fj-vllY.js +104 -0
- package/project/app/ui/dist/assets/index-CdK3Qrep.css +1 -0
- package/project/app/ui/dist/index.html +13 -0
- package/project/app/ui/package.json +23 -0
- package/project/app/ui/scripts/build-snapshot-html.cjs +214 -0
- package/project/app/ui/scripts/dashboard-open.cjs +88 -0
- package/project/app/ui/scripts/dashboard-start.cjs +87 -0
- package/scripts/acquire-concurrency-slot.sh +220 -0
- package/scripts/compute-deploy-reconcile.sh +439 -0
- package/scripts/jenga-permission-level-switch.sh +19 -3
- package/scripts/mark-deployed.sh +532 -0
- package/scripts/populate-knowledge-graph.js +429 -0
- package/scripts/release-concurrency-slot.sh +129 -0
- package/scripts/validate-board.sh +60 -2
- package/scripts/verify-consumer-install.sh +470 -0
- package/skills/j-cloud-connect/SKILL.md +95 -0
- package/skills/j-cloud-connect/scripts/configure-backend.sh +267 -0
- package/skills/j-cloud-connect/scripts/install-rclone.sh +153 -0
- package/skills/j-dashboard/SKILL.md +144 -0
- package/skills/j-dashboard/scripts/launch.sh +121 -0
- package/skills/j-dashboard/scripts/resolve-app-dir.sh +164 -0
- package/skills/j-dashboard/scripts/snapshot.sh +267 -0
- package/skills/j-dashboard-share/SKILL.md +96 -0
- package/skills/j-dashboard-share/scripts/upload-snapshot.sh +173 -0
- package/skills/j-playbook/SKILL.md +12 -0
- package/skills/j-playbook-new/SKILL.md +155 -0
- package/skills/j-playbook-new/scripts/playbook-new.sh +332 -0
- package/skills/j-publish/scripts/npm_ci_pipeline.sh +6 -0
- package/skills/jenga/scripts/load-nl-catalog.js +22 -6
- package/skills/jenga/scripts/load-playbooks.sh +123 -24
- package/templates/SCRUM_BOARD_SCHEMA.md +18 -6
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# -----------------------------------------------------------------------------
|
|
3
|
+
# skills/j-dashboard-share/scripts/upload-snapshot.sh
|
|
4
|
+
#
|
|
5
|
+
# Uploads a local dashboard snapshot HTML file (as produced by j-dashboard's
|
|
6
|
+
# `scripts/snapshot.sh`) to a configured rclone remote via `rclone copyto`, at
|
|
7
|
+
# a templated, Drive-side (remote-side) destination path:
|
|
8
|
+
#
|
|
9
|
+
# JengaAI/<repo-directory-name>/<datetime>-board-snapshot.html
|
|
10
|
+
#
|
|
11
|
+
# - <repo-directory-name> is the basename of the repository root, derived
|
|
12
|
+
# via `basename "$(git rev-parse --show-toplevel)"` — never hardcoded to
|
|
13
|
+
# any specific project.
|
|
14
|
+
# - <datetime> is the actual UTC run time, formatted `%Y%m%dT%H%M%SZ`
|
|
15
|
+
# (compact ISO 8601-ish, sortable, filesystem/URL-safe).
|
|
16
|
+
#
|
|
17
|
+
# This destination path is entirely independent of the LOCAL snapshot
|
|
18
|
+
# filename (whatever `snapshot.sh --out` wrote it as) — this script only
|
|
19
|
+
# reads the local file's bytes and uploads them under its own remote-side
|
|
20
|
+
# name.
|
|
21
|
+
#
|
|
22
|
+
# Before attempting any upload, this script verifies the target rclone
|
|
23
|
+
# remote is actually configured (present in `rclone listremotes`). If it is
|
|
24
|
+
# not, this script does NOT attempt the upload and does NOT let rclone
|
|
25
|
+
# surface its own raw error — it dies with an actionable message pointing
|
|
26
|
+
# the user at `j.cloud-connect` (E60_S01) to configure a remote first.
|
|
27
|
+
#
|
|
28
|
+
# HARD SCOPE BOUNDARY: this script uploads ONLY. It never invokes
|
|
29
|
+
# `rclone link` or any other link-creating/sharing subcommand, anywhere in
|
|
30
|
+
# its logic — that is a deliberate, separate, manual action reserved for the
|
|
31
|
+
# user (see E47_S05's story decision). Do not add one.
|
|
32
|
+
#
|
|
33
|
+
# This script does not create or wire up the `j-dashboard-share` skill
|
|
34
|
+
# itself (SKILL.md) — that is a separate task (E47_S05_T02). It assumes a
|
|
35
|
+
# remote has already been configured via `j.cloud-connect`
|
|
36
|
+
# (skills/j-cloud-connect/scripts/{install-rclone.sh,configure-backend.sh},
|
|
37
|
+
# E60_S01) and that `rclone` is already on PATH.
|
|
38
|
+
#
|
|
39
|
+
# Invoked via `bash`, not executed directly: this script intentionally ships
|
|
40
|
+
# without the executable bit, matching the convention already documented in
|
|
41
|
+
# skills/j-cloud-connect/scripts/install-rclone.sh and
|
|
42
|
+
# skills/j-dashboard/scripts/resolve-app-dir.sh. Whatever wires this into the
|
|
43
|
+
# j-dashboard-share skill (E47_S05_T02) should invoke it as
|
|
44
|
+
# `bash skills/j-dashboard-share/scripts/upload-snapshot.sh`.
|
|
45
|
+
#
|
|
46
|
+
# Usage:
|
|
47
|
+
# upload-snapshot.sh --file <local-snapshot-path> --remote <remote-name>
|
|
48
|
+
#
|
|
49
|
+
# --file <path> Path to the local snapshot HTML file to upload (e.g.
|
|
50
|
+
# the output of `j.dashboard --snapshot`). Must exist
|
|
51
|
+
# and be non-empty.
|
|
52
|
+
# --remote <name> Name of an already-configured rclone remote (without a
|
|
53
|
+
# trailing colon), e.g. `gdrive`. Checked against
|
|
54
|
+
# `rclone listremotes` before any upload is attempted.
|
|
55
|
+
#
|
|
56
|
+
# Exit codes:
|
|
57
|
+
# 0 Upload succeeded. The full remote destination path is printed on
|
|
58
|
+
# stdout as the last line.
|
|
59
|
+
# 1 Any failure: bad arguments, missing/empty local file, `rclone` not on
|
|
60
|
+
# PATH, not inside a git repository, the target remote is not
|
|
61
|
+
# configured (points the user at `j.cloud-connect`), or `rclone copyto`
|
|
62
|
+
# itself failed. A human-readable reason is always printed to stderr.
|
|
63
|
+
# -----------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
set -euo pipefail
|
|
66
|
+
|
|
67
|
+
usage() {
|
|
68
|
+
cat <<'EOF'
|
|
69
|
+
Usage: upload-snapshot.sh --file <local-snapshot-path> --remote <remote-name>
|
|
70
|
+
|
|
71
|
+
Uploads a local dashboard snapshot HTML file to a configured rclone remote
|
|
72
|
+
via `rclone copyto`, at the templated destination path:
|
|
73
|
+
|
|
74
|
+
JengaAI/<repo-directory-name>/<datetime>-board-snapshot.html
|
|
75
|
+
|
|
76
|
+
--file <path> Path to the local snapshot HTML file to upload. Must
|
|
77
|
+
exist and be non-empty.
|
|
78
|
+
--remote <name> Name of an already-configured rclone remote (no
|
|
79
|
+
trailing colon). If not configured, this script tells
|
|
80
|
+
you to run j.cloud-connect instead of attempting the
|
|
81
|
+
upload.
|
|
82
|
+
|
|
83
|
+
Upload only — never runs `rclone link` or any other sharing/link-creating
|
|
84
|
+
command.
|
|
85
|
+
EOF
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
die() {
|
|
89
|
+
echo "Error: $*" >&2
|
|
90
|
+
exit 1
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# -----------------------------------------------------------------------------
|
|
94
|
+
# Argument parsing.
|
|
95
|
+
# -----------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
LOCAL_FILE=""
|
|
98
|
+
REMOTE_NAME=""
|
|
99
|
+
|
|
100
|
+
while [ "$#" -gt 0 ]; do
|
|
101
|
+
case "$1" in
|
|
102
|
+
-h|--help)
|
|
103
|
+
usage
|
|
104
|
+
exit 0
|
|
105
|
+
;;
|
|
106
|
+
--file)
|
|
107
|
+
[ "$#" -ge 2 ] || die "--file requires a value"
|
|
108
|
+
LOCAL_FILE="$2"
|
|
109
|
+
shift 2
|
|
110
|
+
;;
|
|
111
|
+
--remote)
|
|
112
|
+
[ "$#" -ge 2 ] || die "--remote requires a value"
|
|
113
|
+
REMOTE_NAME="$2"
|
|
114
|
+
shift 2
|
|
115
|
+
;;
|
|
116
|
+
*)
|
|
117
|
+
usage >&2
|
|
118
|
+
die "unknown argument: $1"
|
|
119
|
+
;;
|
|
120
|
+
esac
|
|
121
|
+
done
|
|
122
|
+
|
|
123
|
+
[ -n "$LOCAL_FILE" ] || { usage >&2; die "--file is required"; }
|
|
124
|
+
[ -n "$REMOTE_NAME" ] || { usage >&2; die "--remote is required"; }
|
|
125
|
+
|
|
126
|
+
# -----------------------------------------------------------------------------
|
|
127
|
+
# Preflight — local file, rclone availability, git repository.
|
|
128
|
+
# -----------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
[ -f "$LOCAL_FILE" ] || die "local snapshot file not found: $LOCAL_FILE"
|
|
131
|
+
[ -s "$LOCAL_FILE" ] || die "local snapshot file is empty: $LOCAL_FILE"
|
|
132
|
+
|
|
133
|
+
command -v rclone >/dev/null 2>&1 || die "rclone is not on PATH. Install it first (see skills/j-cloud-connect/scripts/install-rclone.sh) and re-run."
|
|
134
|
+
|
|
135
|
+
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || die "not inside a git repository — cannot derive <repo-directory-name> for the destination path. Run this script from within the project's repository."
|
|
136
|
+
REPO_DIR_NAME="$(basename "$REPO_ROOT")"
|
|
137
|
+
|
|
138
|
+
# -----------------------------------------------------------------------------
|
|
139
|
+
# Step 1 — Verify the target remote is actually configured BEFORE attempting
|
|
140
|
+
# any upload. Never let a raw rclone error surface for "remote not
|
|
141
|
+
# configured" — point the user at j.cloud-connect instead.
|
|
142
|
+
# -----------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
REMOTES_OUTPUT="$(rclone listremotes 2>&1)" || die "'rclone listremotes' failed: $REMOTES_OUTPUT"
|
|
145
|
+
|
|
146
|
+
if ! printf '%s\n' "$REMOTES_OUTPUT" | grep -Fxq "${REMOTE_NAME}:"; then
|
|
147
|
+
die "remote '$REMOTE_NAME' is not configured in rclone. Run j.cloud-connect first to set it up, then re-run this upload."
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
# -----------------------------------------------------------------------------
|
|
151
|
+
# Step 2 — Build the templated destination path.
|
|
152
|
+
# -----------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
DATETIME="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
155
|
+
DEST_RELATIVE_PATH="JengaAI/${REPO_DIR_NAME}/${DATETIME}-board-snapshot.html"
|
|
156
|
+
DEST="${REMOTE_NAME}:${DEST_RELATIVE_PATH}"
|
|
157
|
+
|
|
158
|
+
# -----------------------------------------------------------------------------
|
|
159
|
+
# Step 3 — Upload. stdio inherited (no capture/reformatting), matching this
|
|
160
|
+
# repo's j-cloud-connect script conventions. Upload only: this script never
|
|
161
|
+
# calls `rclone link` or any other sharing/link-creating command.
|
|
162
|
+
# -----------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
echo "Uploading '$LOCAL_FILE' to '$DEST'..."
|
|
165
|
+
|
|
166
|
+
COPY_EXIT=0
|
|
167
|
+
rclone copyto "$LOCAL_FILE" "$DEST" || COPY_EXIT=$?
|
|
168
|
+
if [ "$COPY_EXIT" -ne 0 ]; then
|
|
169
|
+
die "'rclone copyto $LOCAL_FILE $DEST' exited with status $COPY_EXIT. Upload did not succeed — see rclone's output above for the reason."
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
echo "Upload succeeded."
|
|
173
|
+
echo "$DEST"
|
|
@@ -32,6 +32,18 @@ this skill is id resolution (step 1 below).
|
|
|
32
32
|
|
|
33
33
|
## Instructions
|
|
34
34
|
|
|
35
|
+
0. **Bare invocation — no id given.** If this skill was invoked with no argument at all, do not
|
|
36
|
+
proceed to step 1. Instead:
|
|
37
|
+
a. Invoke `skills/jenga/scripts/load-playbooks.sh` with no arguments (its existing full-catalog
|
|
38
|
+
mode — the same call step 1's `"not_found"` branch already uses for its "did you mean"
|
|
39
|
+
nudge; no new script is introduced for this).
|
|
40
|
+
b. Render the returned JSON array as a Markdown table with columns `Id`, `Name`, `Source`, and
|
|
41
|
+
`Steps`. For the `Source` column, render `Built-in` for a `source` field of `"builtin"` and
|
|
42
|
+
`Project` for `"project"`. For the `Steps` column, render each entry in that playbook's
|
|
43
|
+
`steps` array joined by `->`: a bare string step renders as itself; a StepObject step
|
|
44
|
+
renders its `skill` or `playbook` field value (whichever is present).
|
|
45
|
+
c. Halt this invocation after rendering the table — do not proceed to step 1.
|
|
46
|
+
|
|
35
47
|
1. **Resolve the id** — invoke `skills/jenga/scripts/load-playbooks.sh lookup "<id>"`
|
|
36
48
|
(`E53_S06_T02`), where `<id>` is this skill's argument. Branch on the returned `status` field:
|
|
37
49
|
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: j.playbook-new
|
|
3
|
+
description: Guided wizard that walks you through authoring a new project-local playbook — id, name, description, keywords, examples, and an ordered list of skills — validates every input against the real catalogs, and self-validates the written file before reporting success.
|
|
4
|
+
keywords:
|
|
5
|
+
- new playbook
|
|
6
|
+
- create playbook
|
|
7
|
+
- author playbook
|
|
8
|
+
- project playbook
|
|
9
|
+
- custom playbook
|
|
10
|
+
examples:
|
|
11
|
+
- "I want to create my own playbook"
|
|
12
|
+
- "help me author a new project-local playbook"
|
|
13
|
+
- "set up a custom workflow chain for my project"
|
|
14
|
+
- "j.playbook-new"
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Playbook New — Guided Playbook-Authoring Wizard
|
|
18
|
+
|
|
19
|
+
## Purpose
|
|
20
|
+
|
|
21
|
+
`skills/jenga/scripts/load-playbooks.sh` merges two playbook sources into one catalog: the
|
|
22
|
+
framework-owned `skills/jenga/playbooks/` tree, and a project-owned `project/.playbooks/`
|
|
23
|
+
directory (`E53_S09_T01`). This skill is the guided authoring path for the second source — it
|
|
24
|
+
walks the user step by step through the required fields, validates each one against the real,
|
|
25
|
+
live catalogs (never a hand-maintained list of skill names or existing playbook ids), writes
|
|
26
|
+
`project/.playbooks/<id>.json`, and re-validates the just-written file via
|
|
27
|
+
`skills/jenga/scripts/load-playbooks.sh lookup` before ever declaring success. Hand-editing the
|
|
28
|
+
written JSON file remains the escape hatch for anything this wizard doesn't author (see the "v1
|
|
29
|
+
scope cut" note below).
|
|
30
|
+
|
|
31
|
+
**All deterministic work — slug/uniqueness validation, catalog lookups, and the JSON write
|
|
32
|
+
itself — lives in `skills/j-playbook-new/scripts/playbook-new.sh`** (per `CLAUDE.md`'s "Scripts
|
|
33
|
+
Over Inline Logic" principle). This skill's own job is only to run the conversational loop around
|
|
34
|
+
that script and interpret its JSON results — it never re-implements any validation or catalog
|
|
35
|
+
logic inline.
|
|
36
|
+
|
|
37
|
+
**v1 scope cut (deliberate, matches `E53_S09`'s story-level scope cut):** this wizard authors
|
|
38
|
+
plain, ordered, bare-string skill-name chains only — no `forward_from`, `resolve`, `conditional`,
|
|
39
|
+
or `playbook`-type (composition) `StepObject` fields. Hand-edit the written file directly if you
|
|
40
|
+
need any of those.
|
|
41
|
+
|
|
42
|
+
**Scope note — keywords and examples.** The playbook schema
|
|
43
|
+
(`skills/jenga/playbooks/schema.json`) requires every playbook to carry non-empty `keywords` and
|
|
44
|
+
`examples` arrays, exactly like every existing built-in playbook file already does — these are
|
|
45
|
+
what let `/jenga`'s own natural-language matching (`match-playbook.sh`) ever propose this playbook
|
|
46
|
+
from a free-text request. This wizard therefore asks for both, in addition to id/name/description/
|
|
47
|
+
skill-list, so the result is a genuinely useful, matchable playbook rather than one that merely
|
|
48
|
+
parses.
|
|
49
|
+
|
|
50
|
+
## Instructions
|
|
51
|
+
|
|
52
|
+
1. **Ask for an id** — a short, stable, kebab-case identifier (e.g. `my-release-flow`). Validate
|
|
53
|
+
it by running:
|
|
54
|
+
```
|
|
55
|
+
skills/j-playbook-new/scripts/playbook-new.sh validate-id "<id>"
|
|
56
|
+
```
|
|
57
|
+
Parse the single JSON object printed to stdout:
|
|
58
|
+
- `{"valid": true}` — continue to step 2.
|
|
59
|
+
- `{"valid": false, "reason": "..."}` — show the `reason` to the user verbatim and re-prompt
|
|
60
|
+
for a different id. Do not proceed until a validation call returns `valid: true`.
|
|
61
|
+
|
|
62
|
+
2. **Ask for a `name`** — a short, human-readable display name (e.g. "My Release Flow"), shown to
|
|
63
|
+
users in confirmation prompts and routing output, mirroring every existing playbook's `name`
|
|
64
|
+
field. No script validation needed beyond "non-empty" — re-prompt if the user gives an empty
|
|
65
|
+
answer.
|
|
66
|
+
|
|
67
|
+
3. **Ask for a `description`** — one sentence explaining what this playbook accomplishes
|
|
68
|
+
end-to-end. Re-prompt if empty.
|
|
69
|
+
|
|
70
|
+
4. **Ask for `keywords`** — one or more short phrases (1-3 words each) a user might type that
|
|
71
|
+
should match this playbook, mirroring every existing playbook's `keywords` field (see
|
|
72
|
+
`skills/jenga/playbooks/brainstorm-to-mirror.json` for a concrete example of the expected
|
|
73
|
+
shape and specificity). Accept them one at a time or as a single comma-separated batch —
|
|
74
|
+
your judgment, whichever the user's response shape suggests. Require at least one.
|
|
75
|
+
|
|
76
|
+
5. **Ask for `examples`** — one or more natural-language example prompts a user might type that
|
|
77
|
+
should resolve to this playbook (again, mirror the existing built-in playbooks' style and
|
|
78
|
+
level of specificity). Require at least one; at least one example should plausibly span the
|
|
79
|
+
full breadth of the playbook's steps, not just its first one, so it's distinguishable from a
|
|
80
|
+
single-skill match — use your judgment coaching the user toward this if their first example is
|
|
81
|
+
too narrow.
|
|
82
|
+
|
|
83
|
+
6. **Ask for an ordered list of skill names** — one at a time, or as a single ordered batch —
|
|
84
|
+
your judgment based on how the user responds. For **each** name entered, validate it by
|
|
85
|
+
running:
|
|
86
|
+
```
|
|
87
|
+
skills/j-playbook-new/scripts/playbook-new.sh validate-skill "<name>"
|
|
88
|
+
```
|
|
89
|
+
Parse the JSON result:
|
|
90
|
+
- `{"valid": true}` — accept it into the ordered list and continue.
|
|
91
|
+
- `{"valid": false, "reason": "..."}` — show the `reason` verbatim and re-prompt for that
|
|
92
|
+
position in the list (do not silently drop it or guess a correction).
|
|
93
|
+
|
|
94
|
+
Once the user signals they're done adding skills, require **at least 2** total (a playbook is a
|
|
95
|
+
chain — a single-skill "playbook" isn't a meaningful use of this mechanism). If fewer than 2
|
|
96
|
+
were entered, tell the user this and continue prompting for more.
|
|
97
|
+
|
|
98
|
+
7. **Write the playbook.** Assemble the JSON payload from steps 1-6:
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"id": "<id>",
|
|
102
|
+
"name": "<name>",
|
|
103
|
+
"description": "<description>",
|
|
104
|
+
"keywords": ["<keyword 1>", "..."],
|
|
105
|
+
"examples": ["<example 1>", "..."],
|
|
106
|
+
"steps": ["<skill 1>", "<skill 2>", "..."]
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
Pipe it to stdin of:
|
|
110
|
+
```
|
|
111
|
+
skills/j-playbook-new/scripts/playbook-new.sh write
|
|
112
|
+
```
|
|
113
|
+
Parse the JSON result:
|
|
114
|
+
- `{"written": true, "path": "..."}` — continue to step 8.
|
|
115
|
+
- `{"written": false, "reason": "..."}` — show the `reason` to the user verbatim. This should
|
|
116
|
+
only happen if something changed between validation and write (e.g. a race, or a shape
|
|
117
|
+
issue this wizard's own prompts didn't already catch) — do not silently retry; tell the user
|
|
118
|
+
what failed and, if it's fixable (e.g. the id collided after all), loop back to the relevant
|
|
119
|
+
earlier step.
|
|
120
|
+
|
|
121
|
+
8. **Self-validate before declaring success — never skip this step.** Run:
|
|
122
|
+
```
|
|
123
|
+
skills/jenga/scripts/load-playbooks.sh lookup "<id>"
|
|
124
|
+
```
|
|
125
|
+
Branch on the returned `status`:
|
|
126
|
+
- **`"valid"`** — report success to the user: the playbook was written to
|
|
127
|
+
`project/.playbooks/<id>.json` and is confirmed loadable. Mention it can now be invoked via
|
|
128
|
+
`j.playbook <id>` or matched naturally through `j.jenga`.
|
|
129
|
+
- **`"invalid"`** — report the `reason` field to the user **verbatim** — never a generic
|
|
130
|
+
failure message. This is a real defect (the write succeeded but load-time validation still
|
|
131
|
+
rejects it) — do not claim success.
|
|
132
|
+
- **`"not_found"`** — report to the user that the write appears to have silently failed (the
|
|
133
|
+
file the wizard just wrote could not be found by the loader) — this would indicate an
|
|
134
|
+
environment problem (e.g. a different project root being resolved by the two scripts), not a
|
|
135
|
+
normal outcome. Never claim success.
|
|
136
|
+
|
|
137
|
+
Under no circumstances report success to the user without having seen `"status": "valid"` from
|
|
138
|
+
this exact call.
|
|
139
|
+
|
|
140
|
+
## Edge Cases
|
|
141
|
+
|
|
142
|
+
- **The user wants to add `forward_from`/`resolve`/`conditional`/composition to a step.** Tell
|
|
143
|
+
them this wizard doesn't author those fields (v1 scope cut) and that they can hand-edit
|
|
144
|
+
`project/.playbooks/<id>.json` afterward — the schema supports these fields identically
|
|
145
|
+
regardless of which directory a playbook file lives in.
|
|
146
|
+
- **The id collides with a project playbook that already exists on disk but is currently
|
|
147
|
+
invalid** (e.g. a hand-edited file with a JSON syntax error) — `validate-id` still rejects it (it
|
|
148
|
+
checks raw file existence, not just catalog membership) rather than silently overwriting a file
|
|
149
|
+
the user may not realize is broken.
|
|
150
|
+
- **A skill name the user enters exists under multiple forms in the catalog** (e.g. both a bare
|
|
151
|
+
and a `j-`-prefixed directory, during this repo's ongoing `E50` naming-contract transition) —
|
|
152
|
+
accept whichever exact form the user typed if `validate-skill` reports it valid; this wizard
|
|
153
|
+
does not impose a preference between forms the catalog itself doesn't distinguish.
|
|
154
|
+
- **The user cancels mid-wizard** — do not write anything; only step 7 ever touches disk, and only
|
|
155
|
+
once a complete, locally-validated payload exists.
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ---------------------------------------------------------------------------
|
|
3
|
+
# skills/j-playbook-new/scripts/playbook-new.sh
|
|
4
|
+
#
|
|
5
|
+
# Deterministic helper behind the `j.playbook-new` guided wizard (E53_S09_T02). Per CLAUDE.md's
|
|
6
|
+
# "Scripts Over Inline Logic" principle, every mechanical step of the wizard -- id slug/uniqueness
|
|
7
|
+
# validation, skill-name validation against the real catalog, and the JSON write itself -- lives
|
|
8
|
+
# here rather than as inline agent prose in skills/j-playbook-new/SKILL.md. The agent driving the
|
|
9
|
+
# wizard calls this script once per step and interprets its JSON result; it never re-implements
|
|
10
|
+
# any of this logic itself.
|
|
11
|
+
#
|
|
12
|
+
# Reuses, never re-implements, the two existing single-source-of-truth catalogs:
|
|
13
|
+
# - skills/jenga/scripts/load-playbooks.sh (merged builtin+project playbook catalog, E53_S09_T01)
|
|
14
|
+
# - skills/jenga/scripts/load-nl-catalog.sh (the real, generated skill catalog /jenga's own
|
|
15
|
+
# natural-language matching already uses)
|
|
16
|
+
#
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# USAGE
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# playbook-new.sh validate-id <id>
|
|
21
|
+
#
|
|
22
|
+
# Checks <id> is slug-safe (the same pattern skills/jenga/playbooks/schema.json requires:
|
|
23
|
+
# ^[a-z0-9]+(-[a-z0-9]+)*$) and does not collide with any id already present in the merged catalog
|
|
24
|
+
# (skills/jenga/scripts/load-playbooks.sh, no arguments -- built-in and project sources both) OR an
|
|
25
|
+
# existing (possibly currently invalid, and therefore catalog-invisible) file at
|
|
26
|
+
# project/.playbooks/<id>.json. Prints exactly one JSON object to stdout:
|
|
27
|
+
# {"valid": true}
|
|
28
|
+
# {"valid": false, "reason": "<human-readable reason>"}
|
|
29
|
+
# Exit 0 for BOTH outcomes -- mirrors load-playbooks.sh's own `lookup` mode convention of
|
|
30
|
+
# reserving a non-zero exit for usage/setup errors only, never for a normal negative validation
|
|
31
|
+
# result the caller is expected to branch on.
|
|
32
|
+
#
|
|
33
|
+
# playbook-new.sh validate-skill <name>
|
|
34
|
+
#
|
|
35
|
+
# Checks <name> is a real, currently-loadable skill per skills/jenga/scripts/load-nl-catalog.sh's
|
|
36
|
+
# generated catalog -- the exact source /jenga's own natural-language matching already uses, never
|
|
37
|
+
# a hand-maintained list. Prints:
|
|
38
|
+
# {"valid": true}
|
|
39
|
+
# {"valid": false, "reason": "..."}
|
|
40
|
+
# Exit 0 for both outcomes, same convention as validate-id.
|
|
41
|
+
#
|
|
42
|
+
# playbook-new.sh write
|
|
43
|
+
#
|
|
44
|
+
# Reads a single JSON object from stdin:
|
|
45
|
+
# {"id": "...", "name": "...", "description": "...", "keywords": ["..."],
|
|
46
|
+
# "examples": ["..."], "steps": ["...", "..."]}
|
|
47
|
+
# Pre-validates it against the same required-field / non-empty-list / steps-shape rules
|
|
48
|
+
# skills/jenga/playbooks/schema.json declares (id slug pattern, all six required fields present,
|
|
49
|
+
# keywords/examples non-empty lists of non-empty strings, steps a list of >= 2 non-empty strings --
|
|
50
|
+
# this wizard authors bare-string steps only, per this task's v1 scope cut) BEFORE writing anything
|
|
51
|
+
# to disk. This is a defensive, redundant pre-check only -- the actual source of truth for validity
|
|
52
|
+
# remains load-playbooks.sh's own load-time validation, which the wizard's own self-validation step
|
|
53
|
+
# (SKILL.md step 8) always runs afterward regardless of this result. Creates project/.playbooks/ if
|
|
54
|
+
# it does not yet exist, and refuses to overwrite an already-existing file for the same id. On
|
|
55
|
+
# success, writes project/.playbooks/<id>.json (pretty-printed) and prints
|
|
56
|
+
# {"written": true, "path": "<absolute path>"}. On any shape/overwrite failure, prints
|
|
57
|
+
# {"written": false, "reason": "..."} and writes nothing. Exit 0 for both outcomes.
|
|
58
|
+
#
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# PROJECT ROOT RESOLUTION
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Honors JENGA_PLAYBOOKS_TEST_ROOT -- the SAME override variable load-playbooks.sh itself defines
|
|
63
|
+
# (see that script's header "TESTING OVERRIDE"), not a second, script-specific one -- so a fixture
|
|
64
|
+
# pointing this script at a throwaway project root also makes load-playbooks.sh (invoked internally
|
|
65
|
+
# by validate-id, and by the wizard's own later self-validation step) resolve project/.playbooks/
|
|
66
|
+
# under that same root. Never set this variable in a real invocation.
|
|
67
|
+
#
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# EXIT CODES
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# 0 a result JSON object was printed to stdout (whether valid:true/false or written:true/false)
|
|
72
|
+
# 2 usage error, or a real setup failure (missing python3, the sibling load-playbooks.sh /
|
|
73
|
+
# load-nl-catalog.sh scripts could not be located, one of them exited non-zero, or the
|
|
74
|
+
# project/.playbooks/ directory could not be created)
|
|
75
|
+
#
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
set -euo pipefail
|
|
79
|
+
|
|
80
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
81
|
+
LOAD_PLAYBOOKS="$SCRIPT_DIR/../../jenga/scripts/load-playbooks.sh"
|
|
82
|
+
LOAD_NL_CATALOG="$SCRIPT_DIR/../../jenga/scripts/load-nl-catalog.sh"
|
|
83
|
+
|
|
84
|
+
if [ ! -f "$LOAD_PLAYBOOKS" ]; then
|
|
85
|
+
echo "Error: could not locate load-playbooks.sh at $LOAD_PLAYBOOKS" >&2
|
|
86
|
+
exit 2
|
|
87
|
+
fi
|
|
88
|
+
if [ ! -f "$LOAD_NL_CATALOG" ]; then
|
|
89
|
+
echo "Error: could not locate load-nl-catalog.sh at $LOAD_NL_CATALOG" >&2
|
|
90
|
+
exit 2
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
94
|
+
echo "Error: python3 is required by playbook-new.sh" >&2
|
|
95
|
+
exit 2
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
if [ -n "${JENGA_PLAYBOOKS_TEST_ROOT:-}" ]; then
|
|
99
|
+
# Test-only override -- see header "PROJECT ROOT RESOLUTION" above. Never set in a real
|
|
100
|
+
# invocation.
|
|
101
|
+
PROJECT_DIR="$JENGA_PLAYBOOKS_TEST_ROOT"
|
|
102
|
+
else
|
|
103
|
+
PROJECT_DIR="${JENGA_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || pwd)}}"
|
|
104
|
+
fi
|
|
105
|
+
PLAYBOOKS_TARGET_DIR="$PROJECT_DIR/project/.playbooks"
|
|
106
|
+
|
|
107
|
+
if [ $# -lt 1 ]; then
|
|
108
|
+
echo "Usage: $(basename "$0") <validate-id|validate-skill|write> [args...]" >&2
|
|
109
|
+
exit 2
|
|
110
|
+
fi
|
|
111
|
+
MODE="$1"
|
|
112
|
+
shift
|
|
113
|
+
|
|
114
|
+
PY_SCRIPT="$(mktemp -t playbook-new-XXXXXX.py)"
|
|
115
|
+
trap 'rm -f "$PY_SCRIPT"' EXIT
|
|
116
|
+
|
|
117
|
+
cat > "$PY_SCRIPT" <<'PY'
|
|
118
|
+
import json
|
|
119
|
+
import os
|
|
120
|
+
import re
|
|
121
|
+
import sys
|
|
122
|
+
|
|
123
|
+
mode = sys.argv[1]
|
|
124
|
+
SLUG_RE = re.compile(r'^[a-z0-9]+(-[a-z0-9]+)*$')
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def emit(obj):
|
|
128
|
+
print(json.dumps(obj))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if mode == "validate-id":
|
|
132
|
+
target_id = sys.argv[2]
|
|
133
|
+
playbooks_target_dir = sys.argv[3]
|
|
134
|
+
|
|
135
|
+
if not SLUG_RE.match(target_id):
|
|
136
|
+
emit({
|
|
137
|
+
"valid": False,
|
|
138
|
+
"reason": (
|
|
139
|
+
f"'{target_id}' is not slug-safe -- must match ^[a-z0-9]+(-[a-z0-9]+)*$ "
|
|
140
|
+
f"(lowercase letters, digits, single hyphens between segments)"
|
|
141
|
+
),
|
|
142
|
+
})
|
|
143
|
+
sys.exit(0)
|
|
144
|
+
|
|
145
|
+
existing_file = os.path.join(playbooks_target_dir, f"{target_id}.json")
|
|
146
|
+
if os.path.isfile(existing_file):
|
|
147
|
+
emit({
|
|
148
|
+
"valid": False,
|
|
149
|
+
"reason": (
|
|
150
|
+
f"a file already exists at {existing_file} -- choose a different id or edit "
|
|
151
|
+
f"that file directly"
|
|
152
|
+
),
|
|
153
|
+
})
|
|
154
|
+
sys.exit(0)
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
catalog = json.load(sys.stdin)
|
|
158
|
+
except Exception as e:
|
|
159
|
+
emit({
|
|
160
|
+
"valid": False,
|
|
161
|
+
"reason": f"could not read the playbook catalog to check for collisions ({e})",
|
|
162
|
+
})
|
|
163
|
+
sys.exit(0)
|
|
164
|
+
|
|
165
|
+
for entry in catalog:
|
|
166
|
+
if entry.get("id") == target_id:
|
|
167
|
+
src = entry.get("source", "unknown")
|
|
168
|
+
emit({
|
|
169
|
+
"valid": False,
|
|
170
|
+
"reason": (
|
|
171
|
+
f"id '{target_id}' collides with an already-loaded {src} playbook (see "
|
|
172
|
+
f"skills/jenga/scripts/load-playbooks.sh's merged catalog) -- choose a "
|
|
173
|
+
f"different id"
|
|
174
|
+
),
|
|
175
|
+
})
|
|
176
|
+
sys.exit(0)
|
|
177
|
+
|
|
178
|
+
emit({"valid": True})
|
|
179
|
+
sys.exit(0)
|
|
180
|
+
|
|
181
|
+
elif mode == "validate-skill":
|
|
182
|
+
target_name = sys.argv[2]
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
nl_catalog = json.load(sys.stdin)
|
|
186
|
+
except Exception as e:
|
|
187
|
+
emit({"valid": False, "reason": f"could not read the generated skill catalog ({e})"})
|
|
188
|
+
sys.exit(0)
|
|
189
|
+
|
|
190
|
+
for entry in nl_catalog:
|
|
191
|
+
if entry.get("name") == target_name:
|
|
192
|
+
emit({"valid": True})
|
|
193
|
+
sys.exit(0)
|
|
194
|
+
|
|
195
|
+
emit({
|
|
196
|
+
"valid": False,
|
|
197
|
+
"reason": (
|
|
198
|
+
f"'{target_name}' is not a recognized skill directory name in the generated skill "
|
|
199
|
+
f"catalog (skills/jenga/scripts/load-nl-catalog.sh) -- check spelling and the exact "
|
|
200
|
+
f"directory-name form the catalog currently lists"
|
|
201
|
+
),
|
|
202
|
+
})
|
|
203
|
+
sys.exit(0)
|
|
204
|
+
|
|
205
|
+
elif mode == "write":
|
|
206
|
+
playbooks_target_dir = sys.argv[2]
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
payload = json.load(sys.stdin)
|
|
210
|
+
except Exception as e:
|
|
211
|
+
emit({"written": False, "reason": f"stdin was not valid JSON ({e})"})
|
|
212
|
+
sys.exit(0)
|
|
213
|
+
|
|
214
|
+
if not isinstance(payload, dict):
|
|
215
|
+
emit({"written": False, "reason": "stdin JSON must be an object"})
|
|
216
|
+
sys.exit(0)
|
|
217
|
+
|
|
218
|
+
required_fields = ["id", "name", "description", "keywords", "examples", "steps"]
|
|
219
|
+
missing = [f for f in required_fields if f not in payload]
|
|
220
|
+
if missing:
|
|
221
|
+
emit({"written": False, "reason": f"missing required field(s) {missing}"})
|
|
222
|
+
sys.exit(0)
|
|
223
|
+
|
|
224
|
+
target_id = payload["id"]
|
|
225
|
+
if not isinstance(target_id, str) or not SLUG_RE.match(target_id):
|
|
226
|
+
emit({
|
|
227
|
+
"written": False,
|
|
228
|
+
"reason": f"id '{target_id}' is not slug-safe -- must match ^[a-z0-9]+(-[a-z0-9]+)*$",
|
|
229
|
+
})
|
|
230
|
+
sys.exit(0)
|
|
231
|
+
|
|
232
|
+
for field in ("name", "description"):
|
|
233
|
+
if not isinstance(payload[field], str) or not payload[field]:
|
|
234
|
+
emit({"written": False, "reason": f"'{field}' must be a non-empty string"})
|
|
235
|
+
sys.exit(0)
|
|
236
|
+
|
|
237
|
+
for field in ("keywords", "examples"):
|
|
238
|
+
value = payload[field]
|
|
239
|
+
if (
|
|
240
|
+
not isinstance(value, list)
|
|
241
|
+
or len(value) == 0
|
|
242
|
+
or not all(isinstance(v, str) and v for v in value)
|
|
243
|
+
):
|
|
244
|
+
emit({
|
|
245
|
+
"written": False,
|
|
246
|
+
"reason": f"'{field}' must be a non-empty list of non-empty strings",
|
|
247
|
+
})
|
|
248
|
+
sys.exit(0)
|
|
249
|
+
|
|
250
|
+
steps = payload["steps"]
|
|
251
|
+
if not isinstance(steps, list) or len(steps) < 2 or not all(isinstance(s, str) and s for s in steps):
|
|
252
|
+
emit({
|
|
253
|
+
"written": False,
|
|
254
|
+
"reason": (
|
|
255
|
+
"'steps' must be a list of at least 2 non-empty strings (bare skill-name steps "
|
|
256
|
+
"only -- this wizard authors no StepObject fields, per this task's v1 scope cut)"
|
|
257
|
+
),
|
|
258
|
+
})
|
|
259
|
+
sys.exit(0)
|
|
260
|
+
|
|
261
|
+
target_file = os.path.join(playbooks_target_dir, f"{target_id}.json")
|
|
262
|
+
if os.path.isfile(target_file):
|
|
263
|
+
emit({
|
|
264
|
+
"written": False,
|
|
265
|
+
"reason": f"a file already exists at {target_file} -- refusing to overwrite",
|
|
266
|
+
})
|
|
267
|
+
sys.exit(0)
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
os.makedirs(playbooks_target_dir, exist_ok=True)
|
|
271
|
+
except OSError as e:
|
|
272
|
+
print(f"Error: could not create {playbooks_target_dir}: {e}", file=sys.stderr)
|
|
273
|
+
sys.exit(2)
|
|
274
|
+
|
|
275
|
+
ordered = {
|
|
276
|
+
"id": target_id,
|
|
277
|
+
"name": payload["name"],
|
|
278
|
+
"description": payload["description"],
|
|
279
|
+
"keywords": payload["keywords"],
|
|
280
|
+
"examples": payload["examples"],
|
|
281
|
+
"steps": steps,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
with open(target_file, "w", encoding="utf-8") as fh:
|
|
286
|
+
json.dump(ordered, fh, indent=2)
|
|
287
|
+
fh.write("\n")
|
|
288
|
+
except OSError as e:
|
|
289
|
+
print(f"Error: could not write {target_file}: {e}", file=sys.stderr)
|
|
290
|
+
sys.exit(2)
|
|
291
|
+
|
|
292
|
+
emit({"written": True, "path": os.path.abspath(target_file)})
|
|
293
|
+
sys.exit(0)
|
|
294
|
+
|
|
295
|
+
else:
|
|
296
|
+
print(f"Error: unrecognized mode '{mode}' (usage: validate-id|validate-skill|write)", file=sys.stderr)
|
|
297
|
+
sys.exit(2)
|
|
298
|
+
PY
|
|
299
|
+
|
|
300
|
+
case "$MODE" in
|
|
301
|
+
validate-id)
|
|
302
|
+
if [ $# -lt 1 ] || [ -z "${1:-}" ]; then
|
|
303
|
+
echo "Usage: $(basename "$0") validate-id <id>" >&2
|
|
304
|
+
exit 2
|
|
305
|
+
fi
|
|
306
|
+
TARGET_ID="$1"
|
|
307
|
+
if ! CATALOG_JSON="$("$LOAD_PLAYBOOKS")"; then
|
|
308
|
+
echo "Error: load-playbooks.sh failed while checking id '$TARGET_ID' for collisions" >&2
|
|
309
|
+
exit 2
|
|
310
|
+
fi
|
|
311
|
+
printf '%s' "$CATALOG_JSON" | python3 "$PY_SCRIPT" validate-id "$TARGET_ID" "$PLAYBOOKS_TARGET_DIR"
|
|
312
|
+
;;
|
|
313
|
+
validate-skill)
|
|
314
|
+
if [ $# -lt 1 ] || [ -z "${1:-}" ]; then
|
|
315
|
+
echo "Usage: $(basename "$0") validate-skill <name>" >&2
|
|
316
|
+
exit 2
|
|
317
|
+
fi
|
|
318
|
+
TARGET_NAME="$1"
|
|
319
|
+
if ! NL_CATALOG_JSON="$("$LOAD_NL_CATALOG")"; then
|
|
320
|
+
echo "Error: load-nl-catalog.sh failed while validating skill name '$TARGET_NAME'" >&2
|
|
321
|
+
exit 2
|
|
322
|
+
fi
|
|
323
|
+
printf '%s' "$NL_CATALOG_JSON" | python3 "$PY_SCRIPT" validate-skill "$TARGET_NAME"
|
|
324
|
+
;;
|
|
325
|
+
write)
|
|
326
|
+
python3 "$PY_SCRIPT" write "$PLAYBOOKS_TARGET_DIR"
|
|
327
|
+
;;
|
|
328
|
+
*)
|
|
329
|
+
echo "Error: unrecognized mode '$MODE' (usage: $(basename "$0") <validate-id|validate-skill|write> [args...])" >&2
|
|
330
|
+
exit 2
|
|
331
|
+
;;
|
|
332
|
+
esac
|