@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.
- package/README.md +10 -3
- package/bin/jenga.js +10 -0
- package/hooks/copilot_session_end.sh +7 -3
- package/hooks/prompt_router_helper.js +17 -5
- package/lib/commands/doctor.js +351 -0
- package/lib/commands/init.js +16 -0
- package/lib/generate-copilot-hooks.js +116 -0
- package/lib/legacy-shipped-paths.json +336 -0
- package/lib/postinstall-manifest.js +469 -0
- package/lib/skill-allow-list.json +1 -1
- package/package.json +2 -1
- package/scripts/build-pages-site.sh +268 -0
- package/scripts/generate-legacy-shipped-paths.js +248 -0
- package/scripts/postinstall.js +205 -2
- package/scripts/verify-legacy-seed-reconcile.sh +254 -0
- package/scripts/verify-postinstall-reconcile.sh +392 -0
- package/skills/j-uncharted/scripts/elicitation-state.sh +15 -1
- package/skills/jenga/SKILL.md +39 -7
- package/skills/jenga/playbooks/brainstorm-to-mirror.json +22 -0
- package/skills/jenga/playbooks/schema.json +42 -0
- package/skills/jenga/scripts/detect-nl-intent.sh +179 -0
- package/skills/jenga/scripts/load-nl-catalog.js +206 -0
- package/skills/jenga/scripts/load-nl-catalog.sh +65 -0
- package/skills/jenga/scripts/load-playbooks.sh +194 -0
- package/skills/jenga/scripts/match-playbook.sh +262 -0
- package/skills/jenga/scripts/render-playbook-confirmation.sh +363 -0
- package/skills/jenga/scripts/run-playbook-step.sh +273 -0
- package/templates/copilot-instructions.md.tpl +32 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ---------------------------------------------------------------------------
|
|
3
|
+
# skills/jenga/scripts/detect-nl-intent.sh
|
|
4
|
+
#
|
|
5
|
+
# Deterministic classification wrapper around `resolve-id.sh` for `/jenga`'s
|
|
6
|
+
# Phase 0.75 entry-mode resolution (E53_S01). Phase 0.75's scoped branch used
|
|
7
|
+
# to call `resolve-id.sh` directly and halt the whole invocation the moment
|
|
8
|
+
# ANY comma-delimited segment was rejected by the ID grammar. E53_S01 adds a
|
|
9
|
+
# new outcome: when EVERY segment is rejected, the raw argument is not a
|
|
10
|
+
# malformed ID list at all — it's natural-language intent, which should be
|
|
11
|
+
# routed to /jenga's new NL branch (wired up in E53_S01_T03) instead of
|
|
12
|
+
# halting.
|
|
13
|
+
#
|
|
14
|
+
# This script performs exactly that classification and nothing else. Per
|
|
15
|
+
# CLAUDE.md's "Skill Implementation Principle — Scripts Over Inline Logic",
|
|
16
|
+
# `skills/jenga/SKILL.md` never parses `resolve-id.sh`'s raw JSON array
|
|
17
|
+
# itself for this decision — it only ever reads this script's three output
|
|
18
|
+
# shapes below.
|
|
19
|
+
#
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# USAGE
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# skills/jenga/scripts/detect-nl-intent.sh "<raw Phase 0.75 argument>"
|
|
24
|
+
#
|
|
25
|
+
# The argument is passed through verbatim to `resolve-id.sh` — this script
|
|
26
|
+
# does not itself split, clean, or reinterpret it. See `resolve-id.sh`'s own
|
|
27
|
+
# header for the exact ID grammar and its per-segment output schema.
|
|
28
|
+
#
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# CLASSIFICATION CONTRACT (stable — E53_S01_T03 wires against this exactly)
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# `resolve-id.sh`'s JSON array (one object per comma-delimited segment) is
|
|
33
|
+
# reduced to exactly ONE of three classifications, based on the distinct set
|
|
34
|
+
# of `status` values across all segments:
|
|
35
|
+
#
|
|
36
|
+
# 1. every segment "resolved" -> "all_resolved"
|
|
37
|
+
# 2. every segment "rejected" -> "nl_intent"
|
|
38
|
+
# 3. a mix of both -> "mixed"
|
|
39
|
+
#
|
|
40
|
+
# stdout is always a single JSON object. Nothing else is ever written to
|
|
41
|
+
# stdout — errors and warnings go to stderr only.
|
|
42
|
+
#
|
|
43
|
+
# "all_resolved":
|
|
44
|
+
# {
|
|
45
|
+
# "classification": "all_resolved",
|
|
46
|
+
# "resolved_ids": ["E01_S02", ...],
|
|
47
|
+
# "resolved_ids_csv": "E01_S02,..."
|
|
48
|
+
# }
|
|
49
|
+
# exit 0 — the existing scoped-branch confirmation flow in
|
|
50
|
+
# `skills/jenga/SKILL.md` is unaffected by this script's introduction.
|
|
51
|
+
#
|
|
52
|
+
# "nl_intent":
|
|
53
|
+
# {
|
|
54
|
+
# "classification": "nl_intent",
|
|
55
|
+
# "raw_argument": "<the original $1, verbatim>"
|
|
56
|
+
# }
|
|
57
|
+
# exit 0 — this is the signal E53_S01_T03's new branch uses to enter
|
|
58
|
+
# natural-language matching instead of halting.
|
|
59
|
+
#
|
|
60
|
+
# "mixed":
|
|
61
|
+
# {
|
|
62
|
+
# "classification": "mixed",
|
|
63
|
+
# "rejected": [
|
|
64
|
+
# {"input": "<raw segment>", "reason": "<human-readable reason>"},
|
|
65
|
+
# ...
|
|
66
|
+
# ]
|
|
67
|
+
# }
|
|
68
|
+
# exit 1 — preserves the exact current halt-and-report behavior already
|
|
69
|
+
# documented in `skills/jenga/SKILL.md`'s "`resolve-id.sh` rejects one or
|
|
70
|
+
# more segments" edge case. No partial scope is ever assembled from the
|
|
71
|
+
# segments that did resolve.
|
|
72
|
+
#
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# EXIT CODES
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
# 0 "all_resolved" or "nl_intent" classification (see above)
|
|
77
|
+
# 1 "mixed" classification (see above)
|
|
78
|
+
# 2 usage error (no argument given) or a `resolve-id.sh` / `board-scan.sh`
|
|
79
|
+
# setup failure — a real setup problem, not a classification outcome.
|
|
80
|
+
# `resolve-id.sh`'s own stderr message (already emitted by it) is what
|
|
81
|
+
# explains the failure; this script does not re-emit a second message
|
|
82
|
+
# on top of it.
|
|
83
|
+
#
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
set -euo pipefail
|
|
87
|
+
|
|
88
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
89
|
+
RESOLVE_ID="$SCRIPT_DIR/resolve-id.sh"
|
|
90
|
+
|
|
91
|
+
if [ $# -lt 1 ] || [ -z "${1:-}" ]; then
|
|
92
|
+
echo 'Usage: detect-nl-intent.sh "<raw Phase 0.75 argument>"' >&2
|
|
93
|
+
exit 2
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
RAW_INPUT="$1"
|
|
97
|
+
|
|
98
|
+
if [ ! -x "$RESOLVE_ID" ]; then
|
|
99
|
+
echo "Error: resolve-id.sh not found or not executable at $RESOLVE_ID" >&2
|
|
100
|
+
exit 2
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
104
|
+
echo "Error: python3 is required by detect-nl-intent.sh" >&2
|
|
105
|
+
exit 2
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# resolve-id.sh exits 0 (all resolved) or 1 (at least one rejected) as
|
|
109
|
+
# legitimate per-segment outcomes — only exit 2 (usage/setup failure) is a
|
|
110
|
+
# real problem here. Guard the capture explicitly so `set -e` doesn't abort
|
|
111
|
+
# this script on resolve-id.sh's own exit 1.
|
|
112
|
+
set +e
|
|
113
|
+
RESOLVE_JSON="$("$RESOLVE_ID" "$RAW_INPUT")"
|
|
114
|
+
RESOLVE_EXIT=$?
|
|
115
|
+
set -e
|
|
116
|
+
|
|
117
|
+
if [ "$RESOLVE_EXIT" -eq 2 ]; then
|
|
118
|
+
# resolve-id.sh already wrote a plain-text error to stderr — just
|
|
119
|
+
# propagate its exit code rather than re-deriving a second message.
|
|
120
|
+
exit 2
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
PY_SCRIPT="$(mktemp -t detect-nl-intent-XXXXXX.py)"
|
|
124
|
+
trap 'rm -f "$PY_SCRIPT"' EXIT
|
|
125
|
+
|
|
126
|
+
cat > "$PY_SCRIPT" <<'PY'
|
|
127
|
+
import json
|
|
128
|
+
import sys
|
|
129
|
+
|
|
130
|
+
raw_argument = sys.argv[1]
|
|
131
|
+
resolve_json = sys.stdin.read()
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
segments = json.loads(resolve_json)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
print(f"Error: could not parse resolve-id.sh output as JSON: {e}", file=sys.stderr)
|
|
137
|
+
sys.exit(2)
|
|
138
|
+
|
|
139
|
+
if not isinstance(segments, list) or len(segments) == 0:
|
|
140
|
+
# resolve-id.sh only emits [] for an empty/whitespace-only argument,
|
|
141
|
+
# which should never reach this script — Phase 0.75 routes an empty
|
|
142
|
+
# argument to the bare branch before detect-nl-intent.sh is ever
|
|
143
|
+
# invoked. Treat this as a setup problem rather than silently guessing
|
|
144
|
+
# a classification for input this script was never meant to see.
|
|
145
|
+
print("Error: resolve-id.sh produced an empty or non-array result", file=sys.stderr)
|
|
146
|
+
sys.exit(2)
|
|
147
|
+
|
|
148
|
+
statuses = {seg.get("status") for seg in segments}
|
|
149
|
+
|
|
150
|
+
if statuses == {"resolved"}:
|
|
151
|
+
resolved_ids = [seg["resolved_id"] for seg in segments]
|
|
152
|
+
print(json.dumps({
|
|
153
|
+
"classification": "all_resolved",
|
|
154
|
+
"resolved_ids": resolved_ids,
|
|
155
|
+
"resolved_ids_csv": ",".join(resolved_ids),
|
|
156
|
+
}))
|
|
157
|
+
sys.exit(0)
|
|
158
|
+
|
|
159
|
+
if statuses == {"rejected"}:
|
|
160
|
+
print(json.dumps({
|
|
161
|
+
"classification": "nl_intent",
|
|
162
|
+
"raw_argument": raw_argument,
|
|
163
|
+
}))
|
|
164
|
+
sys.exit(0)
|
|
165
|
+
|
|
166
|
+
rejected = [
|
|
167
|
+
{"input": seg.get("input"), "reason": seg.get("reason")}
|
|
168
|
+
for seg in segments
|
|
169
|
+
if seg.get("status") == "rejected"
|
|
170
|
+
]
|
|
171
|
+
print(json.dumps({
|
|
172
|
+
"classification": "mixed",
|
|
173
|
+
"rejected": rejected,
|
|
174
|
+
}))
|
|
175
|
+
sys.exit(1)
|
|
176
|
+
PY
|
|
177
|
+
|
|
178
|
+
python3 "$PY_SCRIPT" "$RAW_INPUT" <<< "$RESOLVE_JSON"
|
|
179
|
+
exit $?
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* skills/jenga/scripts/load-nl-catalog.js
|
|
4
|
+
*
|
|
5
|
+
* Node (ESM) helper behind `load-nl-catalog.sh` — the SINGLE REQUIRED SOURCE of skill-catalog
|
|
6
|
+
* data for `/jenga`'s natural-language branch (E53_S01_T03). `skills/jenga/SKILL.md` must never
|
|
7
|
+
* re-implement its own skill directory scan or hand-maintain a skill list; it only ever reads
|
|
8
|
+
* this script's stdout.
|
|
9
|
+
*
|
|
10
|
+
* The catalog's NAME LIST comes exclusively from `lib/generate-skill-allow-list.js`'s generated
|
|
11
|
+
* inventory (`readSkillAllowList()`, which reads the committed `lib/skill-allow-list.json`
|
|
12
|
+
* artifact) — this script does not independently re-scan `skills/` for a name list of its own,
|
|
13
|
+
* per E53_S01_T02's acceptance criteria and the drift lesson E41_S04 already documented for that
|
|
14
|
+
* generator. For each name in that inventory, this script reads exactly one file —
|
|
15
|
+
* `skills/<name>/SKILL.md` — to populate the remaining catalog fields: `description`, `keywords`,
|
|
16
|
+
* `examples`, and `metadata.prefered_agent`. These are the same fields `/route`'s Step 1
|
|
17
|
+
* ("Discover Available Skills") collects.
|
|
18
|
+
*
|
|
19
|
+
* ---------------------------------------------------------------------------
|
|
20
|
+
* USAGE
|
|
21
|
+
* ---------------------------------------------------------------------------
|
|
22
|
+
* node load-nl-catalog.js <projectRoot> <pkgRoot>
|
|
23
|
+
*
|
|
24
|
+
* <projectRoot> the consuming project's root (passed through to
|
|
25
|
+
* readSkillAllowList's projectRoot fallback candidate)
|
|
26
|
+
* <pkgRoot> the jenga-agent PACKAGE root — where lib/generate-skill-allow-list.js and the
|
|
27
|
+
* canonical skills/ tree actually live (monorepo checkout root, or
|
|
28
|
+
* node_modules/@jenga-ai/agent for an installed consumer)
|
|
29
|
+
*
|
|
30
|
+
* ---------------------------------------------------------------------------
|
|
31
|
+
* OUTPUT SCHEMA
|
|
32
|
+
* ---------------------------------------------------------------------------
|
|
33
|
+
* stdout is a single JSON array, one object per catalog entry, e.g.:
|
|
34
|
+
*
|
|
35
|
+
* [
|
|
36
|
+
* {
|
|
37
|
+
* "name": "btw",
|
|
38
|
+
* "description": "...",
|
|
39
|
+
* "keywords": ["..."],
|
|
40
|
+
* "examples": ["..."],
|
|
41
|
+
* "prefered_agent": "scrum-master" // or null when absent
|
|
42
|
+
* },
|
|
43
|
+
* ...
|
|
44
|
+
* ]
|
|
45
|
+
*
|
|
46
|
+
* Nothing but this JSON array is ever written to stdout. Warnings (a skill skipped because its
|
|
47
|
+
* SKILL.md is missing/unreadable, or its frontmatter lacks `description`) go to stderr only, and
|
|
48
|
+
* are non-fatal.
|
|
49
|
+
*
|
|
50
|
+
* ---------------------------------------------------------------------------
|
|
51
|
+
* EXIT CODES
|
|
52
|
+
* ---------------------------------------------------------------------------
|
|
53
|
+
* 0 catalog written to stdout (possibly with skip warnings already emitted to stderr)
|
|
54
|
+
* 2 usage error, or a real setup failure (allow-list inventory unreadable/empty, or
|
|
55
|
+
* lib/generate-skill-allow-list.js failed to load)
|
|
56
|
+
*
|
|
57
|
+
* ---------------------------------------------------------------------------
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
import { readFileSync, existsSync } from "fs";
|
|
61
|
+
import { join } from "path";
|
|
62
|
+
import { pathToFileURL } from "url";
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parses YAML frontmatter from a SKILL.md's content into a plain object. This is a hand-rolled,
|
|
66
|
+
* intentionally minimal parser scoped to the small set of shapes SKILL.md frontmatter actually
|
|
67
|
+
* uses — it follows the same overall approach as mcp/router/skill-index.js's `parseFrontmatter`
|
|
68
|
+
* (scalar keys, and array keys introduced by an empty `key:` line followed by ` - item` lines),
|
|
69
|
+
* extended here to also recognize ONE level of nested mapping (the `metadata:` block, e.g.
|
|
70
|
+
* `metadata:\n prefered_agent: developer`) — a shape skill-index.js's parser doesn't need to
|
|
71
|
+
* handle, since it never reads `metadata`.
|
|
72
|
+
*
|
|
73
|
+
* Returns {} if no frontmatter block is found.
|
|
74
|
+
*/
|
|
75
|
+
function parseFrontmatter(content) {
|
|
76
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
77
|
+
if (!match) return {};
|
|
78
|
+
|
|
79
|
+
const lines = match[1].split("\n");
|
|
80
|
+
const obj = {};
|
|
81
|
+
let i = 0;
|
|
82
|
+
|
|
83
|
+
const stripQuotes = (s) => s.trim().replace(/^["']|["']$/g, "");
|
|
84
|
+
|
|
85
|
+
while (i < lines.length) {
|
|
86
|
+
const topMatch = lines[i].match(/^(\w+):\s*(.*)$/);
|
|
87
|
+
if (!topMatch) {
|
|
88
|
+
i++;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const [, key, valRaw] = topMatch;
|
|
92
|
+
const val = valRaw.trim();
|
|
93
|
+
|
|
94
|
+
if (val === "[]") {
|
|
95
|
+
obj[key] = [];
|
|
96
|
+
i++;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (val !== "") {
|
|
101
|
+
obj[key] = stripQuotes(val);
|
|
102
|
+
i++;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Empty value — look ahead at indented child lines to decide whether this key is an array
|
|
107
|
+
// (child lines shaped ` - item`) or a nested map (child lines shaped ` childKey: value`).
|
|
108
|
+
// A block is only ever consistently one or the other in this repo's frontmatter, so the
|
|
109
|
+
// first child line's shape decides it.
|
|
110
|
+
let j = i + 1;
|
|
111
|
+
const arrayItems = [];
|
|
112
|
+
const mapObj = {};
|
|
113
|
+
let sawArray = false;
|
|
114
|
+
let sawMap = false;
|
|
115
|
+
|
|
116
|
+
while (j < lines.length && /^\s+\S/.test(lines[j])) {
|
|
117
|
+
const arrItem = lines[j].match(/^\s+-\s+(.*)$/);
|
|
118
|
+
const mapItem = lines[j].match(/^\s+(\w+):\s*(.*)$/);
|
|
119
|
+
if (arrItem && !sawMap) {
|
|
120
|
+
sawArray = true;
|
|
121
|
+
arrayItems.push(stripQuotes(arrItem[1]));
|
|
122
|
+
} else if (mapItem && !sawArray) {
|
|
123
|
+
sawMap = true;
|
|
124
|
+
mapObj[mapItem[1]] = stripQuotes(mapItem[2]);
|
|
125
|
+
}
|
|
126
|
+
j++;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
obj[key] = sawArray ? arrayItems : sawMap ? mapObj : [];
|
|
130
|
+
i = j;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return obj;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function main() {
|
|
137
|
+
const [projectRoot, pkgRoot] = process.argv.slice(2);
|
|
138
|
+
if (!projectRoot || !pkgRoot) {
|
|
139
|
+
process.stderr.write("Usage: load-nl-catalog.js <projectRoot> <pkgRoot>\n");
|
|
140
|
+
process.exit(2);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const generatorPath = join(pkgRoot, "lib", "generate-skill-allow-list.js");
|
|
144
|
+
let readSkillAllowList;
|
|
145
|
+
try {
|
|
146
|
+
({ readSkillAllowList } = await import(pathToFileURL(generatorPath).href));
|
|
147
|
+
} catch (e) {
|
|
148
|
+
process.stderr.write(`Error: failed to load ${generatorPath}: ${e.message}\n`);
|
|
149
|
+
process.exit(2);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const names = readSkillAllowList(projectRoot, pkgRoot);
|
|
154
|
+
if (!Array.isArray(names) || names.length === 0) {
|
|
155
|
+
process.stderr.write(
|
|
156
|
+
"Error: skill allow-list inventory is empty or unreadable (lib/skill-allow-list.json) — cannot build NL catalog\n"
|
|
157
|
+
);
|
|
158
|
+
process.exit(2);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const catalog = [];
|
|
163
|
+
for (const name of names) {
|
|
164
|
+
const skillMdPath = join(pkgRoot, "skills", name, "SKILL.md");
|
|
165
|
+
if (!existsSync(skillMdPath)) {
|
|
166
|
+
process.stderr.write(
|
|
167
|
+
`Warning: ${skillMdPath} not found for allow-listed skill '${name}' — skipped\n`
|
|
168
|
+
);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let content;
|
|
173
|
+
try {
|
|
174
|
+
content = readFileSync(skillMdPath, "utf8");
|
|
175
|
+
} catch (e) {
|
|
176
|
+
process.stderr.write(`Warning: failed to read ${skillMdPath}: ${e.message} — skipped\n`);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const fm = parseFrontmatter(content);
|
|
181
|
+
if (!fm.description) {
|
|
182
|
+
process.stderr.write(
|
|
183
|
+
`Warning: ${skillMdPath} missing 'description' in frontmatter — skipped\n`
|
|
184
|
+
);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
catalog.push({
|
|
189
|
+
name,
|
|
190
|
+
description: fm.description,
|
|
191
|
+
keywords: Array.isArray(fm.keywords) ? fm.keywords : [],
|
|
192
|
+
examples: Array.isArray(fm.examples) ? fm.examples : [],
|
|
193
|
+
prefered_agent:
|
|
194
|
+
fm.metadata && typeof fm.metadata === "object" && fm.metadata.prefered_agent
|
|
195
|
+
? fm.metadata.prefered_agent
|
|
196
|
+
: null,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
process.stdout.write(JSON.stringify(catalog, null, 2) + "\n");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
main().catch((e) => {
|
|
204
|
+
process.stderr.write(`Error: ${e.message}\n`);
|
|
205
|
+
process.exit(2);
|
|
206
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ---------------------------------------------------------------------------
|
|
3
|
+
# skills/jenga/scripts/load-nl-catalog.sh
|
|
4
|
+
#
|
|
5
|
+
# Thin bash entry point for `load-nl-catalog.js` — the SINGLE REQUIRED SOURCE of skill-catalog
|
|
6
|
+
# data for `/jenga`'s natural-language branch (E53_S01_T03). `skills/jenga/SKILL.md` must never
|
|
7
|
+
# re-implement its own skill directory scan or hand-maintain a skill list; it only ever invokes
|
|
8
|
+
# this script and reads its stdout.
|
|
9
|
+
#
|
|
10
|
+
# This script's only job is root resolution: it locates JENGA_PROJECT_DIR (the consuming
|
|
11
|
+
# project's root) and PKG_ROOT (the jenga-agent PACKAGE root — where the canonical skills/ tree
|
|
12
|
+
# and lib/generate-skill-allow-list.js actually live, which may differ from the project root for
|
|
13
|
+
# an npm-installed consumer) and hands both to the Node helper, which does the actual reading and
|
|
14
|
+
# JSON assembly (see load-nl-catalog.js's own header for the full contract).
|
|
15
|
+
#
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# USAGE
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# skills/jenga/scripts/load-nl-catalog.sh
|
|
20
|
+
#
|
|
21
|
+
# No arguments. Emits the full JSON catalog array to stdout — see load-nl-catalog.js's header for
|
|
22
|
+
# the exact per-entry shape (name/description/keywords/examples/prefered_agent).
|
|
23
|
+
#
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# EXIT CODES
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# 0 catalog written to stdout
|
|
28
|
+
# 2 setup failure: package root could not be located, node is missing, or the Node helper
|
|
29
|
+
# itself failed (see its own stderr message for the reason)
|
|
30
|
+
#
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
set -euo pipefail
|
|
34
|
+
|
|
35
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
36
|
+
|
|
37
|
+
# Resolve JENGA_PROJECT_DIR the same way every other script in skills/jenga/scripts/ does.
|
|
38
|
+
if [ -f "$SCRIPT_DIR/../../../lib/resolve-project-dir.sh" ]; then
|
|
39
|
+
# shellcheck source=/dev/null
|
|
40
|
+
source "$SCRIPT_DIR/../../../lib/resolve-project-dir.sh"
|
|
41
|
+
elif [ -n "${CLAUDE_PROJECT_DIR:-}" ]; then
|
|
42
|
+
JENGA_PROJECT_DIR="$CLAUDE_PROJECT_DIR"
|
|
43
|
+
else
|
|
44
|
+
JENGA_PROJECT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Resolve the jenga-agent PACKAGE root (where lib/generate-skill-allow-list.js and the canonical
|
|
48
|
+
# skills/ tree actually live) — same monorepo-checkout vs. installed-npm-package detection used
|
|
49
|
+
# by skills/init/scripts/init.sh's PKG_ROOT resolution.
|
|
50
|
+
if [ -d "$SCRIPT_DIR/../../../templates" ]; then
|
|
51
|
+
PKG_ROOT="$SCRIPT_DIR/../../.."
|
|
52
|
+
elif [ -d "$JENGA_PROJECT_DIR/node_modules/@jenga-ai/agent/templates" ]; then
|
|
53
|
+
PKG_ROOT="$JENGA_PROJECT_DIR/node_modules/@jenga-ai/agent"
|
|
54
|
+
else
|
|
55
|
+
echo "Error: could not locate the jenga-agent package root (templates/ not found via monorepo checkout or node_modules/@jenga-ai/agent)." >&2
|
|
56
|
+
exit 2
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
60
|
+
echo "Error: node is required by load-nl-catalog.sh" >&2
|
|
61
|
+
exit 2
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
node "$SCRIPT_DIR/load-nl-catalog.js" "$JENGA_PROJECT_DIR" "$PKG_ROOT"
|
|
65
|
+
exit $?
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ---------------------------------------------------------------------------
|
|
3
|
+
# skills/jenga/scripts/load-playbooks.sh
|
|
4
|
+
#
|
|
5
|
+
# Deterministic loader for `/jenga`'s multi-skill PLAYBOOK catalog (E53_S02_T01) — the SINGLE
|
|
6
|
+
# REQUIRED SOURCE of playbook data for E53_S02_T02's `match-playbook.sh` and E53_S02_T04's wiring
|
|
7
|
+
# into `skills/jenga/SKILL.md`. Neither of those may re-scan `skills/jenga/playbooks/` or
|
|
8
|
+
# hand-maintain a playbook list of their own; they only ever invoke this script and read its
|
|
9
|
+
# stdout, mirroring the single-source contract `load-nl-catalog.sh` already established for the
|
|
10
|
+
# single-skill catalog (E53_S01_T02).
|
|
11
|
+
#
|
|
12
|
+
# A "playbook" is an ORDERED chain of skills (e.g. brainstorm -> todo -> do -> dev-done ->
|
|
13
|
+
# mirror-public) that `/jenga`'s natural-language branch may propose, as an editable, confirmable
|
|
14
|
+
# numbered list (see `render-playbook-confirmation.sh`, E53_S02_T03), when free-text intent spans
|
|
15
|
+
# more than one skill and does not cleanly resolve to a single one.
|
|
16
|
+
#
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# DATA SOURCE
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Every `*.json` file directly under `skills/jenga/playbooks/`, EXCLUDING `schema.json` (which
|
|
21
|
+
# documents the required shape — see that file's own header — but is never itself a playbook
|
|
22
|
+
# entry). See `schema.json` for the authoritative field list; this script's validation below is
|
|
23
|
+
# a runtime mirror of that schema, not a substitute for it.
|
|
24
|
+
#
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# USAGE
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# skills/jenga/scripts/load-playbooks.sh
|
|
29
|
+
#
|
|
30
|
+
# No arguments. Emits the full JSON playbook catalog array to stdout.
|
|
31
|
+
#
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# OUTPUT SCHEMA
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# stdout is a single JSON array, one object per valid playbook:
|
|
36
|
+
#
|
|
37
|
+
# [
|
|
38
|
+
# {
|
|
39
|
+
# "id": "brainstorm-to-mirror",
|
|
40
|
+
# "name": "Idea to Public Release",
|
|
41
|
+
# "description": "...",
|
|
42
|
+
# "keywords": ["..."],
|
|
43
|
+
# "examples": ["..."],
|
|
44
|
+
# "steps": ["brainstorm", "todo", "do", "dev-done", "mirror-public"]
|
|
45
|
+
# },
|
|
46
|
+
# ...
|
|
47
|
+
# ]
|
|
48
|
+
#
|
|
49
|
+
# Nothing but this JSON array is ever written to stdout. Skip warnings go to stderr only and are
|
|
50
|
+
# non-fatal — a single malformed playbook file never aborts the whole catalog load.
|
|
51
|
+
#
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# VALIDATION / SKIP CONDITIONS (each skip is a stderr warning, never fatal)
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# - File is not valid JSON, or is not a JSON object -> skipped
|
|
56
|
+
# - Missing any required field: id, name, description, keywords,
|
|
57
|
+
# examples, steps -> skipped
|
|
58
|
+
# - `keywords`, `examples`, or `steps` present but empty (or not a
|
|
59
|
+
# list) -> skipped
|
|
60
|
+
# - `id` does not equal the filename's basename without `.json` -> skipped
|
|
61
|
+
# (prevents a playbook's identity from silently drifting from its
|
|
62
|
+
# file location)
|
|
63
|
+
# - Any entry in `steps` has no corresponding `skills/<name>/SKILL.md`
|
|
64
|
+
# on disk -> skipped (the whole
|
|
65
|
+
# playbook is skipped, not just the bad step — a chain with a broken
|
|
66
|
+
# link is not a usable chain)
|
|
67
|
+
#
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# EXIT CODES
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# 0 catalog written to stdout (possibly with skip warnings already emitted to stderr;
|
|
72
|
+
# an empty catalog `[]` is a valid, non-error outcome — e.g. every playbook file was
|
|
73
|
+
# malformed, or no playbook files exist yet beyond schema.json)
|
|
74
|
+
# 2 usage error, or a real setup failure (playbooks directory missing entirely, or
|
|
75
|
+
# python3 unavailable)
|
|
76
|
+
#
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
set -euo pipefail
|
|
80
|
+
|
|
81
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
82
|
+
|
|
83
|
+
# Resolve the jenga-agent PACKAGE root (where the canonical skills/ tree actually lives) — same
|
|
84
|
+
# monorepo-checkout vs. installed-npm-package detection used by
|
|
85
|
+
# skills/jenga/scripts/load-nl-catalog.sh's PKG_ROOT resolution and skills/init/scripts/init.sh.
|
|
86
|
+
if [ -d "$SCRIPT_DIR/../../../templates" ]; then
|
|
87
|
+
PKG_ROOT="$SCRIPT_DIR/../../.."
|
|
88
|
+
elif [ -n "${CLAUDE_PROJECT_DIR:-}" ] && [ -d "${CLAUDE_PROJECT_DIR}/node_modules/@jenga-ai/agent/templates" ]; then
|
|
89
|
+
PKG_ROOT="${CLAUDE_PROJECT_DIR}/node_modules/@jenga-ai/agent"
|
|
90
|
+
else
|
|
91
|
+
echo "Error: could not locate the jenga-agent package root (templates/ not found via monorepo checkout or node_modules/@jenga-ai/agent)." >&2
|
|
92
|
+
exit 2
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
PLAYBOOKS_DIR="$PKG_ROOT/skills/jenga/playbooks"
|
|
96
|
+
SKILLS_DIR="$PKG_ROOT/skills"
|
|
97
|
+
|
|
98
|
+
if [ ! -d "$PLAYBOOKS_DIR" ]; then
|
|
99
|
+
echo "Error: playbooks directory not found at $PLAYBOOKS_DIR" >&2
|
|
100
|
+
exit 2
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
104
|
+
echo "Error: python3 is required by load-playbooks.sh" >&2
|
|
105
|
+
exit 2
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
PY_SCRIPT="$(mktemp -t load-playbooks-XXXXXX.py)"
|
|
109
|
+
trap 'rm -f "$PY_SCRIPT"' EXIT
|
|
110
|
+
|
|
111
|
+
cat > "$PY_SCRIPT" <<'PY'
|
|
112
|
+
import json
|
|
113
|
+
import os
|
|
114
|
+
import sys
|
|
115
|
+
|
|
116
|
+
playbooks_dir = sys.argv[1]
|
|
117
|
+
skills_dir = sys.argv[2]
|
|
118
|
+
|
|
119
|
+
REQUIRED_FIELDS = ["id", "name", "description", "keywords", "examples", "steps"]
|
|
120
|
+
LIST_FIELDS = ["keywords", "examples", "steps"]
|
|
121
|
+
|
|
122
|
+
catalog = []
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
filenames = sorted(
|
|
126
|
+
f for f in os.listdir(playbooks_dir)
|
|
127
|
+
if f.endswith(".json") and f != "schema.json"
|
|
128
|
+
)
|
|
129
|
+
except OSError as e:
|
|
130
|
+
print(f"Error: could not list {playbooks_dir}: {e}", file=sys.stderr)
|
|
131
|
+
sys.exit(2)
|
|
132
|
+
|
|
133
|
+
for filename in filenames:
|
|
134
|
+
path = os.path.join(playbooks_dir, filename)
|
|
135
|
+
basename = filename[: -len(".json")]
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
with open(path, encoding="utf-8") as fh:
|
|
139
|
+
data = json.load(fh)
|
|
140
|
+
except Exception as e:
|
|
141
|
+
print(f"Warning: {path} is not valid JSON ({e}) — skipped", file=sys.stderr)
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
if not isinstance(data, dict):
|
|
145
|
+
print(f"Warning: {path} is not a JSON object — skipped", file=sys.stderr)
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
missing = [f for f in REQUIRED_FIELDS if f not in data]
|
|
149
|
+
if missing:
|
|
150
|
+
print(f"Warning: {path} missing required field(s) {missing} — skipped", file=sys.stderr)
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
bad_list = [
|
|
154
|
+
f for f in LIST_FIELDS
|
|
155
|
+
if not isinstance(data.get(f), list) or len(data.get(f)) == 0
|
|
156
|
+
]
|
|
157
|
+
if bad_list:
|
|
158
|
+
print(f"Warning: {path} field(s) {bad_list} must be non-empty lists — skipped", file=sys.stderr)
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
if data["id"] != basename:
|
|
162
|
+
print(
|
|
163
|
+
f"Warning: {path} has id '{data['id']}' which does not match its filename "
|
|
164
|
+
f"'{basename}.json' — skipped",
|
|
165
|
+
file=sys.stderr,
|
|
166
|
+
)
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
missing_skills = [
|
|
170
|
+
step for step in data["steps"]
|
|
171
|
+
if not os.path.isfile(os.path.join(skills_dir, step, "SKILL.md"))
|
|
172
|
+
]
|
|
173
|
+
if missing_skills:
|
|
174
|
+
print(
|
|
175
|
+
f"Warning: {path} references nonexistent skill(s) {missing_skills} "
|
|
176
|
+
f"(no skills/<name>/SKILL.md found) — playbook skipped",
|
|
177
|
+
file=sys.stderr,
|
|
178
|
+
)
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
catalog.append({
|
|
182
|
+
"id": data["id"],
|
|
183
|
+
"name": data["name"],
|
|
184
|
+
"description": data["description"],
|
|
185
|
+
"keywords": data["keywords"],
|
|
186
|
+
"examples": data["examples"],
|
|
187
|
+
"steps": data["steps"],
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
print(json.dumps(catalog, indent=2))
|
|
191
|
+
PY
|
|
192
|
+
|
|
193
|
+
python3 "$PY_SCRIPT" "$PLAYBOOKS_DIR" "$SKILLS_DIR"
|
|
194
|
+
exit $?
|