@jenga-ai/agent 1.2.4 → 1.3.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 +1 -0
- package/agents/developer.md +18 -0
- package/agents/scrum-master.md +1 -0
- package/agents/tester.md +18 -0
- package/hooks/on_session_end.sh +27 -0
- package/package.json +18 -17
- package/skills/commit/SKILL.md +11 -1
- package/skills/dev-done/SKILL.md +46 -0
- package/skills/dev-done/scripts/classify-commit-outcome.sh +114 -0
- package/skills/init/SKILL.md +7 -6
- package/skills/init/assets/scope-thresholds_template.json +7 -0
- package/skills/init/scripts/init.sh +6 -0
- package/skills/publish/SKILL.md +66 -0
- package/skills/publish/adapters/npm-ci.md +34 -0
- package/skills/publish/adapters/npm.md +18 -0
- package/skills/publish/assets/ci-contract.md +27 -0
- package/skills/publish/assets/publish.example.json +27 -0
- package/skills/publish/schemas/publish.schema.json +20 -0
- package/skills/publish/scripts/npm_ci_pipeline.sh +29 -0
- package/skills/publish/scripts/npm_stage_inspect.sh +829 -0
- package/skills/publish/scripts/npm_stage_pipeline.sh +427 -0
- package/skills/publish/scripts/publish_common.sh +16 -0
- package/skills/publish/scripts/show_history.sh +12 -5
- package/skills/publish/scripts/validate_npm_stage_env.sh +184 -0
- package/skills/publish/scripts/write_ledger_entry.sh +92 -2
- package/skills/reconcile/SKILL.md +121 -11
- package/skills/reconcile/assets/report_format.md +17 -0
- package/skills/reconcile/scripts/resolve-reconcile-scope.sh +489 -0
- package/skills/uncharted/SKILL.md +200 -21
- package/skills/uncharted/scripts/directory-triage.sh +342 -0
- package/skills/uncharted/scripts/elicitation-state.sh +457 -0
- package/templates/SCRUM_BOARD_SCHEMA.md +57 -0
- package/templates/agent-context.md.tpl +23 -11
- package/templates/copilot-instructions.md.tpl +21 -11
- package/mcp/router/README.md +0 -19
- package/mcp/router/embedder.js +0 -23
- package/mcp/router/index.js +0 -204
- package/mcp/router/matcher.js +0 -87
- package/mcp/router/package-lock.json +0 -1048
- package/mcp/router/package.json +0 -11
- package/mcp/router/skill-index.js +0 -104
- package/skills/route/SKILL.md +0 -180
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ---------------------------------------------------------------------------
|
|
3
|
+
# skills/uncharted/scripts/elicitation-state.sh
|
|
4
|
+
#
|
|
5
|
+
# Deterministic persistence + turn-cap counting for `/uncharted`'s
|
|
6
|
+
# conversational convergence loop (E20_S08_T03) — the "propose understanding,
|
|
7
|
+
# confirm/correct" cycle that runs during `onboard`'s default (non-`--legacy`)
|
|
8
|
+
# flow and `segment --mode investigate`.
|
|
9
|
+
#
|
|
10
|
+
# Two problems this script exists to solve mechanically rather than leave to
|
|
11
|
+
# agent memory across a long, possibly multi-session conversation:
|
|
12
|
+
#
|
|
13
|
+
# 1. HARD TURN CAP (solution-assessment-uncharted-interactive-elicitation.md,
|
|
14
|
+
# Problem 10). A convergence loop with no termination bound can run
|
|
15
|
+
# forever on a genuinely hard case. `turn` increments a per-node
|
|
16
|
+
# counter and exits 3 — not 0 — the instant the cap is reached, so the
|
|
17
|
+
# caller gets an unmissable, mechanical stop signal instead of having to
|
|
18
|
+
# remember to compare numbers itself.
|
|
19
|
+
#
|
|
20
|
+
# 2. MULTI-SESSION PERSISTENCE (same document, Problem 11). A whole-
|
|
21
|
+
# codebase onboard conversation can span many sessions. `init` /
|
|
22
|
+
# `checkpoint` / `pause` / `complete` / `list-paused` give the
|
|
23
|
+
# conversation a durable, resumable state file, checkpointed after each
|
|
24
|
+
# converged node (per that problem's RECOMMENDED solution) rather than
|
|
25
|
+
# only at the very end.
|
|
26
|
+
#
|
|
27
|
+
# This script performs NO judgement — it does not decide what to ask, what
|
|
28
|
+
# counts as high-impact, or when understanding has actually converged. It
|
|
29
|
+
# only counts turns, tracks status, and persists whatever the agent asks it
|
|
30
|
+
# to persist. All of that judgement lives in skills/uncharted/SKILL.md's
|
|
31
|
+
# Convergence Loop subsection.
|
|
32
|
+
#
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# STATE FILE
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# project/queue/elicitation-state/<id>.json — one file per elicitation run
|
|
37
|
+
# (a whole `onboard` pass, or a single `segment --mode investigate` target).
|
|
38
|
+
# This directory is git-ignored (mirrors project/queue/handoffs/ — see
|
|
39
|
+
# templates/SCRUM_BOARD_SCHEMA.md): it is session-scratch resumability data,
|
|
40
|
+
# never a durable artifact. The durable output of a converged elicitation is
|
|
41
|
+
# the graph write (project/knowledge-graph/graph.json, per the stub schema at
|
|
42
|
+
# project/knowledge-graph/STUB_SCHEMA.md) and the `[ARCH]`-tagged board item —
|
|
43
|
+
# both written by the agent driving the flow, never by this script.
|
|
44
|
+
#
|
|
45
|
+
# Shape:
|
|
46
|
+
# {
|
|
47
|
+
# "id": "<id>",
|
|
48
|
+
# "target": "<free text, e.g. the investigated path or description>",
|
|
49
|
+
# "cap": <int>, // hard turn cap, default 5
|
|
50
|
+
# "status": "in_progress" | "paused" | "complete",
|
|
51
|
+
# "created_at": "<ISO 8601 UTC>",
|
|
52
|
+
# "updated_at": "<ISO 8601 UTC>",
|
|
53
|
+
# "nodes": {
|
|
54
|
+
# "<node-id>": { "turns": <int>, "status": "pending"|"converged"|"flagged", "note": "<text>" }
|
|
55
|
+
# },
|
|
56
|
+
# "checkpoint": { ...arbitrary, agent-defined fields, e.g. directory-triage results... }
|
|
57
|
+
# }
|
|
58
|
+
#
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# SUBCOMMANDS
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# init --id ID [--target TEXT] [--cap N]
|
|
63
|
+
# Create the state file if it does not already exist (default cap 5,
|
|
64
|
+
# per the solution assessment's "3-5 rounds" recommendation). Idempotent
|
|
65
|
+
# — calling init again on an existing id returns the existing state
|
|
66
|
+
# unchanged rather than resetting it, so a resumed session can call
|
|
67
|
+
# init unconditionally without wiping progress.
|
|
68
|
+
#
|
|
69
|
+
# turn --id ID --node NODE
|
|
70
|
+
# Increment NODE's turn counter by one. Prints
|
|
71
|
+
# {"turns": N, "cap": C, "cap_reached": true|false}. EXITS 3 (not 0)
|
|
72
|
+
# when the increment reaches the cap — the node's status is also set to
|
|
73
|
+
# "flagged" in the state file at that point, so the cap event is
|
|
74
|
+
# durable, not just a transient exit code the caller might not act on.
|
|
75
|
+
#
|
|
76
|
+
# converge --id ID --node NODE [--note TEXT]
|
|
77
|
+
# Mark NODE's status "converged", independent of whether the cap was
|
|
78
|
+
# ever hit (a node can converge on turn 1). Optional TEXT is stored as
|
|
79
|
+
# the node's note (e.g. a one-line summary of what was confirmed).
|
|
80
|
+
#
|
|
81
|
+
# checkpoint --id ID --json FILE
|
|
82
|
+
# Shallow-merge the JSON object in FILE (or stdin when FILE is "-")
|
|
83
|
+
# into the state's top-level "checkpoint" field. New keys are added;
|
|
84
|
+
# existing keys are overwritten by the new value. This is the generic
|
|
85
|
+
# "save progress" primitive — directory-triage results, draft node
|
|
86
|
+
# content, anything else the flow wants durable before it might pause.
|
|
87
|
+
#
|
|
88
|
+
# pause --id ID
|
|
89
|
+
# Set status "paused" and update "updated_at". The caller (the agent
|
|
90
|
+
# driving `/uncharted`) is responsible for also writing a scrum-master
|
|
91
|
+
# SessionEnd handoff with status "elicitation_paused" and this state
|
|
92
|
+
# file's path, per skills/uncharted/SKILL.md's Multi-Session
|
|
93
|
+
# Persistence subsection — this script only updates the state file
|
|
94
|
+
# itself, it does not write handoffs.
|
|
95
|
+
#
|
|
96
|
+
# complete --id ID
|
|
97
|
+
# Set status "complete" and update "updated_at". A completed
|
|
98
|
+
# elicitation's state file is left on disk (not deleted) as an audit
|
|
99
|
+
# trail of what was asked and confirmed; nothing currently prunes it.
|
|
100
|
+
#
|
|
101
|
+
# status --id ID
|
|
102
|
+
# Read-only. Prints the current state file, pretty-printed.
|
|
103
|
+
#
|
|
104
|
+
# list-paused
|
|
105
|
+
# Read-only. Prints a JSON array of every state file currently
|
|
106
|
+
# "status": "paused" — {"id", "state_file", "target", "updated_at"} per
|
|
107
|
+
# entry — for a resuming session (or on_session_end.sh's routing logic)
|
|
108
|
+
# to discover what is waiting to be picked back up.
|
|
109
|
+
#
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
# CONCURRENCY
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
113
|
+
# Every mutating subcommand (init/turn/converge/checkpoint/pause/complete)
|
|
114
|
+
# wraps its read-modify-write in scripts/with-lock.sh, keyed to the target
|
|
115
|
+
# state file — the same atomic mkdir-based lock already used for board files
|
|
116
|
+
# and events.json (see hooks/on_session_end.sh and
|
|
117
|
+
# templates/SCRUM_BOARD_SCHEMA.md's File Locking section), not a new
|
|
118
|
+
# concurrency mechanism. The write itself is atomic (temp file in the same
|
|
119
|
+
# directory, then `mv`), matching the pattern in hooks/on_session_end.sh's
|
|
120
|
+
# own events.json append.
|
|
121
|
+
#
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
# EXIT CODES
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# 0 — success
|
|
126
|
+
# 1 — usage error: unknown subcommand/flag, missing required argument
|
|
127
|
+
# 2 — input error: state file missing for a subcommand that requires it
|
|
128
|
+
# (everything except init/list-paused), or --json input unreadable/
|
|
129
|
+
# not a JSON object
|
|
130
|
+
# 3 — turn cap reached (ONLY for `turn`; the increment still happened and
|
|
131
|
+
# was persisted — this is a signal to stop looping, not a failure)
|
|
132
|
+
# 4 — write failure: could not acquire the lock, or the atomic write failed
|
|
133
|
+
#
|
|
134
|
+
# Examples:
|
|
135
|
+
# elicitation-state.sh init --id onboard-2026-09-01 --target . --cap 5
|
|
136
|
+
# elicitation-state.sh turn --id onboard-2026-09-01 --node billing-worker
|
|
137
|
+
# elicitation-state.sh converge --id onboard-2026-09-01 --node billing-worker --note "confirmed: reconciles ledger entries"
|
|
138
|
+
# echo '{"directory_triage": {...}}' | elicitation-state.sh checkpoint --id onboard-2026-09-01 --json -
|
|
139
|
+
# elicitation-state.sh pause --id onboard-2026-09-01
|
|
140
|
+
# elicitation-state.sh list-paused
|
|
141
|
+
#
|
|
142
|
+
# Requires: bash, python3, git (to resolve the repo root).
|
|
143
|
+
|
|
144
|
+
set -euo pipefail
|
|
145
|
+
|
|
146
|
+
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
|
|
147
|
+
REPO_ROOT=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || true)
|
|
148
|
+
|
|
149
|
+
if [ -z "$REPO_ROOT" ]; then
|
|
150
|
+
echo "elicitation-state.sh: could not resolve repository root from $SCRIPT_DIR (not inside a git work tree?)" >&2
|
|
151
|
+
exit 2
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
WITH_LOCK="$REPO_ROOT/scripts/with-lock.sh"
|
|
155
|
+
STATE_DIR="$REPO_ROOT/project/queue/elicitation-state"
|
|
156
|
+
DEFAULT_CAP=5
|
|
157
|
+
|
|
158
|
+
mkdir -p "$STATE_DIR"
|
|
159
|
+
|
|
160
|
+
usage() {
|
|
161
|
+
cat <<'EOF'
|
|
162
|
+
Usage: elicitation-state.sh <subcommand> [options]
|
|
163
|
+
|
|
164
|
+
Subcommands:
|
|
165
|
+
init --id ID [--target TEXT] [--cap N]
|
|
166
|
+
turn --id ID --node NODE
|
|
167
|
+
converge --id ID --node NODE [--note TEXT]
|
|
168
|
+
checkpoint --id ID --json FILE|-
|
|
169
|
+
pause --id ID
|
|
170
|
+
complete --id ID
|
|
171
|
+
status --id ID
|
|
172
|
+
list-paused
|
|
173
|
+
|
|
174
|
+
See this script's own header comment for full semantics of each subcommand.
|
|
175
|
+
|
|
176
|
+
Exit codes: 0 success, 1 usage error, 2 input error, 3 turn cap reached
|
|
177
|
+
(only for `turn`), 4 write failure.
|
|
178
|
+
EOF
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if [ "$#" -eq 0 ]; then
|
|
182
|
+
usage
|
|
183
|
+
exit 1
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
SUBCOMMAND="$1"
|
|
187
|
+
shift
|
|
188
|
+
|
|
189
|
+
if [ "$SUBCOMMAND" = "-h" ] || [ "$SUBCOMMAND" = "--help" ]; then
|
|
190
|
+
usage
|
|
191
|
+
exit 0
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
ID=""
|
|
195
|
+
NODE=""
|
|
196
|
+
TARGET=""
|
|
197
|
+
CAP="$DEFAULT_CAP"
|
|
198
|
+
NOTE=""
|
|
199
|
+
JSON_ARG=""
|
|
200
|
+
|
|
201
|
+
while [ "$#" -gt 0 ]; do
|
|
202
|
+
case "$1" in
|
|
203
|
+
--id)
|
|
204
|
+
[ "$#" -ge 2 ] || { usage; exit 1; }
|
|
205
|
+
ID="$2"; shift 2 ;;
|
|
206
|
+
--node)
|
|
207
|
+
[ "$#" -ge 2 ] || { usage; exit 1; }
|
|
208
|
+
NODE="$2"; shift 2 ;;
|
|
209
|
+
--target)
|
|
210
|
+
[ "$#" -ge 2 ] || { usage; exit 1; }
|
|
211
|
+
TARGET="$2"; shift 2 ;;
|
|
212
|
+
--cap)
|
|
213
|
+
[ "$#" -ge 2 ] || { usage; exit 1; }
|
|
214
|
+
CAP="$2"; shift 2 ;;
|
|
215
|
+
--note)
|
|
216
|
+
[ "$#" -ge 2 ] || { usage; exit 1; }
|
|
217
|
+
NOTE="$2"; shift 2 ;;
|
|
218
|
+
--json)
|
|
219
|
+
[ "$#" -ge 2 ] || { usage; exit 1; }
|
|
220
|
+
JSON_ARG="$2"; shift 2 ;;
|
|
221
|
+
-h|--help)
|
|
222
|
+
usage; exit 0 ;;
|
|
223
|
+
*)
|
|
224
|
+
echo "elicitation-state.sh: unknown argument '$1'" >&2
|
|
225
|
+
exit 1 ;;
|
|
226
|
+
esac
|
|
227
|
+
done
|
|
228
|
+
|
|
229
|
+
case "$SUBCOMMAND" in
|
|
230
|
+
init|turn|converge|checkpoint|pause|complete|status)
|
|
231
|
+
if [ -z "$ID" ]; then
|
|
232
|
+
echo "elicitation-state.sh: --id is required for '$SUBCOMMAND'" >&2
|
|
233
|
+
exit 1
|
|
234
|
+
fi
|
|
235
|
+
;;
|
|
236
|
+
list-paused)
|
|
237
|
+
;;
|
|
238
|
+
*)
|
|
239
|
+
echo "elicitation-state.sh: unknown subcommand '$SUBCOMMAND'" >&2
|
|
240
|
+
exit 1
|
|
241
|
+
;;
|
|
242
|
+
esac
|
|
243
|
+
|
|
244
|
+
if [ "$SUBCOMMAND" = "turn" ] || [ "$SUBCOMMAND" = "converge" ]; then
|
|
245
|
+
if [ -z "$NODE" ]; then
|
|
246
|
+
echo "elicitation-state.sh: --node is required for '$SUBCOMMAND'" >&2
|
|
247
|
+
exit 1
|
|
248
|
+
fi
|
|
249
|
+
fi
|
|
250
|
+
|
|
251
|
+
if [ "$SUBCOMMAND" = "checkpoint" ] && [ -z "$JSON_ARG" ]; then
|
|
252
|
+
echo "elicitation-state.sh: --json is required for 'checkpoint'" >&2
|
|
253
|
+
exit 1
|
|
254
|
+
fi
|
|
255
|
+
|
|
256
|
+
if [ -n "$ID" ]; then
|
|
257
|
+
STATE_FILE="$STATE_DIR/${ID}.json"
|
|
258
|
+
fi
|
|
259
|
+
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
# Read-only subcommands — no lock needed, nothing is mutated.
|
|
262
|
+
# ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
if [ "$SUBCOMMAND" = "status" ]; then
|
|
265
|
+
if [ ! -f "$STATE_FILE" ]; then
|
|
266
|
+
echo "elicitation-state.sh: no state file for id '$ID' ($STATE_FILE)" >&2
|
|
267
|
+
exit 2
|
|
268
|
+
fi
|
|
269
|
+
python3 -m json.tool "$STATE_FILE"
|
|
270
|
+
exit 0
|
|
271
|
+
fi
|
|
272
|
+
|
|
273
|
+
if [ "$SUBCOMMAND" = "list-paused" ]; then
|
|
274
|
+
python3 -c '
|
|
275
|
+
import json, glob, os, sys
|
|
276
|
+
|
|
277
|
+
state_dir = sys.argv[1]
|
|
278
|
+
paused = []
|
|
279
|
+
for path in sorted(glob.glob(os.path.join(state_dir, "*.json"))):
|
|
280
|
+
try:
|
|
281
|
+
with open(path) as f:
|
|
282
|
+
data = json.load(f)
|
|
283
|
+
except (OSError, ValueError):
|
|
284
|
+
continue
|
|
285
|
+
if data.get("status") == "paused":
|
|
286
|
+
paused.append({
|
|
287
|
+
"id": data.get("id", os.path.splitext(os.path.basename(path))[0]),
|
|
288
|
+
"state_file": path,
|
|
289
|
+
"target": data.get("target", ""),
|
|
290
|
+
"updated_at": data.get("updated_at", ""),
|
|
291
|
+
})
|
|
292
|
+
print(json.dumps(paused, indent=2))
|
|
293
|
+
' "$STATE_DIR"
|
|
294
|
+
exit 0
|
|
295
|
+
fi
|
|
296
|
+
|
|
297
|
+
# ---------------------------------------------------------------------------
|
|
298
|
+
# Mutating subcommands — everything below goes through with-lock.sh.
|
|
299
|
+
# ---------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
if [ ! -f "$WITH_LOCK" ]; then
|
|
302
|
+
echo "elicitation-state.sh: scripts/with-lock.sh not found at $WITH_LOCK" >&2
|
|
303
|
+
exit 4
|
|
304
|
+
fi
|
|
305
|
+
|
|
306
|
+
if [ "$SUBCOMMAND" != "init" ] && [ ! -f "$STATE_FILE" ]; then
|
|
307
|
+
echo "elicitation-state.sh: no state file for id '$ID' ($STATE_FILE) — run 'init' first" >&2
|
|
308
|
+
exit 2
|
|
309
|
+
fi
|
|
310
|
+
|
|
311
|
+
# checkpoint's JSON payload is read here (outside the lock) so a bad/missing
|
|
312
|
+
# file fails fast with exit 2 before ever touching the lock.
|
|
313
|
+
CHECKPOINT_JSON="{}"
|
|
314
|
+
if [ "$SUBCOMMAND" = "checkpoint" ]; then
|
|
315
|
+
if [ "$JSON_ARG" = "-" ]; then
|
|
316
|
+
CHECKPOINT_JSON=$(cat)
|
|
317
|
+
else
|
|
318
|
+
if [ ! -f "$JSON_ARG" ]; then
|
|
319
|
+
echo "elicitation-state.sh: --json file '$JSON_ARG' does not exist" >&2
|
|
320
|
+
exit 2
|
|
321
|
+
fi
|
|
322
|
+
CHECKPOINT_JSON=$(cat "$JSON_ARG")
|
|
323
|
+
fi
|
|
324
|
+
if ! echo "$CHECKPOINT_JSON" | python3 -c 'import json,sys; d=json.load(sys.stdin); assert isinstance(d, dict)' 2>/dev/null; then
|
|
325
|
+
echo "elicitation-state.sh: --json payload is not a JSON object" >&2
|
|
326
|
+
exit 2
|
|
327
|
+
fi
|
|
328
|
+
fi
|
|
329
|
+
|
|
330
|
+
# The update logic is captured as a standalone python3 script and run via
|
|
331
|
+
# `python3 -c` with every value passed as a positional argv entry (never
|
|
332
|
+
# interpolated into the script text), mirroring the same avoid-fragile-
|
|
333
|
+
# quoting convention hooks/on_session_end.sh already uses for its own
|
|
334
|
+
# events.json read-modify-write.
|
|
335
|
+
UPDATE_SCRIPT=$(cat <<'PYEOF'
|
|
336
|
+
import json, os, sys, tempfile, datetime
|
|
337
|
+
|
|
338
|
+
state_file, subcommand, elicitation_id, target, cap_s, node, note, checkpoint_json = sys.argv[1:9]
|
|
339
|
+
|
|
340
|
+
now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
341
|
+
|
|
342
|
+
def load():
|
|
343
|
+
with open(state_file) as f:
|
|
344
|
+
return json.load(f)
|
|
345
|
+
|
|
346
|
+
def atomic_write(data):
|
|
347
|
+
dir_name = os.path.dirname(state_file)
|
|
348
|
+
fd, tmp_path = tempfile.mkstemp(prefix=".elicitation_tmp.", dir=dir_name)
|
|
349
|
+
try:
|
|
350
|
+
with os.fdopen(fd, "w") as f:
|
|
351
|
+
json.dump(data, f, indent=2)
|
|
352
|
+
f.write("\n")
|
|
353
|
+
os.replace(tmp_path, state_file)
|
|
354
|
+
except Exception:
|
|
355
|
+
if os.path.exists(tmp_path):
|
|
356
|
+
os.remove(tmp_path)
|
|
357
|
+
raise
|
|
358
|
+
|
|
359
|
+
result = {}
|
|
360
|
+
exit_code = 0
|
|
361
|
+
|
|
362
|
+
if subcommand == "init":
|
|
363
|
+
if os.path.exists(state_file):
|
|
364
|
+
state = load()
|
|
365
|
+
result = {"created": False, "state": state}
|
|
366
|
+
else:
|
|
367
|
+
cap = int(cap_s)
|
|
368
|
+
state = {
|
|
369
|
+
"id": elicitation_id,
|
|
370
|
+
"target": target,
|
|
371
|
+
"cap": cap,
|
|
372
|
+
"status": "in_progress",
|
|
373
|
+
"created_at": now,
|
|
374
|
+
"updated_at": now,
|
|
375
|
+
"nodes": {},
|
|
376
|
+
"checkpoint": {},
|
|
377
|
+
}
|
|
378
|
+
atomic_write(state)
|
|
379
|
+
result = {"created": True, "state": state}
|
|
380
|
+
|
|
381
|
+
elif subcommand == "turn":
|
|
382
|
+
state = load()
|
|
383
|
+
nodes = state.setdefault("nodes", {})
|
|
384
|
+
entry = nodes.setdefault(node, {"turns": 0, "status": "pending", "note": ""})
|
|
385
|
+
entry["turns"] = entry.get("turns", 0) + 1
|
|
386
|
+
cap = state.get("cap", int(cap_s))
|
|
387
|
+
cap_reached = entry["turns"] >= cap
|
|
388
|
+
if cap_reached:
|
|
389
|
+
entry["status"] = "flagged"
|
|
390
|
+
state["updated_at"] = now
|
|
391
|
+
atomic_write(state)
|
|
392
|
+
result = {"turns": entry["turns"], "cap": cap, "cap_reached": cap_reached}
|
|
393
|
+
if cap_reached:
|
|
394
|
+
exit_code = 3
|
|
395
|
+
|
|
396
|
+
elif subcommand == "converge":
|
|
397
|
+
state = load()
|
|
398
|
+
nodes = state.setdefault("nodes", {})
|
|
399
|
+
entry = nodes.setdefault(node, {"turns": 0, "status": "pending", "note": ""})
|
|
400
|
+
entry["status"] = "converged"
|
|
401
|
+
if note:
|
|
402
|
+
entry["note"] = note
|
|
403
|
+
state["updated_at"] = now
|
|
404
|
+
atomic_write(state)
|
|
405
|
+
result = {"node": node, "status": "converged", "turns": entry.get("turns", 0)}
|
|
406
|
+
|
|
407
|
+
elif subcommand == "checkpoint":
|
|
408
|
+
state = load()
|
|
409
|
+
payload = json.loads(checkpoint_json)
|
|
410
|
+
cp = state.setdefault("checkpoint", {})
|
|
411
|
+
cp.update(payload)
|
|
412
|
+
state["updated_at"] = now
|
|
413
|
+
atomic_write(state)
|
|
414
|
+
result = {"checkpoint_keys": list(payload.keys())}
|
|
415
|
+
|
|
416
|
+
elif subcommand == "pause":
|
|
417
|
+
state = load()
|
|
418
|
+
state["status"] = "paused"
|
|
419
|
+
state["updated_at"] = now
|
|
420
|
+
atomic_write(state)
|
|
421
|
+
result = {"status": "paused"}
|
|
422
|
+
|
|
423
|
+
elif subcommand == "complete":
|
|
424
|
+
state = load()
|
|
425
|
+
state["status"] = "complete"
|
|
426
|
+
state["updated_at"] = now
|
|
427
|
+
atomic_write(state)
|
|
428
|
+
result = {"status": "complete"}
|
|
429
|
+
|
|
430
|
+
else:
|
|
431
|
+
sys.stderr.write("elicitation-state.sh: internal error — unhandled subcommand '%s'\n" % subcommand)
|
|
432
|
+
sys.exit(1)
|
|
433
|
+
|
|
434
|
+
print(json.dumps(result, indent=2))
|
|
435
|
+
sys.exit(exit_code)
|
|
436
|
+
PYEOF
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
set +e
|
|
440
|
+
# NOTE: unlike `bash -c`, `python3 -c CODE arg1 arg2...` sets sys.argv[0] to
|
|
441
|
+
# the literal string "-c" (not the first following argument) — there is no
|
|
442
|
+
# python3 equivalent of bash's "$0 placeholder" convention. sys.argv[1:9]
|
|
443
|
+
# below therefore lines up directly with the positional arguments given here,
|
|
444
|
+
# with no placeholder needed.
|
|
445
|
+
"$WITH_LOCK" "$STATE_FILE" -- python3 -c "$UPDATE_SCRIPT" \
|
|
446
|
+
"$STATE_FILE" "$SUBCOMMAND" "$ID" "$TARGET" "$CAP" "$NODE" "$NOTE" "$CHECKPOINT_JSON"
|
|
447
|
+
STATUS=$?
|
|
448
|
+
set -e
|
|
449
|
+
|
|
450
|
+
if [ "$STATUS" -eq 2 ]; then
|
|
451
|
+
# with-lock.sh's own "could not acquire the lock" exit code — remap to
|
|
452
|
+
# this script's write-failure code so callers have one code (4), not two,
|
|
453
|
+
# to check for "the mutation did not happen".
|
|
454
|
+
exit 4
|
|
455
|
+
fi
|
|
456
|
+
|
|
457
|
+
exit "$STATUS"
|
|
@@ -254,6 +254,62 @@ construction**. Agents and humans reading the board should not treat a backfille
|
|
|
254
254
|
Definition of Done as a build plan, and should not assume that an incomplete-looking backfilled
|
|
255
255
|
epic represents unbuilt functionality.
|
|
256
256
|
|
|
257
|
+
## Board Item Tag Conventions
|
|
258
|
+
|
|
259
|
+
Two bracketed title-tag conventions mark board items whose nature differs from ordinary delivery
|
|
260
|
+
work: `[SPIKE]` (bounded research) and `[ARCH]` (durable architectural inventory). Both are
|
|
261
|
+
**title-text conventions, not frontmatter fields** — there is no `tag:` key; the tag is written
|
|
262
|
+
directly into the item's `title` (e.g. `title: "[SPIKE] Security Section"`) and is not validated or
|
|
263
|
+
enforced by `scripts/validate-board.sh`, which treats `title` as free text. Using either tag is
|
|
264
|
+
advisory: it signals intent to readers of the board, `/status` output, and rollup logic, but nothing
|
|
265
|
+
currently gates on its presence.
|
|
266
|
+
|
|
267
|
+
### `[SPIKE]` — Bounded Research
|
|
268
|
+
|
|
269
|
+
**Scope:** story and task level only.
|
|
270
|
+
|
|
271
|
+
**Meaning:** a time-boxed research or exploration effort whose output is a decision, a design note,
|
|
272
|
+
or an answered question — **not** shippable implementation code. Existing usage (`E06_S03_spike-editable-board.md`,
|
|
273
|
+
`E08_S02_spike-security-section.md`) follows a consistent shape:
|
|
274
|
+
|
|
275
|
+
- Bracketed title: `title: "[SPIKE] <Topic>"`.
|
|
276
|
+
- A `## Spike Questions to Answer` section (or equivalent) in place of, or alongside, ordinary
|
|
277
|
+
Acceptance Criteria.
|
|
278
|
+
- A Definition of Done line stating that no implementation code is produced by the spike itself —
|
|
279
|
+
only findings, a design note, or a recommendation that a follow-up story/task will act on.
|
|
280
|
+
|
|
281
|
+
`[SPIKE]` predates this document; this section formalizes an existing informal convention rather
|
|
282
|
+
than introducing new behavior.
|
|
283
|
+
|
|
284
|
+
### `[ARCH]` — Durable Architectural Inventory
|
|
285
|
+
|
|
286
|
+
**Scope:** epic, story, and task level. This is the **first tag extended to epic level** — `[SPIKE]`
|
|
287
|
+
has never applied above story/task.
|
|
288
|
+
|
|
289
|
+
**Meaning:** durable architectural-inventory record-keeping — capturing how existing or
|
|
290
|
+
newly-understood code is structured, so the record persists as a lasting reference — as distinct
|
|
291
|
+
from `[SPIKE]`'s bounded, time-boxed research meaning. `[ARCH]`-tagged items are not "temporary
|
|
292
|
+
until answered" the way a spike is; they are the durable output itself (e.g. graph nodes/edges,
|
|
293
|
+
architecture documentation) and are not expected to be superseded by a subsequent non-`[ARCH]` item
|
|
294
|
+
the way a spike's findings feed into a normal follow-up.
|
|
295
|
+
|
|
296
|
+
Introduced for E20_S08's conversational architecture elicitation flow (`/uncharted` integration),
|
|
297
|
+
where generated board items record architectural understanding of code rather than proposing new
|
|
298
|
+
delivery work, at a scale (potentially a whole investigated subsystem) that can reach epic level.
|
|
299
|
+
|
|
300
|
+
**Explicitly distinct from `[SPIKE]`:**
|
|
301
|
+
|
|
302
|
+
| | `[SPIKE]` | `[ARCH]` |
|
|
303
|
+
|---|---|---|
|
|
304
|
+
| Nature | Bounded, time-boxed research | Durable architectural record |
|
|
305
|
+
| Valid levels | Story, task | Epic, story, task |
|
|
306
|
+
| Typical DoD | "No implementation code produced" | Graph nodes/edges written, or architecture documented |
|
|
307
|
+
| Lifecycle | Findings feed a follow-up item | The record itself is the lasting artifact |
|
|
308
|
+
|
|
309
|
+
Do not use `[ARCH]` and `[SPIKE]` interchangeably or on the same item — pick whichever meaning
|
|
310
|
+
actually applies. An epic can only ever be `[ARCH]` (or untagged); `[SPIKE]` is not valid at epic
|
|
311
|
+
level.
|
|
312
|
+
|
|
257
313
|
## Execution Scope Fields (Task)
|
|
258
314
|
|
|
259
315
|
These six fields control the execution footprint of a task within the `/jenga` and `/do` workflows. They are **optional** — omitting all six is valid and equivalent to `execution_scope: task` / `needs_docs: true`.
|
|
@@ -429,6 +485,7 @@ A `crucial_escalation` rapport must also name the target item's ID (`E##`, `E##_
|
|
|
429
485
|
| `rapport_review` | on_session_end.sh | New problem rapport(s) detected; create backlog items or mark Failed |
|
|
430
486
|
| `status_review` | on_session_end.sh | Session ended; review board for stale statuses |
|
|
431
487
|
| `story_rollup` | tester / on_session_end.sh | All tasks under story complete; check rollup |
|
|
488
|
+
| `elicitation_resume` | on_session_end.sh (from a scrum-master `elicitation_paused` handoff) | A `/uncharted` conversational architecture elicitation (E20_S08_T03) paused mid-run; resume it from the state file persisted by `skills/uncharted/scripts/elicitation-state.sh` |
|
|
432
489
|
|
|
433
490
|
### `developer_triggers.jsonl` — processed by developer at session start
|
|
434
491
|
|
|
@@ -15,23 +15,35 @@ This project uses **Jenga** — a skill-based AI agent framework. Jenga organise
|
|
|
15
15
|
|
|
16
16
|
### Skill Routing
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
If you are Claude Code, `/skill-name` is a native harness-level mechanism: the harness itself
|
|
19
|
+
intercepts the literal command and loads the skill for you, independent of anything written here. If
|
|
20
|
+
you are any other agent (Codex, or a generic `AGENTS.md` consumer) with no equivalent native
|
|
21
|
+
interception, you depend entirely on the instructions below to know what "invoking a skill" concretely
|
|
22
|
+
means. Do not improvise a plausible-sounding response instead of following these steps — that is the
|
|
23
|
+
exact failure this section exists to prevent.
|
|
24
|
+
|
|
25
|
+
When the user's message is or matches `/skill-name` (or otherwise clearly matches a known skill's
|
|
26
|
+
keyword or intent):
|
|
27
|
+
|
|
28
|
+
1. Locate the target file at `{{SKILL_DISCOVERY_PATH}}<skill-name>/SKILL.md` (the discovery path from
|
|
29
|
+
"How Jenga Works" above).
|
|
30
|
+
2. Open and read that file **in full** before doing anything else.
|
|
31
|
+
3. Execute its instructions exactly as written, for the rest of this turn — including running any
|
|
32
|
+
shell scripts or commands it references (e.g. via a terminal/shell tool).
|
|
33
|
+
4. Do not substitute your own judgment about what "running the skill" should look like, and do not
|
|
34
|
+
answer with free-form prose describing what the skill would do — the `SKILL.md` file's contents
|
|
35
|
+
are the authoritative procedure, not a summary or a suggestion.
|
|
36
|
+
|
|
37
|
+
For free-form questions (architecture, code review, debugging, general Q&A) that do not match a skill,
|
|
38
|
+
answer directly using your full capabilities.
|
|
27
39
|
|
|
28
40
|
#### Routing decision table
|
|
29
41
|
|
|
30
42
|
| Situation | Action |
|
|
31
43
|
|-----------|--------|
|
|
32
|
-
| Message matches a skill keyword or intent |
|
|
44
|
+
| Message matches a skill keyword or intent | Open `{{SKILL_DISCOVERY_PATH}}<skill-name>/SKILL.md`, read it fully, execute it as written |
|
|
33
45
|
| Message is a general coding or project question | Answer directly |
|
|
34
|
-
| Ambiguous — could be skill or free-form | Prefer the skill;
|
|
46
|
+
| Ambiguous — could be skill or free-form | Prefer the skill; open and execute its `SKILL.md` rather than describing it |
|
|
35
47
|
|
|
36
48
|
### Available Skills
|
|
37
49
|
|
|
@@ -15,23 +15,33 @@ This project uses **Jenga** — a skill-based AI agent framework. Jenga organise
|
|
|
15
15
|
|
|
16
16
|
### Skill Routing
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
Unlike Claude Code, GitHub Copilot has **no native slash-command interception** — typing `/skill-name`
|
|
19
|
+
does not automatically load or run anything on its own. Copilot depends entirely on the instructions
|
|
20
|
+
below to know what "invoking a skill" concretely means. Do not improvise a plausible-sounding response
|
|
21
|
+
instead of following these steps — that is the exact failure this section exists to prevent.
|
|
22
|
+
|
|
23
|
+
When the user's message is or matches `/skill-name` (or otherwise clearly matches a known skill's
|
|
24
|
+
keyword or intent):
|
|
25
|
+
|
|
26
|
+
1. Locate the target file at `.agents/skills/<skill-name>/SKILL.md` (the discovery path from "How
|
|
27
|
+
Jenga Works" above).
|
|
28
|
+
2. Open and read that file **in full** before doing anything else.
|
|
29
|
+
3. Execute its instructions exactly as written, for the rest of this turn — including running any
|
|
30
|
+
shell scripts or commands it references (e.g. via a terminal/shell tool).
|
|
31
|
+
4. Do not substitute your own judgment about what "running the skill" should look like, and do not
|
|
32
|
+
answer with free-form prose describing what the skill would do — the `SKILL.md` file's contents
|
|
33
|
+
are the authoritative procedure, not a summary or a suggestion.
|
|
34
|
+
|
|
35
|
+
For free-form questions (architecture, code review, debugging, general Q&A) that do not match a skill,
|
|
36
|
+
answer directly using your full capabilities.
|
|
27
37
|
|
|
28
38
|
#### Routing decision table
|
|
29
39
|
|
|
30
40
|
| Situation | Action |
|
|
31
41
|
|-----------|--------|
|
|
32
|
-
| Message matches a skill keyword or intent |
|
|
42
|
+
| Message matches a skill keyword or intent | Open `.agents/skills/<skill-name>/SKILL.md`, read it fully, execute it as written |
|
|
33
43
|
| Message is a general coding or project question | Answer directly |
|
|
34
|
-
| Ambiguous — could be skill or free-form | Prefer the skill;
|
|
44
|
+
| Ambiguous — could be skill or free-form | Prefer the skill; open and execute its `SKILL.md` rather than describing it |
|
|
35
45
|
|
|
36
46
|
### Available Skills
|
|
37
47
|
|
package/mcp/router/README.md
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# jenga-router
|
|
2
|
-
|
|
3
|
-
A stdio MCP server that routes incoming prompts to the appropriate Jenga skill.
|
|
4
|
-
|
|
5
|
-
## Usage
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
node mcp/router/index.js
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Tools
|
|
12
|
-
|
|
13
|
-
| Tool | Description |
|
|
14
|
-
|------|-------------|
|
|
15
|
-
| `ping` | Health check — returns `{ ok: true, uptime: <ms> }` |
|
|
16
|
-
|
|
17
|
-
## Protocol
|
|
18
|
-
|
|
19
|
-
Communicates over stdin/stdout using the Model Context Protocol (MCP) with `StdioServerTransport`.
|
package/mcp/router/embedder.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { pipeline } from "@huggingface/transformers";
|
|
2
|
-
|
|
3
|
-
let _pipe = null;
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Loads the feature-extraction pipeline for Xenova/all-MiniLM-L6-v2.
|
|
7
|
-
* Safe to call multiple times — no-op if already loaded.
|
|
8
|
-
*/
|
|
9
|
-
export async function warmUp() {
|
|
10
|
-
if (_pipe) return;
|
|
11
|
-
const t = Date.now();
|
|
12
|
-
_pipe = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2");
|
|
13
|
-
process.stderr.write(`[jenga-router] Model warmed up in ${Date.now() - t}ms\n`);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Embeds text using the loaded pipeline.
|
|
18
|
-
* Returns a Float32Array of length 384.
|
|
19
|
-
*/
|
|
20
|
-
export async function embed(text) {
|
|
21
|
-
const output = await _pipe(text, { pooling: "mean", normalize: true });
|
|
22
|
-
return output.data;
|
|
23
|
-
}
|