@jenga-ai/agent 3.1.0 → 3.2.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 (44) hide show
  1. package/README.md +3 -3
  2. package/agents/developer.md +15 -15
  3. package/agents/scrum-master.md +17 -17
  4. package/agents/tester.md +25 -15
  5. package/lib/skill-allow-list.json +3 -2
  6. package/package.json +1 -1
  7. package/scripts/audit-twin-divergence.sh +625 -0
  8. package/scripts/build-pages-site.sh +268 -0
  9. package/scripts/check-public-playbook-steps.sh +136 -0
  10. package/skills/j-close-story/SKILL.md +1 -1
  11. package/skills/j-do/SKILL.md +19 -19
  12. package/skills/j-doc-sync/SKILL.md +12 -1
  13. package/skills/j-idea/SKILL.md +1 -1
  14. package/skills/j-init/SKILL.md +5 -4
  15. package/skills/j-init/assets/directory_structure.txt +1 -0
  16. package/skills/j-init/scripts/detect-existing-codebase.sh +2 -2
  17. package/skills/j-init/scripts/init.sh +13 -2
  18. package/skills/j-playbook/SKILL.md +81 -0
  19. package/skills/j-proceed/SKILL.md +1 -1
  20. package/skills/j-publish/SKILL.md +1 -1
  21. package/skills/j-publish/adapters/npm-ci.md +29 -0
  22. package/skills/j-publish/scripts/npm_ci_pipeline.sh +3 -0
  23. package/skills/j-publish/scripts/npm_pipeline.sh +18 -0
  24. package/skills/j-publish/scripts/npm_stage_pipeline.sh +81 -41
  25. package/skills/j-reconcile/SKILL.md +1 -0
  26. package/skills/j-redo/SKILL.md +1 -1
  27. package/skills/j-status/SKILL.md +12 -0
  28. package/skills/j-todo/SKILL.md +2 -2
  29. package/skills/j-uncharted/SKILL.md +8 -7
  30. package/skills/j-uncharted/scripts/elicitation-state.sh +15 -1
  31. package/skills/j-uncharted/scripts/validate-proposed-items.sh +18 -2
  32. package/skills/jenga/SKILL.md +80 -9
  33. package/skills/jenga/playbooks/idea-to-committed.json +20 -0
  34. package/skills/jenga/playbooks/schema.json +42 -0
  35. package/skills/jenga/scripts/detect-nl-intent.sh +179 -0
  36. package/skills/jenga/scripts/load-nl-catalog.js +206 -0
  37. package/skills/jenga/scripts/load-nl-catalog.sh +65 -0
  38. package/skills/jenga/scripts/load-playbooks.sh +1022 -0
  39. package/skills/jenga/scripts/match-playbook.sh +262 -0
  40. package/skills/jenga/scripts/render-playbook-confirmation.sh +517 -0
  41. package/skills/jenga/scripts/run-playbook-step.sh +766 -0
  42. package/skills/jenga-permission-level/SKILL.md +4 -4
  43. package/templates/KNOWLEDGE_GRAPH_STUB_SCHEMA_TEMPLATE.md +128 -0
  44. package/templates/playbook-types.json +8 -0
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env bash
2
+ # ---------------------------------------------------------------------------
3
+ # skills/jenga/scripts/match-playbook.sh
4
+ #
5
+ # Deterministic PLAYBOOK matcher for `/jenga`'s natural-language branch (E53_S02_T02). Runs the
6
+ # same three-pass matching *philosophy* as `skills/route/SKILL.md`'s Step 2 (keyword ->
7
+ # example similarity -> description), but scoped to the playbook catalog produced by
8
+ # `load-playbooks.sh` (E53_S02_T01) instead of the single-skill catalog `load-nl-catalog.sh`
9
+ # produces for `/route`/`/jenga`'s existing single-skill matching.
10
+ #
11
+ # ---------------------------------------------------------------------------
12
+ # THIS IS A FALLBACK — READ BEFORE WIRING (E53_S02_T04)
13
+ # ---------------------------------------------------------------------------
14
+ # This script is invoked ONLY after `/jenga`'s existing single-skill match (against
15
+ # `load-nl-catalog.sh`'s catalog, per E53_S01_T03) has already been attempted and did NOT produce
16
+ # a confident result (no match, or an ambiguous multi-way tie). It never runs ahead of, or in
17
+ # place of, single-skill matching — a confident single-skill match always wins and this script is
18
+ # never even invoked in that case. This mirrors the story's own framing: playbooks are proposed
19
+ # only when intent "does not cleanly resolve to one skill."
20
+ #
21
+ # ---------------------------------------------------------------------------
22
+ # USAGE
23
+ # ---------------------------------------------------------------------------
24
+ # skills/jenga/scripts/match-playbook.sh "<raw prompt>"
25
+ #
26
+ # The argument is the same raw natural-language argument `detect-nl-intent.sh` classified as
27
+ # `nl_intent` (its `raw_argument` field) — passed through verbatim, not re-cleaned here.
28
+ #
29
+ # ---------------------------------------------------------------------------
30
+ # MATCHING ALGORITHM (deterministic — a shell/python script cannot do semantic judgment the way
31
+ # an agent can, so this is a concrete, repeatable heuristic standing in for /route's Step 2 prose)
32
+ # ---------------------------------------------------------------------------
33
+ # Three passes are run in order against the full playbook catalog (from `load-playbooks.sh`).
34
+ # Each pass narrows the candidate pool; the first pass to produce a single unique leader commits
35
+ # to that result. A pass that produces a TIE narrows the pool to just the tied candidates and
36
+ # falls through to the next pass as a tie-breaker (rather than immediately declaring ambiguity) —
37
+ # only if the FINAL pass (description match) still can't break the tie is the result "ambiguous".
38
+ # If the final pass finds NO signal at all (every candidate scores 0) but an earlier pass had
39
+ # already established a real tie among 2+ candidates (on a genuine positive score, not a
40
+ # default/empty pool), that pre-existing tie is reported as "ambiguous" rather than discarded as
41
+ # "no_match" — the final pass adding no new information does not erase the real signal a prior
42
+ # pass already found. "no_match" is reserved for when NO pass ever found any positive signal.
43
+ #
44
+ # Pass 1 — Keyword match: score = count of this playbook's `keywords` entries that appear as a
45
+ # case-insensitive substring of the raw prompt. Playbooks scoring 0 are dropped from
46
+ # the pool for this pass. If exactly one playbook has the (positive) max score -> match.
47
+ # If the pool is empty (no playbook matched any keyword) -> proceed to Pass 2 over the
48
+ # FULL catalog. If tied among 2+ -> proceed to Pass 2 restricted to the TIED set.
49
+ #
50
+ # Pass 2 — Example similarity: score = the highest Jaccard token-overlap ratio (lowercased,
51
+ # stopword-filtered word sets) between the prompt and any one of the playbook's
52
+ # `examples`. Scores below MIN_SIMILARITY are treated as 0 (non-candidates). Same
53
+ # unique-max / tie / empty-pool handling as Pass 1, operating on the current pool.
54
+ #
55
+ # Pass 3 — Description match: same Jaccard token-overlap approach, against each playbook's
56
+ # single `description` string instead of its `examples` list. This is the FINAL pass:
57
+ # a unique max -> match; a tie among 2+ -> "ambiguous"; an empty pool / all-zero scores
58
+ # -> "no_match".
59
+ #
60
+ # ---------------------------------------------------------------------------
61
+ # OUTPUT SCHEMA
62
+ # ---------------------------------------------------------------------------
63
+ # stdout is always a single JSON object, one of:
64
+ #
65
+ # {"classification": "playbook_match", "playbook_id": "brainstorm-to-mirror",
66
+ # "name": "Idea to Public Release", "steps": ["j-brainstorm", "j-todo", "j-do", "j-dev-done", "j-mirror-public"]}
67
+ #
68
+ # {"classification": "ambiguous", "candidates": [{"playbook_id": "...", "name": "..."}, ...]}
69
+ #
70
+ # {"classification": "no_match"}
71
+ #
72
+ # Nothing else is ever written to stdout — errors/warnings go to stderr only.
73
+ #
74
+ # ---------------------------------------------------------------------------
75
+ # EXIT CODES
76
+ # ---------------------------------------------------------------------------
77
+ # 0 any of the three classifications above (all are legitimate outcomes, not errors)
78
+ # 2 usage error (no argument given), or a real setup failure (`load-playbooks.sh` failed,
79
+ # python3 unavailable)
80
+ #
81
+ # ---------------------------------------------------------------------------
82
+
83
+ set -euo pipefail
84
+
85
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
86
+ LOAD_PLAYBOOKS="$SCRIPT_DIR/load-playbooks.sh"
87
+
88
+ if [ $# -lt 1 ] || [ -z "${1:-}" ]; then
89
+ echo 'Usage: match-playbook.sh "<raw prompt>"' >&2
90
+ exit 2
91
+ fi
92
+
93
+ RAW_PROMPT="$1"
94
+
95
+ if [ ! -x "$LOAD_PLAYBOOKS" ] && [ ! -f "$LOAD_PLAYBOOKS" ]; then
96
+ echo "Error: load-playbooks.sh not found at $LOAD_PLAYBOOKS" >&2
97
+ exit 2
98
+ fi
99
+
100
+ if ! command -v python3 >/dev/null 2>&1; then
101
+ echo "Error: python3 is required by match-playbook.sh" >&2
102
+ exit 2
103
+ fi
104
+
105
+ CATALOG_JSON="$(bash "$LOAD_PLAYBOOKS")" || {
106
+ echo "Error: load-playbooks.sh failed" >&2
107
+ exit 2
108
+ }
109
+
110
+ PY_SCRIPT="$(mktemp -t match-playbook-XXXXXX.py)"
111
+ trap 'rm -f "$PY_SCRIPT"' EXIT
112
+
113
+ cat > "$PY_SCRIPT" <<'PY'
114
+ import json
115
+ import re
116
+ import sys
117
+
118
+ raw_prompt = sys.argv[1]
119
+ catalog_json = sys.stdin.read()
120
+
121
+ try:
122
+ catalog = json.loads(catalog_json)
123
+ except Exception as e:
124
+ print(f"Error: could not parse load-playbooks.sh output as JSON: {e}", file=sys.stderr)
125
+ sys.exit(2)
126
+
127
+ if not isinstance(catalog, list):
128
+ print("Error: load-playbooks.sh produced a non-array result", file=sys.stderr)
129
+ sys.exit(2)
130
+
131
+ if len(catalog) == 0:
132
+ print(json.dumps({"classification": "no_match"}))
133
+ sys.exit(0)
134
+
135
+ MIN_SIMILARITY = 0.15
136
+
137
+ STOPWORDS = {
138
+ "a", "an", "the", "to", "and", "or", "of", "in", "on", "for", "this", "that",
139
+ "is", "it", "its", "with", "from", "into", "i", "my", "me", "we", "our",
140
+ "you", "your", "then", "so", "be", "as", "at", "by", "up", "out", "all",
141
+ "let", "lets", "let's", "go", "want", "please", "help", "would", "like",
142
+ }
143
+
144
+
145
+ def tokenize(text):
146
+ words = re.findall(r"[a-z0-9']+", text.lower())
147
+ return {w for w in words if w not in STOPWORDS and len(w) > 1}
148
+
149
+
150
+ def jaccard(a_tokens, b_tokens):
151
+ if not a_tokens or not b_tokens:
152
+ return 0.0
153
+ inter = len(a_tokens & b_tokens)
154
+ union = len(a_tokens | b_tokens)
155
+ return inter / union if union else 0.0
156
+
157
+
158
+ prompt_lower = raw_prompt.lower()
159
+ prompt_tokens = tokenize(raw_prompt)
160
+
161
+ pool = list(catalog)
162
+
163
+
164
+ def resolve_pool(scored_pool):
165
+ """Given a list of (playbook, score) with score > 0 meaning 'candidate', return
166
+ ('unique', playbook) | ('tie', [playbooks]) | ('empty', None)."""
167
+ positive = [(pb, s) for pb, s in scored_pool if s > 0]
168
+ if not positive:
169
+ return ("empty", None)
170
+ max_score = max(s for _, s in positive)
171
+ leaders = [pb for pb, s in positive if s == max_score]
172
+ if len(leaders) == 1:
173
+ return ("unique", leaders[0])
174
+ return ("tie", leaders)
175
+
176
+
177
+ def emit_match(pb):
178
+ print(json.dumps({
179
+ "classification": "playbook_match",
180
+ "playbook_id": pb["id"],
181
+ "name": pb["name"],
182
+ "steps": pb["steps"],
183
+ }))
184
+ sys.exit(0)
185
+
186
+
187
+ def emit_ambiguous(pbs):
188
+ print(json.dumps({
189
+ "classification": "ambiguous",
190
+ "candidates": [{"playbook_id": pb["id"], "name": pb["name"]} for pb in pbs],
191
+ }))
192
+ sys.exit(0)
193
+
194
+
195
+ def emit_no_match():
196
+ print(json.dumps({"classification": "no_match"}))
197
+ sys.exit(0)
198
+
199
+
200
+ # `narrowed_by_tie` tracks whether ANY earlier pass established a genuine tie among 2+
201
+ # candidates on real signal (a positive score, not a default/empty pool). This matters for the
202
+ # final pass below: if Pass 3 can't further discriminate (all scores drop to 0 — e.g. two
203
+ # playbooks share near-identical description text relative to a short prompt), that must NOT be
204
+ # reported as "no_match" when a real tie already existed upstream — the correct outcome is
205
+ # "ambiguous" over that already-established candidate set. "no_match" is reserved for the case
206
+ # where NO pass ever found any positive signal at all.
207
+ narrowed_by_tie = False
208
+
209
+ # --- Pass 1: keyword match -------------------------------------------------
210
+ pass1_scores = []
211
+ for pb in pool:
212
+ score = sum(1 for kw in pb["keywords"] if kw.lower() in prompt_lower)
213
+ pass1_scores.append((pb, score))
214
+
215
+ outcome, result = resolve_pool(pass1_scores)
216
+ if outcome == "unique":
217
+ emit_match(result)
218
+ elif outcome == "tie":
219
+ pool = result # narrow to tied candidates, fall through to Pass 2 as tie-breaker
220
+ narrowed_by_tie = True
221
+ # else "empty" -> pool stays the FULL catalog for Pass 2
222
+
223
+ # --- Pass 2: example similarity --------------------------------------------
224
+ pass2_scores = []
225
+ for pb in pool:
226
+ best = 0.0
227
+ for example in pb["examples"]:
228
+ sim = jaccard(prompt_tokens, tokenize(example))
229
+ if sim > best:
230
+ best = sim
231
+ pass2_scores.append((pb, best if best >= MIN_SIMILARITY else 0.0))
232
+
233
+ outcome, result = resolve_pool(pass2_scores)
234
+ if outcome == "unique":
235
+ emit_match(result)
236
+ elif outcome == "tie":
237
+ pool = result # narrow further, fall through to Pass 3 as tie-breaker
238
+ narrowed_by_tie = True
239
+ # else "empty" -> pool stays whatever it was entering Pass 2 for Pass 3 (narrowed_by_tie
240
+ # unchanged — an empty pass2 outcome adds no new tie signal of its own)
241
+
242
+ # --- Pass 3: description match (final pass) --------------------------------
243
+ pass3_scores = []
244
+ for pb in pool:
245
+ sim = jaccard(prompt_tokens, tokenize(pb["description"]))
246
+ pass3_scores.append((pb, sim if sim >= MIN_SIMILARITY else 0.0))
247
+
248
+ outcome, result = resolve_pool(pass3_scores)
249
+ if outcome == "unique":
250
+ emit_match(result)
251
+ elif outcome == "tie":
252
+ emit_ambiguous(result)
253
+ elif narrowed_by_tie and len(pool) > 1:
254
+ # Pass 3 added no further discriminating signal, but an earlier pass already established a
255
+ # real tie among these exact candidates -- report that tie rather than discarding it.
256
+ emit_ambiguous(pool)
257
+ else:
258
+ emit_no_match()
259
+ PY
260
+
261
+ python3 "$PY_SCRIPT" "$RAW_PROMPT" <<< "$CATALOG_JSON"
262
+ exit $?