@jenga-ai/agent 4.0.0 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env bash
2
+ # ---------------------------------------------------------------------------
3
+ # skills/jenga/scripts/enrich-nl-prompt.sh
4
+ #
5
+ # Deterministic board + documentation enrichment scan for `/jenga`'s natural-language branch,
6
+ # ported from `/route`'s Steps 3-5 (E53_S13_T01). `/route` is being retired in this same story
7
+ # (E53_S13_T02) — this script is how its board-context and documentation enrichment survives, as
8
+ # an OPT-IN capability behind `/jenga`'s `--enrich` flag (see `skills/jenga/SKILL.md`'s Phase 0.75
9
+ # natural-language branch). The default, unflagged NL path never invokes this script.
10
+ #
11
+ # Per CLAUDE.md's "Scripts Over Inline Logic" principle, the board scan and docs scan are
12
+ # deterministic and belong here — the agent's job is limited to invoking this script and
13
+ # assembling the enriched prompt from its structured output, exactly as it already does for
14
+ # `board-scan.sh`/`detect-nl-intent.sh`/`match-playbook.sh` elsewhere in this directory.
15
+ #
16
+ # ---------------------------------------------------------------------------
17
+ # USAGE
18
+ # ---------------------------------------------------------------------------
19
+ # skills/jenga/scripts/enrich-nl-prompt.sh "<raw prompt text>"
20
+ #
21
+ # The argument is the same raw natural-language text `detect-nl-intent.sh` classified as
22
+ # `nl_intent` (its `raw_argument` field) — passed through verbatim, not re-cleaned here.
23
+ #
24
+ # ---------------------------------------------------------------------------
25
+ # ALGORITHM
26
+ # ---------------------------------------------------------------------------
27
+ # Board half (`/route`'s Step 3) — reuses `skills/jenga/scripts/board-scan.sh` verbatim for the
28
+ # board inventory (no duplicate board-scanning logic is introduced here). The prompt is tokenized
29
+ # (lowercased, stopword-filtered) and an item is a match if any prompt token appears as a substring
30
+ # of its `title` or `summary` field. Items with `status` of `Archived` or `Cancelled` are excluded.
31
+ # Results are capped at the top 5, in `board-scan.sh`'s own stable order (epics, then stories, then
32
+ # tasks; lexical by filename within each type).
33
+ #
34
+ # Docs half (`/route`'s Step 4) — scans `project/documentation/plans/`,
35
+ # `project/documentation/summaries/`, `project/documentation/examples/`, and `docs/` (non-recursive
36
+ # within each) for files whose filename OR first top-level heading contains a prompt token.
37
+ # Results are capped at the top 3, in directory-then-lexical-filename order.
38
+ #
39
+ # ---------------------------------------------------------------------------
40
+ # OUTPUT SCHEMA (stable)
41
+ # ---------------------------------------------------------------------------
42
+ # stdout is always a single JSON object. Nothing else is ever written to stdout.
43
+ #
44
+ # {
45
+ # "board_items": [
46
+ # {"id": "E12_S03", "type": "story", "status": "Pending", "title": "...",
47
+ # "file": "project/board/stories/E12_S03_....md"},
48
+ # ... // up to 5
49
+ # ],
50
+ # "docs": [
51
+ # {"path": "docs/skill-authoring.md", "summary": "<first heading or filename>"},
52
+ # ... // up to 3
53
+ # ],
54
+ # "board_items_found": 2, // total matches BEFORE the top-5 cap
55
+ # "docs_found": 1 // total matches BEFORE the top-3 cap
56
+ # }
57
+ #
58
+ # An empty result (`board_items: []`, `docs: []`, both counts 0) is a normal, non-error outcome —
59
+ # it means the prompt simply didn't match anything on the board or in docs. Exit code is 0 in that
60
+ # case, same as any other successful scan.
61
+ #
62
+ # ---------------------------------------------------------------------------
63
+ # EXIT CODES
64
+ # ---------------------------------------------------------------------------
65
+ # 0 scan completed (stdout is always valid JSON on this path, including the empty-match case)
66
+ # 1 usage error (no argument given), or a setup problem: `board-scan.sh` missing/failing, or
67
+ # python3 unavailable — real setup problems, not classification outcomes.
68
+ #
69
+ # ---------------------------------------------------------------------------
70
+
71
+ set -euo pipefail
72
+
73
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
74
+ BOARD_SCAN="$SCRIPT_DIR/board-scan.sh"
75
+
76
+ if [ $# -lt 1 ] || [ -z "${1:-}" ]; then
77
+ echo 'Usage: enrich-nl-prompt.sh "<raw prompt text>"' >&2
78
+ exit 1
79
+ fi
80
+
81
+ RAW_PROMPT="$1"
82
+
83
+ if [ ! -x "$BOARD_SCAN" ]; then
84
+ echo "Error: board-scan.sh not found or not executable at $BOARD_SCAN" >&2
85
+ exit 1
86
+ fi
87
+
88
+ if ! command -v python3 >/dev/null 2>&1; then
89
+ echo "Error: python3 is required by enrich-nl-prompt.sh" >&2
90
+ exit 1
91
+ fi
92
+
93
+ # Resolve JENGA_PROJECT_DIR the same way every other script in this directory does
94
+ # (CLAUDE_PROJECT_DIR -> git toplevel -> cwd).
95
+ if [ -f "$SCRIPT_DIR/../../../lib/resolve-project-dir.sh" ]; then
96
+ # shellcheck source=lib/resolve-project-dir.sh
97
+ source "$SCRIPT_DIR/../../../lib/resolve-project-dir.sh"
98
+ elif [ -n "${CLAUDE_PROJECT_DIR:-}" ]; then
99
+ JENGA_PROJECT_DIR="$CLAUDE_PROJECT_DIR"
100
+ else
101
+ JENGA_PROJECT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || pwd)"
102
+ fi
103
+
104
+ BOARD_JSON="$("$BOARD_SCAN")"
105
+
106
+ PY_SCRIPT="$(mktemp -t enrich-nl-prompt-XXXXXX.py)"
107
+ trap 'rm -f "$PY_SCRIPT"' EXIT
108
+
109
+ cat > "$PY_SCRIPT" <<'PY'
110
+ import json
111
+ import re
112
+ import sys
113
+ from pathlib import Path
114
+
115
+ project_root = Path(sys.argv[1])
116
+ raw_prompt = sys.argv[2]
117
+ board_json = sys.stdin.read()
118
+
119
+ STOPWORDS = {
120
+ "a", "an", "the", "to", "and", "or", "of", "in", "on", "for", "this", "that",
121
+ "is", "it", "its", "with", "from", "into", "i", "my", "me", "we", "our",
122
+ "you", "your", "then", "so", "be", "as", "at", "by", "up", "out", "all",
123
+ "let", "lets", "let's", "go", "want", "please", "help", "would", "like",
124
+ }
125
+
126
+
127
+ def tokenize(text):
128
+ words = re.findall(r"[a-z0-9']+", text.lower())
129
+ return {w for w in words if w not in STOPWORDS and len(w) > 1}
130
+
131
+
132
+ prompt_tokens = tokenize(raw_prompt)
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # Board half
136
+ # ---------------------------------------------------------------------------
137
+ try:
138
+ board_items = json.loads(board_json)
139
+ except Exception as e:
140
+ print(f"Error: could not parse board-scan.sh output as JSON: {e}", file=sys.stderr)
141
+ sys.exit(1)
142
+
143
+ EXCLUDED_STATUSES = {"Archived", "Cancelled"}
144
+
145
+
146
+ def item_matches(item):
147
+ haystack = f"{item.get('title', '')} {item.get('summary', '')}".lower()
148
+ return any(tok in haystack for tok in prompt_tokens)
149
+
150
+
151
+ matched_board = [
152
+ item for item in board_items
153
+ if item.get("status") not in EXCLUDED_STATUSES and item_matches(item)
154
+ ]
155
+
156
+ board_items_found = len(matched_board)
157
+ top_board_items = [
158
+ {
159
+ "id": item.get("id", ""),
160
+ "type": item.get("type", ""),
161
+ "status": item.get("status", ""),
162
+ "title": item.get("title", ""),
163
+ "file": item.get("file", ""),
164
+ }
165
+ for item in matched_board[:5]
166
+ ]
167
+
168
+ # ---------------------------------------------------------------------------
169
+ # Docs half
170
+ # ---------------------------------------------------------------------------
171
+ DOC_DIRS = [
172
+ "project/documentation/plans",
173
+ "project/documentation/summaries",
174
+ "project/documentation/examples",
175
+ "docs",
176
+ ]
177
+
178
+ HEADING_RE = re.compile(r'^#+\s+(.*\S)\s*$')
179
+
180
+
181
+ def first_heading(path):
182
+ try:
183
+ with path.open(encoding="utf-8") as f:
184
+ for line in f:
185
+ m = HEADING_RE.match(line.rstrip("\n"))
186
+ if m:
187
+ return m.group(1)
188
+ except Exception:
189
+ pass
190
+ return ""
191
+
192
+
193
+ matched_docs = []
194
+ for rel_dir in DOC_DIRS:
195
+ dir_path = project_root / rel_dir
196
+ if not dir_path.is_dir():
197
+ continue
198
+ for f in sorted(dir_path.glob("*.md")):
199
+ heading = first_heading(f)
200
+ haystack = f"{f.stem} {heading}".lower()
201
+ if any(tok in haystack for tok in prompt_tokens):
202
+ try:
203
+ rel_file = f.relative_to(project_root).as_posix()
204
+ except ValueError:
205
+ rel_file = f.as_posix()
206
+ matched_docs.append({
207
+ "path": rel_file,
208
+ "summary": heading or f.stem,
209
+ })
210
+
211
+ docs_found = len(matched_docs)
212
+ top_docs = matched_docs[:3]
213
+
214
+ result = {
215
+ "board_items": top_board_items,
216
+ "docs": top_docs,
217
+ "board_items_found": board_items_found,
218
+ "docs_found": docs_found,
219
+ }
220
+
221
+ json.dump(result, sys.stdout, indent=2)
222
+ sys.stdout.write("\n")
223
+ PY
224
+
225
+ python3 "$PY_SCRIPT" "$JENGA_PROJECT_DIR" "$RAW_PROMPT" <<< "$BOARD_JSON"
@@ -20,7 +20,7 @@
20
20
  * `keywords`, `examples`, and `metadata.prefered_agent`, and emits `dirName` (not the bare
21
21
  * identifier) as the catalog entry's `name` — callers like `/jenga`'s Skill invocation and
