@jenga-ai/agent 1.2.4 → 2.0.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 +97 -91
- package/agents/developer.md +26 -7
- package/agents/scrum-master.md +57 -22
- package/agents/tester.md +68 -4
- package/hooks/on_session_end.sh +40 -1
- package/lib/generate-agent-context.js +18 -1
- package/lib/generate-copilot-instructions.js +18 -1
- package/lib/generate-skill-allow-list.js +191 -0
- package/lib/skill-allow-list.json +43 -0
- package/package.json +35 -20
- package/scripts/apply-j-prefix.sh +230 -0
- package/scripts/consume-context-digest.sh +103 -0
- package/scripts/postinstall.js +25 -0
- package/scripts/sweep-stale-context-digests.sh +132 -0
- package/scripts/validate-board.sh +5 -0
- package/scripts/write-context-digest.sh +230 -0
- package/skills/brainstorm/SKILL.md +1 -1
- package/skills/btw/SKILL.md +1 -1
- package/skills/clearify/SKILL.md +1 -1
- package/skills/close-story/SKILL.md +78 -6
- package/skills/close-story/scripts/check-privatized.sh +345 -0
- package/skills/commit/SKILL.md +12 -2
- package/skills/continue/SKILL.md +1 -1
- package/skills/deep-dive/SKILL.md +1 -1
- package/skills/dev-done/SKILL.md +46 -0
- package/skills/dev-done/scripts/classify-commit-outcome.sh +114 -0
- package/skills/distribute/SKILL.md +1 -1
- package/skills/do/SKILL.md +100 -10
- package/skills/doc/README.md +155 -0
- package/skills/doc/SKILL.md +43 -13
- package/skills/doc/authoring-notes.md +72 -0
- package/skills/doc/scripts/resolve_last_update.py +149 -0
- package/skills/doc-sync/SKILL.md +1 -1
- package/skills/dooo/SKILL.md +1 -1
- package/skills/error/SKILL.md +1 -1
- package/skills/evaluate/SKILL.md +1 -1
- package/skills/examplify/SKILL.md +1 -1
- package/skills/help/SKILL.md +1 -1
- package/skills/idea/SKILL.md +1 -1
- package/skills/improve/SKILL.md +1 -1
- package/skills/init/SKILL.md +8 -7
- package/skills/init/assets/scope-thresholds_template.json +7 -0
- package/skills/init/scripts/init.sh +6 -0
- package/skills/j-init/SKILL.md +168 -0
- package/skills/j-init/assets/.gitignore_template +15 -0
- package/skills/j-init/assets/PROJECT_SUMMARY_template.md +13 -0
- package/skills/j-init/assets/directory_structure.txt +14 -0
- package/skills/j-init/assets/scope-thresholds_template.json +7 -0
- package/skills/j-init/assets/strategy_stub_template.md +38 -0
- package/skills/j-init/assets/test-config_template.json +4 -0
- package/skills/j-init/assets/workflow_template.json +30 -0
- package/skills/j-init/scripts/apply-project-visibility.sh +176 -0
- package/skills/j-init/scripts/detect-existing-codebase.sh +166 -0
- package/skills/j-init/scripts/init.sh +116 -0
- package/skills/jbp/SKILL.md +1 -1
- package/skills/jenga/SKILL.md +1 -1
- package/skills/jenga/scripts/render-confirmation.sh +55 -18
- package/skills/jenga-permission-level/SKILL.md +1 -1
- package/skills/lgtm/SKILL.md +1 -1
- package/skills/pi-plan/SKILL.md +1 -1
- package/skills/proceed/SKILL.md +1 -1
- package/skills/publish/SKILL.md +67 -1
- package/skills/publish/adapters/npm-ci.md +60 -4
- 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 +50 -1
- 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 +122 -12
- package/skills/reconcile/assets/report_format.md +17 -0
- package/skills/reconcile/scripts/resolve-reconcile-scope.sh +489 -0
- package/skills/reconcile-origin/SKILL.md +1 -1
- package/skills/redo/SKILL.md +1 -1
- package/skills/skillify/SKILL.md +1 -1
- package/skills/spinoff/SKILL.md +1 -1
- package/skills/status/SKILL.md +1 -1
- package/skills/todo/SKILL.md +40 -3
- package/skills/todo/scripts/add_trivial_task.sh +216 -0
- package/skills/todo/scripts/update_story_tasks.py +87 -0
- package/skills/uncharted/SKILL.md +201 -22
- package/skills/uncharted/scripts/directory-triage.sh +342 -0
- package/skills/uncharted/scripts/elicitation-state.sh +457 -0
- package/skills/wtf/SKILL.md +1 -1
- package/templates/SCRUM_BOARD_SCHEMA.md +90 -2
- package/templates/agent-context.md.tpl +47 -12
- package/templates/copilot-instructions.md.tpl +36 -9
- 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"
|
package/skills/wtf/SKILL.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: wtf
|
|
2
|
+
name: j:wtf
|
|
3
3
|
description: Alias of /clearify — clarifies ambiguous, dense, or under-specified prompts and conversation on request. This folder exists only so the `/wtf` slash command resolves to a skill; behaviour is identical to `/clearify`.
|
|
4
4
|
keywords:
|
|
5
5
|
- wtf
|
|
@@ -67,9 +67,28 @@ All status fields must use one of the following exact strings:
|
|
|
67
67
|
| `Blocked` | Cannot proceed; human intervention required |
|
|
68
68
|
| `Backlog` | Epic-level only; queued but not yet prioritized for work |
|
|
69
69
|
| `Done` | Epic-level only; all child stories/tasks closed out |
|
|
70
|
+
| `Merged` | Set after a successful `/self-sync` run's file diff shows the ticket's recorded files were touched |
|
|
71
|
+
| `Publicized` | Set after a successful `/mirror-public` run's file diff shows the ticket's recorded files were touched |
|
|
72
|
+
| `Privatized` | Set at ticket-close time via a static `.publicignore` blocklist membership check (no run dependency) |
|
|
73
|
+
| `Deployed to Stage` | Set when the public `jenga-npm` repo's CI tags a `vX.Y.Z-stage` tag that resolves back (via the `Source-Commit:` trailer) to this ticket's commit |
|
|
74
|
+
| `Deployed to Prod` | Set when the public `jenga-npm` repo's CI tags a `vX.Y.Z` (prod) tag that resolves back to this ticket's commit |
|
|
70
75
|
|
|
71
76
|
Only the **tester agent** may write status values to story and task files. Only the **scrum master** may write status values to epic files and may update story status as part of rollup.
|
|
72
77
|
|
|
78
|
+
All five statuses above are **script-set, never agent-judged** — no agent decides when a ticket becomes `Merged`, `Publicized`, `Privatized`, `Deployed to Stage`, or `Deployed to Prod`; a deterministic script observation sets them, per the mechanisms described below.
|
|
79
|
+
|
|
80
|
+
### Static vs. Reactive Status Setting
|
|
81
|
+
|
|
82
|
+
`Privatized` is set **statically**: at ticket-close time, a script checks whether the ticket's recorded files match the `.publicignore` blocklist. This check has no dependency on any particular run having occurred — it is a pure membership test.
|
|
83
|
+
|
|
84
|
+
The other four — `Merged`, `Publicized`, `Deployed to Stage`, `Deployed to Prod` — are set **reactively**: a script observes the outcome of a specific run (a `/self-sync` or `/mirror-public` file diff, or a public-repo CI tag event) and sets the status only when that run's evidence confirms the ticket was affected. Absent a qualifying run, the status is not set.
|
|
85
|
+
|
|
86
|
+
### Publicized / Privatized / Deployed Lifecycle Relationship
|
|
87
|
+
|
|
88
|
+
`Publicized` and `Privatized` are **mutually exclusive** — a ticket is one or the other, never both. A ticket's files either pass the `.publicignore` blocklist check (making it eligible for `Publicized`) or match it (making it `Privatized`); it cannot satisfy both conditions at once.
|
|
89
|
+
|
|
90
|
+
Only **`Publicized`** tickets are eligible to progress further down the deploy lifecycle, from `Deployed to Stage` to `Deployed to Prod`. A `Privatized` ticket's files never reach the public `jenga-npm` repo, so it can never acquire a CI tag there and therefore can never reach either deploy status.
|
|
91
|
+
|
|
73
92
|
---
|
|
74
93
|
|
|
75
94
|
## File Formats
|
|
@@ -161,7 +180,7 @@ reopened_on: # comma-separated list, e.g. 2026-02-01, 2026-04-10
|
|
|
161
180
|
reopened_reason: # comma-separated list, e.g. "Scope expanded", "Bug found post-release"
|
|
162
181
|
assigned_to: developer | tester | scrum-master
|
|
163
182
|
docs: [] # optional list of repo-relative documentation paths, e.g. ["README.md", "docs/API.md"]
|
|
164
|
-
execution_scope: task # task | story | epic | inline; omit for legacy tasks (defaults to task)
|
|
183
|
+
execution_scope: task # task | story | epic | inline | light; omit for legacy tasks (defaults to task)
|
|
165
184
|
needs_docs: true # boolean; omit for legacy tasks (defaults to true)
|
|
166
185
|
scope_rationale: "" # required when execution_scope is set; must contain a numeric/file-count claim
|
|
167
186
|
jenga_assigned: true # boolean; true = machine-assigned, false = human override
|
|
@@ -254,18 +273,75 @@ construction**. Agents and humans reading the board should not treat a backfille
|
|
|
254
273
|
Definition of Done as a build plan, and should not assume that an incomplete-looking backfilled
|
|
255
274
|
epic represents unbuilt functionality.
|
|
256
275
|
|
|
276
|
+
## Board Item Tag Conventions
|
|
277
|
+
|
|
278
|
+
Two bracketed title-tag conventions mark board items whose nature differs from ordinary delivery
|
|
279
|
+
work: `[SPIKE]` (bounded research) and `[ARCH]` (durable architectural inventory). Both are
|
|
280
|
+
**title-text conventions, not frontmatter fields** — there is no `tag:` key; the tag is written
|
|
281
|
+
directly into the item's `title` (e.g. `title: "[SPIKE] Security Section"`) and is not validated or
|
|
282
|
+
enforced by `scripts/validate-board.sh`, which treats `title` as free text. Using either tag is
|
|
283
|
+
advisory: it signals intent to readers of the board, `/status` output, and rollup logic, but nothing
|
|
284
|
+
currently gates on its presence.
|
|
285
|
+
|
|
286
|
+
### `[SPIKE]` — Bounded Research
|
|
287
|
+
|
|
288
|
+
**Scope:** story and task level only.
|
|
289
|
+
|
|
290
|
+
**Meaning:** a time-boxed research or exploration effort whose output is a decision, a design note,
|
|
291
|
+
or an answered question — **not** shippable implementation code. Existing usage (`E06_S03_spike-editable-board.md`,
|
|
292
|
+
`E08_S02_spike-security-section.md`) follows a consistent shape:
|
|
293
|
+
|
|
294
|
+
- Bracketed title: `title: "[SPIKE] <Topic>"`.
|
|
295
|
+
- A `## Spike Questions to Answer` section (or equivalent) in place of, or alongside, ordinary
|
|
296
|
+
Acceptance Criteria.
|
|
297
|
+
- A Definition of Done line stating that no implementation code is produced by the spike itself —
|
|
298
|
+
only findings, a design note, or a recommendation that a follow-up story/task will act on.
|
|
299
|
+
|
|
300
|
+
`[SPIKE]` predates this document; this section formalizes an existing informal convention rather
|
|
301
|
+
than introducing new behavior.
|
|
302
|
+
|
|
303
|
+
### `[ARCH]` — Durable Architectural Inventory
|
|
304
|
+
|
|
305
|
+
**Scope:** epic, story, and task level. This is the **first tag extended to epic level** — `[SPIKE]`
|
|
306
|
+
has never applied above story/task.
|
|
307
|
+
|
|
308
|
+
**Meaning:** durable architectural-inventory record-keeping — capturing how existing or
|
|
309
|
+
newly-understood code is structured, so the record persists as a lasting reference — as distinct
|
|
310
|
+
from `[SPIKE]`'s bounded, time-boxed research meaning. `[ARCH]`-tagged items are not "temporary
|
|
311
|
+
until answered" the way a spike is; they are the durable output itself (e.g. graph nodes/edges,
|
|
312
|
+
architecture documentation) and are not expected to be superseded by a subsequent non-`[ARCH]` item
|
|
313
|
+
the way a spike's findings feed into a normal follow-up.
|
|
314
|
+
|
|
315
|
+
Introduced for E20_S08's conversational architecture elicitation flow (`/uncharted` integration),
|
|
316
|
+
where generated board items record architectural understanding of code rather than proposing new
|
|
317
|
+
delivery work, at a scale (potentially a whole investigated subsystem) that can reach epic level.
|
|
318
|
+
|
|
319
|
+
**Explicitly distinct from `[SPIKE]`:**
|
|
320
|
+
|
|
321
|
+
| | `[SPIKE]` | `[ARCH]` |
|
|
322
|
+
|---|---|---|
|
|
323
|
+
| Nature | Bounded, time-boxed research | Durable architectural record |
|
|
324
|
+
| Valid levels | Story, task | Epic, story, task |
|
|
325
|
+
| Typical DoD | "No implementation code produced" | Graph nodes/edges written, or architecture documented |
|
|
326
|
+
| Lifecycle | Findings feed a follow-up item | The record itself is the lasting artifact |
|
|
327
|
+
|
|
328
|
+
Do not use `[ARCH]` and `[SPIKE]` interchangeably or on the same item — pick whichever meaning
|
|
329
|
+
actually applies. An epic can only ever be `[ARCH]` (or untagged); `[SPIKE]` is not valid at epic
|
|
330
|
+
level.
|
|
331
|
+
|
|
257
332
|
## Execution Scope Fields (Task)
|
|
258
333
|
|
|
259
334
|
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`.
|
|
260
335
|
|
|
261
336
|
**`execution_scope`**
|
|
262
|
-
- Valid values: `task` | `story` | `epic` | `inline`
|
|
337
|
+
- Valid values: `task` | `story` | `epic` | `inline` | `light`
|
|
263
338
|
- When required: optional; omit for legacy tasks (runtime default: `task`)
|
|
264
339
|
- Description: defines how broadly this task's implementation touches the codebase.
|
|
265
340
|
- `task` — standard single-task scope (default)
|
|
266
341
|
- `story` — task may touch files across multiple tasks in the same story
|
|
267
342
|
- `epic` — task may touch files across stories; requires `epic_scope_approval: true` on the parent epic
|
|
268
343
|
- `inline` — trivial change (e.g. config tweak, comment, schema doc); no execution plan or summary document is needed
|
|
344
|
+
- `light` — sits between `inline` and `task` in scope: a single developer subagent pass with no worktree, self-verified via `scripts/smoke-harness.sh` in lieu of a separate tester invocation; if the smoke harness fails, execution falls back to `task` scope
|
|
269
345
|
|
|
270
346
|
**`needs_docs`**
|
|
271
347
|
- Valid values: `true` | `false`
|
|
@@ -429,6 +505,7 @@ A `crucial_escalation` rapport must also name the target item's ID (`E##`, `E##_
|
|
|
429
505
|
| `rapport_review` | on_session_end.sh | New problem rapport(s) detected; create backlog items or mark Failed |
|
|
430
506
|
| `status_review` | on_session_end.sh | Session ended; review board for stale statuses |
|
|
431
507
|
| `story_rollup` | tester / on_session_end.sh | All tasks under story complete; check rollup |
|
|
508
|
+
| `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
509
|
|
|
433
510
|
### `developer_triggers.jsonl` — processed by developer at session start
|
|
434
511
|
|
|
@@ -471,6 +548,7 @@ Each file is written by an agent as the **last action** of its session, and is s
|
|
|
471
548
|
| `worktree` | developer, tester | Absolute path |
|
|
472
549
|
| `paths` | developer, tester | Commit SHAs |
|
|
473
550
|
| `rapport_file`| tester only | Path to rapport if status is failed/error |
|
|
551
|
+
| `resolved_context` | all, optional | Digest of context the sending agent already resolved; see below |
|
|
474
552
|
| `date` | all | ISO 8601 UTC |
|
|
475
553
|
|
|
476
554
|
**Status values per agent:**
|
|
@@ -478,6 +556,16 @@ Each file is written by an agent as the **last action** of its session, and is s
|
|
|
478
556
|
- `developer`: `implementation_complete`
|
|
479
557
|
- `tester`: `passed`, `passed_with_remarks`, `failed`, `error`
|
|
480
558
|
|
|
559
|
+
**`resolved_context` — digest, not a dump (E49).** An optional field a sending agent populates with a short digest of conclusions it already reached while navigating source documents (e.g. which schema fields apply, which skill precedent governs, which decisions are already made) — so the receiving subagent doesn't have to cold-re-read the same files from scratch. It must stay under a size cap of roughly 100 lines (a few hundred tokens), mirroring `scope_rationale`'s "must contain a measurable claim" discipline: a `resolved_context` value that is a raw file dump or exceeds the cap is not valid. The digest is a starting point only — it never restricts the receiving agent from reading full source files when the digest is insufficient or needs verification. The digest body itself lives in a per-task file at `project/queue/context/<agent>-<session_id>-<task_id>.json`, following the same unique-path, single-use, session-scoped convention as `handoffs/` above (not a shared, clobber-prone slot); this handoff's `resolved_context` field holds a reference to (or the inline content of) that file.
|
|
560
|
+
|
|
561
|
+
**`project/queue/context/` — physical digest files (E49_S01_T02).** The directory itself is kept via `.gitkeep`; individual digest files (`*.json`) are git-ignored for the same reason `handoffs/*.json` is — a committed one can no longer be told apart from a live pending digest by inspection alone. Three scripts implement the convention end to end:
|
|
562
|
+
|
|
563
|
+
- `scripts/write-context-digest.sh` — the sending agent's write path. Takes `--agent`, `--session-id`, `--task-id`, and digest content (`--content`, `--content-file`, or stdin); enforces the ~100-line cap above by **rejecting** (not truncating) an oversized digest, since a silently-truncated digest could cut off mid-thought and mislead the receiver — the sender is the only party that actually knows what's safe to cut. Writes atomically (tmp file in the same directory, then `mv`) and prints the resulting absolute path to stdout for the caller to place in the handoff's `resolved_context` field.
|
|
564
|
+
- `scripts/consume-context-digest.sh <path>` — the receiving agent's read path. Atomically claims the file (rename to a `.claimed.$$` sibling, same TOCTOU-safe pattern `on_session_end.sh` section 4 uses for `handoffs/`), prints its content (full JSON envelope, or just the `digest` field with `--raw`), and deletes it — single-use, like `handoffs/`.
|
|
565
|
+
- `scripts/sweep-stale-context-digests.sh` — an age-based backstop (default 24h, overridable), invoked from `hooks/on_session_end.sh` on every session end regardless of agent, for a digest whose intended receiver never calls the consume script (abandoned dispatch, or a receiver that read the raw file directly and forgot to clean up). Age-based rather than routed-and-deleted-immediately like `handoffs/`, because a digest's consumer is a later session that may not have started yet when some unrelated session's `SessionEnd` hook fires.
|
|
566
|
+
|
|
567
|
+
Populating `resolved_context` when dispatching (scrum-master → developer, developer → tester) is sibling task E49_S01_T03 — not yet wired into `agents/scrum-master.md` or `agents/developer.md` as of this writing. The physical convention above is usable standalone in the meantime.
|
|
568
|
+
|
|
481
569
|
|
|
482
570
|
|
|
483
571
|
Located at `project/configs/workflow.json`. Scaffolded by `/init` and owned by the scrum master.
|