@jenga-ai/agent 4.0.0 → 4.1.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.
- 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-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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Jenga AI Dashboard</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-DX2pfTAW.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-BADc5mmH.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
@@ -330,13 +330,27 @@ run_install "$PKG2C" "$CONSUMER_C" "$FIXTURE/case-v2.log"
|
|
|
330
330
|
|
|
331
331
|
# Detect whether this filesystem is even case-insensitive; on a case-SENSITIVE fs
|
|
332
332
|
# both names legitimately coexist and the old one is genuinely stale.
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
333
|
+
#
|
|
334
|
+
# This probes the filesystem DIRECTLY rather than inferring the answer from the
|
|
335
|
+
# mirrored tree. The previous inference was circular -- it asked whether both
|
|
336
|
+
# skill.md and SKILL.md exist with DIFFERENT content, but both fixture packages
|
|
337
|
+
# above write the identical body ("# alpha"), so the inequality was false by
|
|
338
|
+
# construction and the branch concluded "case-insensitive" on every filesystem.
|
|
339
|
+
# On macOS that happened to be the right answer; on a case-SENSITIVE fs (any
|
|
340
|
+
# Linux CI runner) it was wrong, and the case-insensitive branch below then ran
|
|
341
|
+
# an assert_grep for "written-this-run" that can only hold when the two names
|
|
342
|
+
# collide -- recording a FAIL that no individual @test asserts, so only
|
|
343
|
+
# "harness reports zero failures overall" caught it. Surfaced by the E28_S17
|
|
344
|
+
# mirror staging gate's first Linux runs (2026-09-28).
|
|
345
|
+
CASE_PROBE="$FIXTURE/case-probe"
|
|
346
|
+
mkdir -p "$CASE_PROBE"
|
|
347
|
+
: > "$CASE_PROBE/probe"
|
|
348
|
+
if [ -e "$CASE_PROBE/PROBE" ]; then
|
|
338
349
|
CASE_INSENSITIVE=1
|
|
350
|
+
else
|
|
351
|
+
CASE_INSENSITIVE=0
|
|
339
352
|
fi
|
|
353
|
+
rm -rf "$CASE_PROBE"
|
|
340
354
|
|
|
341
355
|
if [ "$CASE_INSENSITIVE" -eq 1 ]; then
|
|
342
356
|
# The whole point: the skill must still exist under SOME name after the upgrade.
|
|
@@ -353,8 +367,20 @@ if [ "$CASE_INSENSITIVE" -eq 1 ]; then
|
|
|
353
367
|
assert_grep "written-this-run" "$FIXTURE/case-v2.log" \
|
|
354
368
|
"identity guard reports the spared entry as written-this-run"
|
|
355
369
|
else
|
|
356
|
-
|
|
357
|
-
|
|
370
|
+
# These names must remain PREFIX-COMPATIBLE with the case-insensitive branch
|
|
371
|
+
# above: assert_check in tests/postinstall-delete-reconciliation.bats matches
|
|
372
|
+
# with grep -F on "<tab><name>", a substring match, so a trailing "(n/a ...)"
|
|
373
|
+
# qualifier still satisfies an assertion written against the canonical name
|
|
374
|
+
# while keeping the skip visible in the harness output. The identity-guard
|
|
375
|
+
# line below already followed this convention; the two case-only-rename lines
|
|
376
|
+
# did not -- they were renamed wholesale to "case-only rename (skipped: ...)",
|
|
377
|
+
# which shares no prefix with the canonical name, so on a case-sensitive fs
|
|
378
|
+
# the "case-only rename does not delete the file the run just wrote" @test
|
|
379
|
+
# would fail with "no such check recorded by the harness". That never fired
|
|
380
|
+
# before only because the broken detection above forced every filesystem down
|
|
381
|
+
# the case-insensitive branch.
|
|
382
|
+
pass "case-only rename does not delete the just-written file (.agents) (n/a: filesystem is case-sensitive, both names legitimately coexist)"
|
|
383
|
+
pass "case-only rename does not delete the just-written file (.claude) (n/a: filesystem is case-sensitive, both names legitimately coexist)"
|
|
358
384
|
pass "identity guard reports the spared entry as written-this-run (n/a on case-sensitive fs)"
|
|
359
385
|
fi
|
|
360
386
|
|
|
@@ -6,6 +6,7 @@ import argparse
|
|
|
6
6
|
import hashlib
|
|
7
7
|
import json
|
|
8
8
|
import re
|
|
9
|
+
import subprocess
|
|
9
10
|
import sys
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
from typing import Any
|
|
@@ -33,7 +34,34 @@ EDGE_TYPES = {
|
|
|
33
34
|
"parent_doc",
|
|
34
35
|
}
|
|
35
36
|
FRONTMATTER_BOUNDARY = "---"
|
|
36
|
-
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _resolve_project_root() -> Path:
|
|
40
|
+
"""Locate the project root via git, not a fixed relative climb.
|
|
41
|
+
|
|
42
|
+
This script is deployed both at its source location
|
|
43
|
+
(skills/index/scripts/) and mirrored one directory deeper into
|
|
44
|
+
.claude/skills/index/scripts/ / .agents/skills/index/scripts/ -- a fixed
|
|
45
|
+
parents[N] climb from __file__ only lands on the real repo root from the
|
|
46
|
+
source location, not from a mirror. Same approach as
|
|
47
|
+
skills/j-close-story/scripts/check-story-closeable.sh,
|
|
48
|
+
check-privatized.sh:138, and compute-scope-divergence.sh:54 use for the
|
|
49
|
+
equivalent bash case.
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
result = subprocess.run(
|
|
53
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
54
|
+
cwd=Path(__file__).resolve().parent,
|
|
55
|
+
capture_output=True,
|
|
56
|
+
text=True,
|
|
57
|
+
check=True,
|
|
58
|
+
)
|
|
59
|
+
return Path(result.stdout.strip())
|
|
60
|
+
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
|
|
61
|
+
return Path.cwd()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
ROOT_DIR = _resolve_project_root()
|
|
37
65
|
BOARD_ID_RE = re.compile(r"^E\d{2}(?:_S\d{2})?(?:_T\d{2})?$")
|
|
38
66
|
PLAN_RE = re.compile(r"^(E\d{2}_S\d{2}(?:_T\d{2})?)-plan$")
|
|
39
67
|
SUMMARY_RE = re.compile(r"^(E\d{2}_S\d{2}(?:_T\d{2})?)-summary$")
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
import argparse
|
|
7
7
|
import json
|
|
8
|
+
import subprocess
|
|
8
9
|
import sys
|
|
9
10
|
from datetime import date
|
|
10
11
|
from pathlib import Path
|
|
@@ -18,6 +19,31 @@ BOARD_DIRS = (
|
|
|
18
19
|
)
|
|
19
20
|
|
|
20
21
|
|
|
22
|
+
def _resolve_project_root() -> Path:
|
|
23
|
+
"""Locate the project root via git, not a fixed relative climb.
|
|
24
|
+
|
|
25
|
+
This script is deployed both at its source location
|
|
26
|
+
(skills/j-doc/scripts/) and mirrored one directory deeper into
|
|
27
|
+
.claude/skills/j-doc/scripts/ / .agents/skills/j-doc/scripts/ -- a fixed
|
|
28
|
+
parents[N] climb from __file__ only lands on the real repo root from the
|
|
29
|
+
source location, not from a mirror. Same approach as
|
|
30
|
+
skills/j-close-story/scripts/check-story-closeable.sh,
|
|
31
|
+
check-privatized.sh:138, and compute-scope-divergence.sh:54 use for the
|
|
32
|
+
equivalent bash case.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
result = subprocess.run(
|
|
36
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
37
|
+
cwd=Path(__file__).resolve().parent,
|
|
38
|
+
capture_output=True,
|
|
39
|
+
text=True,
|
|
40
|
+
check=True,
|
|
41
|
+
)
|
|
42
|
+
return Path(result.stdout.strip())
|
|
43
|
+
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
|
|
44
|
+
return Path.cwd()
|
|
45
|
+
|
|
46
|
+
|
|
21
47
|
def parse_args() -> argparse.Namespace:
|
|
22
48
|
parser = argparse.ArgumentParser(
|
|
23
49
|
description="Resolve the latest completed board date for a documentation target.",
|
|
@@ -25,7 +51,7 @@ def parse_args() -> argparse.Namespace:
|
|
|
25
51
|
parser.add_argument("target_path", help="Repo-relative documentation target path, e.g. README.md")
|
|
26
52
|
parser.add_argument(
|
|
27
53
|
"--root",
|
|
28
|
-
default=
|
|
54
|
+
default=_resolve_project_root(),
|
|
29
55
|
type=Path,
|
|
30
56
|
help="Repository root containing project/board/",
|
|
31
57
|
)
|
|
@@ -28,7 +28,18 @@
|
|
|
28
28
|
|
|
29
29
|
set -euo pipefail
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
# Locate the project root via git, not a fixed relative climb — this script
|
|
32
|
+
# is deployed both at its source location (skills/j-todo/scripts/) and
|
|
33
|
+
# mirrored to .claude/skills/j-todo/scripts/ / .agents/skills/j-todo/scripts/,
|
|
34
|
+
# and a fixed "three levels up" climb lands in the wrong place from a mirror.
|
|
35
|
+
# Same approach as skills/j-close-story/scripts/check-story-closeable.sh,
|
|
36
|
+
# check-privatized.sh:138, and compute-scope-divergence.sh:54.
|
|
37
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
38
|
+
REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || true)"
|
|
39
|
+
if [[ -z "$REPO_ROOT" ]]; then
|
|
40
|
+
echo "ERROR: could not locate project root (git rev-parse --show-toplevel failed from $SCRIPT_DIR)" >&2
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
32
43
|
cd "$REPO_ROOT"
|
|
33
44
|
|
|
34
45
|
TASKS_DIR="project/board/tasks"
|
package/skills/j-wtf/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: j.wtf
|
|
3
|
-
description: Alias of /clearify — clarifies ambiguous, dense, or under-specified prompts and conversation on request. This folder exists
|
|
3
|
+
description: Alias of /clearify — clarifies ambiguous, dense, or under-specified prompts and conversation on request. This folder exists so `j.wtf` (and its `/j-wtf` directory form) resolves to a skill; behaviour is identical to `/clearify`.
|
|
4
4
|
keywords:
|
|
5
5
|
- wtf
|
|
6
6
|
- confused
|
package/skills/jenga/SKILL.md
CHANGED
|
@@ -137,11 +137,33 @@ If all applicable rules pass (or the task is a legacy task), proceed to the next
|
|
|
137
137
|
|
|
138
138
|
This phase determines **how `/jenga` was invoked** and, for two of the four entry modes, produces a **scoped set** — a confirmed list of board IDs (epics/stories/tasks) that Phases 1-4 must restrict themselves to. All board scanning, ID parsing, cascade expansion, and rendering used by this phase already live in `skills/jenga/scripts/` per this repo's "Scripts Over Inline Logic" principle — this phase never re-implements any of that logic inline. The executing agent's job here is limited to: invoking the right script with the right arguments, relaying its STDOUT verbatim to the user when the contract calls for that, capturing the `STATE_FILE:` line from STDERR for the next turn, and forwarding the user's raw reply back into the next invocation unmodified.
|
|
139
139
|
|
|
140
|
-
**
|
|
140
|
+
**First, check for the `--enrich` flag** (`E53_S13_T01`, ported from the now-retired `/route`'s board
|
|
141
|
+
+ docs enrichment — see the Natural-language branch's step 3 and the Skill Matching & Invocation
|
|
142
|
+
Contract's Report format below for what it actually does). If the raw argument passed to `/jenga`
|
|
143
|
+
begins with the exact leading token `--enrich` followed by at least one space, strip that token
|
|
144
|
+
(and the single space after it) before anything else runs, and remember `enrichment_requested =
|
|
145
|
+
true` for the remainder of this invocation. The **remaining text** — never the original argument
|
|
146
|
+
with the flag still attached — is what every step below (including `detect-nl-intent.sh`) treats as
|
|
147
|
+
"the raw argument passed to `/jenga`"; this is what keeps the flag from ever being visible to
|
|
148
|
+
`detect-nl-intent.sh`'s `all_resolved`/`nl_intent`/`mixed` classification. If `--enrich` is not
|
|
149
|
+
present as a leading token, `enrichment_requested = false` and the argument is used as-is —
|
|
150
|
+
byte-for-byte the same behavior as before this flag existed.
|
|
151
|
+
|
|
152
|
+
`--enrich` alone (nothing after it, once whitespace is stripped) reduces to an empty remaining
|
|
153
|
+
argument — treat this exactly like no argument at all (**bare branch**); the flag has nothing to
|
|
154
|
+
enrich without free-form text and bare `/jenga` never does skill matching. `--enrich *` reduces to
|
|
155
|
+
the **wildcard branch** the same way (remaining text is the literal `*`); the wildcard branch also
|
|
156
|
+
never does skill matching, so `enrichment_requested` is simply never consulted there. Concretely,
|
|
157
|
+
`enrichment_requested` is only ever read in the **natural-language branch** below, and only once
|
|
158
|
+
that branch's own step 3 confirms a single-skill match — it is inert everywhere else. Passing
|
|
159
|
+
`--enrich` on a scoped (`<ids>`) argument is likewise a no-op: the scoped branch does not perform
|
|
160
|
+
skill matching either, so there is nothing for the flag to attach to.
|
|
161
|
+
|
|
162
|
+
**Then, determine the invocation form** from the remaining argument (if any) passed to `/jenga`:
|
|
141
163
|
|
|
142
164
|
- No argument at all → **bare branch**.
|
|
143
165
|
- The argument is the literal string `*` → **wildcard branch**.
|
|
144
|
-
- Any other non-empty argument → invoke `skills/jenga/scripts/detect-nl-intent.sh "<
|
|
166
|
+
- Any other non-empty argument → invoke `skills/jenga/scripts/detect-nl-intent.sh "<remaining argument>"` (E53_S01_T01) and branch on its `classification` field:
|
|
145
167
|
- `all_resolved` or `mixed` → **scoped branch** (below) — this is the same branch as before; only its internal mechanics changed (see below).
|
|
146
168
|
- `nl_intent` → **natural-language branch** (below) — new for E53_S01, no new sigil or entry point, purely a new outcome of this same argument-shape detection.
|
|
147
169
|
|
|
@@ -170,7 +192,12 @@ This branch is entered when `detect-nl-intent.sh` classifies the argument as `nl
|
|
|
170
192
|
|
|
171
193
|
1. **Load the catalog** — invoke `skills/jenga/scripts/load-nl-catalog.sh` with no arguments (E53_S01_T02). Its stdout is the full skill catalog (`name`/`description`/`keywords`/`examples`/`prefered_agent` per skill), sourced exclusively from `lib/generate-skill-allow-list.js`'s generated inventory — see the script's own header for the full contract. Never re-derive this catalog by re-scanning `skills/` inline.
|
|
172
194
|
2. **Match** — run this section's own **Skill Matching & Invocation Contract** (below) — the three-pass keyword → example-similarity → description match, including its tie-break and no-match handling — against this catalog, treating `detect-nl-intent.sh`'s `raw_argument` field as the prompt.
|
|
173
|
-
3. **Confident single match** —
|
|
195
|
+
3. **Confident single match** — if `enrichment_requested` is `true` (the `--enrich` flag was passed, per Phase 0.75's preamble above), first invoke `skills/jenga/scripts/enrich-nl-prompt.sh "<raw_argument>"` (`E53_S13_T01`, ported from `/route`'s Steps 3-5) and assemble the enriched composite message from its JSON output, in this exact order:
|
|
196
|
+
- **Part A — Matched skill (full content)** — the matched skill's full `SKILL.md` body (everything after its YAML front-matter), wrapped as `<!-- SKILL: /<matched-skill-name> --> ... <!-- END SKILL -->`.
|
|
197
|
+
- **Part B — Board & documentation context** — a `## 📋 Relevant Board Context` list (one line per `board_items` entry: `- [<status>] **<id>** — <title> (\`<file>\`)`) followed by a `## 📄 Relevant Documentation` list (one line per `docs` entry: `` - `<path>` — <summary> ``). Omit either sub-list entirely (not an empty heading) when its array is empty.
|
|
198
|
+
- **Part C — Original prompt** — `## 🗣 Original Prompt` followed by the raw prompt, verbatim, in a blockquote.
|
|
199
|
+
|
|
200
|
+
Then report the routing decision using the **Skill Matching & Invocation Contract**'s **Report** format (including its `Board items found`/`Docs found` lines, populated from `enrich-nl-prompt.sh`'s `board_items_found`/`docs_found` fields, since `enrichment_requested` is `true` here), and invoke the matched skill exactly as the **Invoke** rule already does — but deliver the enriched composite message as the working input instead of the raw prompt when enrichment ran. When `enrichment_requested` is `false` (the default, unflagged path — unchanged from before this flag existed), skip `enrich-nl-prompt.sh` entirely: report using the **Report** format's default (no `Board items found`/`Docs found` lines) and invoke the matched skill directly with the raw prompt, exactly as before. Either way: load `agents/<prefered_agent>.md` when the matched skill specifies `metadata.prefered_agent`, otherwise execute the skill instructions directly. The matched skill's own execution takes over from here — do not continue into this `/jenga` invocation's Phase 1.
|
|
174
201
|
4. **No match, or an ambiguous multi-way tie (single-skill match)** — before surfacing the **Skill Matching & Invocation Contract**'s generic disambiguation options, attempt a **playbook fallback** (E53_S02): invoke `skills/jenga/scripts/match-playbook.sh "<raw_argument>"`. This step only ever runs when step 3 above did NOT already commit to a confident single-skill match — a confident single-skill match always wins outright and this playbook fallback is never even invoked in that case. Branch on `match-playbook.sh`'s `classification` field:
|
|
175
202
|
- `playbook_match` → continue to **step 5 (Playbook proposal and execution)** below.
|
|
176
203
|
- `ambiguous` or `no_match` → continue to **step 6 (Fall through to the Skill Matching & Invocation Contract's disambiguation)** below — the exact behavior this branch already had before E53_S02, unchanged.
|
|
@@ -217,14 +244,11 @@ This branch is entered when `detect-nl-intent.sh` classifies the argument as `nl
|
|
|
217
244
|
|
|
218
245
|
##### Skill Matching & Invocation Contract
|
|
219
246
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
`skills/j-route/SKILL.md`'s own Step 2/6/7 carry the authoritative copy of this same contract for
|
|
226
|
-
`/route`'s own use — the two are intentionally duplicated for public-mirror reasons; keep them in sync
|
|
227
|
-
by hand if either changes.
|
|
247
|
+
`/jenga` is the **sole owner** of this contract (`E53_S13_T02`). `/route` — which previously carried
|
|
248
|
+
an independent, hand-synced copy of this same three-pass matching/tie-break/report logic in its own
|
|
249
|
+
Step 2/6/7 — has been retired outright (hard break, no shim; see `E53_S13`'s story). There is no
|
|
250
|
+
other copy of this contract anywhere in the codebase to keep in sync with; if you change the matching
|
|
251
|
+
logic here, this section is the only place that needs updating.
|
|
228
252
|
|
|
229
253
|
**Matching** (three passes, stop at first confident match):
|
|
230
254
|
|
|
@@ -269,9 +293,20 @@ Routing to: /<matched-skill-name>
|
|
|
269
293
|
Reason: <one sentence explaining why this skill was chosen>
|
|
270
294
|
```
|
|
271
295
|
|
|
272
|
-
(
|
|
273
|
-
|
|
274
|
-
|
|
296
|
+
**`Board items found`/`Docs found` (opt-in, `E53_S13_T01`)** — these two lines are appended to the
|
|
297
|
+
Report above, in this order, **only** when the natural-language branch's `enrichment_requested` is
|
|
298
|
+
`true` (i.e. the caller passed `--enrich`, per Phase 0.75's preamble):
|
|
299
|
+
|
|
300
|
+
```
|
|
301
|
+
Board items found: <count>
|
|
302
|
+
Docs found: <count>
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
`<count>` is `enrich-nl-prompt.sh`'s `board_items_found`/`docs_found` field respectively (the total
|
|
306
|
+
match count before the top-5/top-3 cap, not the number of items actually listed in the enriched
|
|
307
|
+
prompt's Part B). When `enrichment_requested` is `false` — the default path, unchanged from before
|
|
308
|
+
this flag existed — neither line is emitted; the Report is exactly the two lines above and nothing
|
|
309
|
+
more.
|
|
275
310
|
|
|
276
311
|
Then proceed immediately — do not wait for user confirmation unless the match was ambiguous (the
|
|
277
312
|
tie-break above already handled that).
|
|
@@ -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.
|