22
22
  * `playbook-new.sh`'s `validate-skill` need the real, invokable directory name. These are the
23
- * same fields `/route`'s Step 1 ("Discover Available Skills") collects.
23
+ * same fields `/jenga`'s own Skill Matching & Invocation Contract needs for matching and invocation.
24
24
  *
25
25
  * ---------------------------------------------------------------------------
26
26
  * USAGE
@@ -6,7 +6,7 @@
6
6
  # same three-pass matching *philosophy* as `skills/jenga/SKILL.md`'s inlined Skill Matching &
7
7
  # Invocation Contract (keyword -> example similarity -> description), but scoped to the playbook
8
8
  # catalog produced by `load-playbooks.sh` (E53_S02_T01) instead of the single-skill catalog
9
- # `load-nl-catalog.sh` produces for `/route`/`/jenga`'s existing single-skill matching.
9
+ # `load-nl-catalog.sh` produces for `/jenga`'s existing single-skill matching.
10
10
  #
11
11
  # ---------------------------------------------------------------------------
12
12
  # THIS IS A FALLBACK — READ BEFORE WIRING (E53_S02_T04)
@@ -28,7 +28,8 @@
28
28
  #
29
29
  # ---------------------------------------------------------------------------
30
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)
31
+ # an agent can, so this is a concrete, repeatable heuristic standing in for the Skill Matching &
32
+ # Invocation Contract's Pass 1/2/3 prose, applied to playbooks instead of single skills)
32
33
  # ---------------------------------------------------------------------------
33
34
  # Three passes are run in order against the full playbook catalog (from `load-playbooks.sh`).
34
35
  # Each pass narrows the candidate pool; the first pass to produce a single unique leader commits
@@ -20,8 +20,9 @@
20
20
  # metadata — see CONDITIONALS below) -> get the first step to invoke.
21
21
  # 2. `should-skip <state_file>` -> deterministically decide whether the CURRENT step should run.
22
22
  # - `{"skip": true, ...}` -> do NOT invoke the step; call `advance <state_file> skipped`.
23
- # - `{"skip": false, ...}` -> invoke the step (as `/route`'s Step 6 already does for a single
24
- # matched skill), then call `advance <state_file> passed ["<typed-output-value>"]` (step
23
+ # - `{"skip": false, ...}` -> invoke the step (as the Skill Matching & Invocation Contract's
24
+ # Invoke rule already does for a single matched skill), then call
25
+ # `advance <state_file> passed ["<typed-output-value>"]` (step
25
26
  # succeeded) or `advance <state_file> failed [note]` (step failed).
26
27
  # 3. Any of the three `advance` outcomes returns the next step, a "complete" signal, or (on
27
28
  # failure) a halt report.