@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.
- package/README.md +32 -1
- package/lib/skill-allow-list.json +1 -1
- package/package.json +1 -1
- package/project/app/api/scripts/capture-snapshot.js +12 -7
- package/project/app/ui/dist/assets/{index-C3oiuli_.js → index-DX2pfTAW.js} +16 -16
- package/project/app/ui/dist/index.html +1 -1
- package/scripts/verify-postinstall-reconcile.sh +33 -7
- package/skills/index/scripts/board_index.py +29 -1
- package/skills/j-do/SKILL.md +28 -15
- package/skills/j-doc/scripts/resolve_last_update.py +27 -1
- package/skills/j-todo/scripts/add_trivial_task.sh +12 -1
- package/skills/j-wtf/SKILL.md +1 -1
- package/skills/jenga/SKILL.md +49 -14
- package/skills/jenga/scripts/enrich-nl-prompt.sh +225 -0
- package/skills/jenga/scripts/load-nl-catalog.js +1 -1
- package/skills/jenga/scripts/match-playbook.sh +3 -2
- package/skills/jenga/scripts/run-playbook-step.sh +3 -2
|
@@ -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 `/
|
|
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 `/
|
|
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
|
|
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
|
|
24
|
-
# matched skill), then call
|
|
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.
|