@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.
Files changed (60) hide show
  1. package/README.md +52 -12
  2. package/agents/developer.md +16 -1
  3. package/agents/scrum-master.md +1 -0
  4. package/bin/jenga.js +10 -0
  5. package/lib/commands/dashboard.js +92 -0
  6. package/lib/skill-allow-list.json +6 -2
  7. package/package.json +21 -2
  8. package/project/app/api/lib/resolve-project-root.js +120 -0
  9. package/project/app/api/package.json +16 -0
  10. package/project/app/api/parsers/architecture.js +72 -0
  11. package/project/app/api/parsers/board.js +141 -0
  12. package/project/app/api/parsers/documentation.js +125 -0
  13. package/project/app/api/parsers/git-log.js +52 -0
  14. package/project/app/api/parsers/ideas.js +62 -0
  15. package/project/app/api/parsers/knowledge-graph.js +73 -0
  16. package/project/app/api/parsers/lib/markdown-dir-reader.js +163 -0
  17. package/project/app/api/parsers/rapports.js +148 -0
  18. package/project/app/api/parsers/todo.js +179 -0
  19. package/project/app/api/response.js +47 -0
  20. package/project/app/api/routes/architecture.js +23 -0
  21. package/project/app/api/routes/board.js +46 -0
  22. package/project/app/api/routes/documentation.js +24 -0
  23. package/project/app/api/routes/health.js +25 -0
  24. package/project/app/api/routes/history.js +55 -0
  25. package/project/app/api/routes/rapports.js +24 -0
  26. package/project/app/api/scripts/capture-snapshot.js +294 -0
  27. package/project/app/api/server.js +112 -0
  28. package/project/app/api/types.js +40 -0
  29. package/project/app/package.json +21 -0
  30. package/project/app/ui/dist/assets/index-7fj-vllY.js +104 -0
  31. package/project/app/ui/dist/assets/index-CdK3Qrep.css +1 -0
  32. package/project/app/ui/dist/index.html +13 -0
  33. package/project/app/ui/package.json +23 -0
  34. package/project/app/ui/scripts/build-snapshot-html.cjs +214 -0
  35. package/project/app/ui/scripts/dashboard-open.cjs +88 -0
  36. package/project/app/ui/scripts/dashboard-start.cjs +87 -0
  37. package/scripts/acquire-concurrency-slot.sh +220 -0
  38. package/scripts/compute-deploy-reconcile.sh +439 -0
  39. package/scripts/jenga-permission-level-switch.sh +19 -3
  40. package/scripts/mark-deployed.sh +532 -0
  41. package/scripts/populate-knowledge-graph.js +429 -0
  42. package/scripts/release-concurrency-slot.sh +129 -0
  43. package/scripts/validate-board.sh +60 -2
  44. package/scripts/verify-consumer-install.sh +470 -0
  45. package/skills/j-cloud-connect/SKILL.md +95 -0
  46. package/skills/j-cloud-connect/scripts/configure-backend.sh +267 -0
  47. package/skills/j-cloud-connect/scripts/install-rclone.sh +153 -0
  48. package/skills/j-dashboard/SKILL.md +144 -0
  49. package/skills/j-dashboard/scripts/launch.sh +121 -0
  50. package/skills/j-dashboard/scripts/resolve-app-dir.sh +164 -0
  51. package/skills/j-dashboard/scripts/snapshot.sh +267 -0
  52. package/skills/j-dashboard-share/SKILL.md +96 -0
  53. package/skills/j-dashboard-share/scripts/upload-snapshot.sh +173 -0
  54. package/skills/j-playbook/SKILL.md +12 -0
  55. package/skills/j-playbook-new/SKILL.md +155 -0
  56. package/skills/j-playbook-new/scripts/playbook-new.sh +332 -0
  57. package/skills/j-publish/scripts/npm_ci_pipeline.sh +6 -0
  58. package/skills/jenga/scripts/load-nl-catalog.js +22 -6
  59. package/skills/jenga/scripts/load-playbooks.sh +123 -24
  60. 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