@jenga-ai/agent 1.0.1 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -7
- package/agents/developer.md +82 -2
- package/agents/scrum-master.md +215 -21
- package/agents/tester.md +90 -8
- package/hooks/on_session_end.sh +171 -20
- package/mcp/router/embedder.js +1 -1
- package/mcp/training_runner/index.js +239 -0
- package/mcp/training_runner/package-lock.json +1065 -0
- package/mcp/training_runner/package.json +15 -0
- package/package.json +14 -16
- package/scripts/check-permission-level.sh +107 -0
- package/scripts/check-publicignore-match.sh +122 -0
- package/scripts/check-worktree-liveness.sh +193 -0
- package/scripts/generate-rapport-manifest.sh +43 -0
- package/scripts/idea_manager.sh +47 -0
- package/scripts/install-worktree-commit-guard.sh +134 -0
- package/scripts/jenga-permission-level-switch.sh +109 -0
- package/scripts/smoke-harness.sh +139 -0
- package/scripts/validate-board.sh +62 -0
- package/scripts/with-lock.sh +158 -0
- package/scripts/worktree-remove-guard.sh +204 -0
- package/skills/clearify/SKILL.md +52 -0
- package/skills/close-story/SKILL.md +203 -0
- package/skills/close-story/scripts/check-story-closeable.sh +195 -0
- package/skills/close-story/scripts/compute-scope-divergence.sh +128 -0
- package/skills/close-story/scripts/extract-diff-stats.sh +48 -0
- package/skills/close-story/scripts/extract-task-diff-stats.sh +97 -0
- package/skills/close-story/scripts/update-task-frontmatter.sh +103 -0
- package/skills/commit/SKILL.md +30 -3
- package/skills/distribute/CONFIG_SCHEMA.md +148 -0
- package/skills/distribute/SKILL.md +173 -0
- package/skills/distribute/scripts/check-version.sh +74 -0
- package/skills/distribute/scripts/commit-version-bump.sh +108 -0
- package/skills/distribute/scripts/distribute-changes.sh +381 -0
- package/skills/do/SKILL.md +352 -1
- package/skills/do/assets/intent-vs-diff-prompt.md +69 -0
- package/skills/doc/assets/path-objectives.yaml +13 -0
- package/skills/doc-sync/SKILL.md +16 -0
- package/skills/doc-sync/assets/doc_targets.md +11 -0
- package/skills/idea/SKILL.md +56 -0
- package/skills/idea/assets/idea_handoff_template.md +26 -0
- package/skills/idea/assets/idea_template.md +3 -0
- package/skills/init/SKILL.md +101 -7
- package/skills/init/assets/directory_structure.txt +1 -0
- package/skills/init/assets/strategy_stub_template.md +38 -0
- package/skills/init/assets/workflow_template.json +1 -1
- package/skills/init/scripts/apply-project-visibility.sh +176 -0
- package/skills/init/scripts/detect-existing-codebase.sh +166 -0
- package/skills/init/scripts/init.sh +35 -1
- package/skills/jenga/SKILL.md +206 -14
- package/skills/jenga/scripts/board-scan.sh +238 -0
- package/skills/jenga/scripts/cascade-resolve.sh +297 -0
- package/skills/jenga/scripts/render-confirmation.sh +679 -0
- package/skills/jenga/scripts/render-picker.sh +439 -0
- package/skills/jenga/scripts/resolve-id.sh +367 -0
- package/skills/jenga-permission-level/SKILL.md +81 -0
- package/skills/proceed/SKILL.md +1 -1
- package/skills/publish/SKILL.md +8 -5
- package/skills/publish/assets/ci-contract.md +2 -2
- package/skills/publish/assets/ownership-matrix.md +1 -1
- package/skills/publish/scripts/finalize_changelog.sh +115 -0
- package/skills/publish/scripts/generate_release_notes.sh +475 -28
- package/skills/publish/scripts/npm_ci_pipeline.sh +44 -6
- package/skills/publish/scripts/publish_deploy.sh +38 -8
- package/skills/publish/scripts/run_gates.sh +2 -2
- package/skills/reconcile/SKILL.md +117 -5
- package/skills/reconcile/scripts/detect-unlinked-code.sh +741 -0
- package/skills/skillify/assets/init-new/assets/directory_structure.txt +5 -1
- package/skills/spinoff/SKILL.md +12 -7
- package/skills/todo/SKILL.md +2 -0
- package/skills/uncharted/SKILL.md +711 -0
- package/skills/uncharted/assets/SEGMENT_PROPOSAL_TEMPLATE.md +129 -0
- package/skills/uncharted/assets/UNDERSTANDING_DOC_TEMPLATE.md +160 -0
- package/skills/uncharted/scripts/apply-subsystem-cap.sh +573 -0
- package/skills/uncharted/scripts/detect-dependencies.sh +732 -0
- package/skills/uncharted/scripts/detect-tests.sh +553 -0
- package/skills/uncharted/scripts/discover-subsystems.sh +1029 -0
- package/skills/uncharted/scripts/enumerate-target.sh +470 -0
- package/skills/uncharted/scripts/import-source.sh +517 -0
- package/skills/uncharted/scripts/inspect-provenance.sh +573 -0
- package/skills/uncharted/scripts/resolve-segment-target.sh +640 -0
- package/skills/uncharted/scripts/run-engine.sh +655 -0
- package/skills/uncharted/scripts/validate-proposed-items.sh +125 -0
- package/skills/uncharted/scripts/write-backfilled-epics.sh +498 -0
- package/skills/wtf/SKILL.md +20 -0
- package/templates/CHANGELOG_TEMPLATE.md +13 -0
- package/templates/PROBLEM_RAPPORT_TEMPLATE.md +4 -1
- package/templates/SCRUM_BOARD_SCHEMA.md +206 -10
- package/templates/permission-levels/README.md +73 -0
- package/templates/permission-levels/level-1-locked.json +71 -0
- package/templates/permission-levels/level-2-guarded.json +64 -0
- package/templates/permission-levels/level-3-standard.json +62 -0
- package/templates/permission-levels/level-4-elevated.json +60 -0
- package/templates/permission-levels/level-5-unrestricted.json +58 -0
- package/skills/convert/SKILL.md +0 -124
- package/skills/convert/convert_cli.py +0 -235
- package/skills/convert/tests/sample.csv +0 -4
- package/skills/convert/tests/sample.json +0 -5
- package/skills/convert/tests/sample.jsonl +0 -3
- package/skills/convert/tests/sample.yaml +0 -18
- package/skills/convert/tests/sample_obj.csv +0 -2
- package/skills/convert/tests/sample_obj.json +0 -9
- package/skills/mirror-public/SKILL.md +0 -237
- package/skills/mirror-public/assets/config.json +0 -5
- package/skills/mirror-public/scripts/mirror.sh +0 -374
- package/skills/self-sync/SKILL.md +0 -73
- package/skills/self-sync/scripts/run.js +0 -136
- package/skills/train/SKILL.md +0 -116
- package/skills/train/assets/dashboard-templates/classifiers.html +0 -106
- package/skills/train/assets/dashboard-templates/nlp.html +0 -102
- package/skills/train/assets/dashboard-templates/transformers.html +0 -98
- package/skills/train/assets/results-parsers/__init__.py +0 -9
- package/skills/train/assets/results-parsers/classifiers.py +0 -84
- package/skills/train/assets/results-parsers/nlp.py +0 -88
- package/skills/train/assets/results-parsers/reporter.py +0 -154
- package/skills/train/assets/results-parsers/transformers.py +0 -120
- package/skills/train/train_cli.py +0 -786
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ---------------------------------------------------------------------------
|
|
3
|
+
# skills/jenga/scripts/render-picker.sh
|
|
4
|
+
#
|
|
5
|
+
# Turn-by-turn numbered checklist renderer for `/jenga`'s bare-invocation
|
|
6
|
+
# interactive picker (E45_S02_T01). Renders a numbered hierarchical
|
|
7
|
+
# checklist of the full board (epics, stories, tasks, with summaries),
|
|
8
|
+
# sourced from `skills/jenga/scripts/board-scan.sh` (E45_S01_T01) — this
|
|
9
|
+
# script never re-scans the board directly.
|
|
10
|
+
#
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
# WHY THIS IS A TWO-INVOCATION, STATE-FILE SCRIPT (READ BEFORE CHANGING)
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
# Claude Code's Bash tool runs a command to completion and returns — it
|
|
15
|
+
# cannot block mid-script waiting on the next chat message. A single script
|
|
16
|
+
# that prints a checklist, then reads stdin for the user's selection, would
|
|
17
|
+
# never see real chat input; it would only ever see whatever the Bash tool
|
|
18
|
+
# call happened to pipe in (nothing, in the normal interactive case).
|
|
19
|
+
#
|
|
20
|
+
# So the picker is split across (at least) two invocations of this same
|
|
21
|
+
# script, coordinated by the calling agent, never by this script itself:
|
|
22
|
+
#
|
|
23
|
+
# 1. Turn 1 (start mode, no arguments): this script scans the board,
|
|
24
|
+
# assigns every item a sequential number, writes that numbered
|
|
25
|
+
# snapshot to a temp "state file", and prints a human-readable
|
|
26
|
+
# checklist + instructions to STDOUT.
|
|
27
|
+
# 2. The calling agent relays this script's literal STDOUT to the user
|
|
28
|
+
# as a chat message, verbatim — no summarizing, no reformatting.
|
|
29
|
+
# 3. The user replies in chat with their raw selection text (e.g.
|
|
30
|
+
# "3, 17, 42").
|
|
31
|
+
# 4. The calling agent re-invokes this script, passing the state file
|
|
32
|
+
# path (captured from turn 1's STDERR — see OUTPUT CONTRACT below)
|
|
33
|
+
# and the user's raw reply as two arguments (continue mode).
|
|
34
|
+
# 5. This script reads the prior state, validates the reply against the
|
|
35
|
+
# numbering-to-item snapshot it already captured, and either:
|
|
36
|
+
# - emits a plain-text error turn (state file left untouched, same
|
|
37
|
+
# path, ready for another continue-mode retry), or
|
|
38
|
+
# - emits the final resolved selection as JSON (state file removed)
|
|
39
|
+
# for the confirmation-tree renderer (E45_S02_T02) to consume.
|
|
40
|
+
#
|
|
41
|
+
# Do NOT attempt to "fix" this with a blocking read, a stdin prompt loop,
|
|
42
|
+
# or a polling/sleep loop waiting for external input — none of those work
|
|
43
|
+
# with the Bash tool's run-to-completion model, and ad-hoc polling loops as
|
|
44
|
+
# an inter-turn signaling mechanism are explicitly prohibited elsewhere in
|
|
45
|
+
# this repo's agent-coordination conventions (see epic E37).
|
|
46
|
+
#
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# USAGE
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# skills/jenga/scripts/render-picker.sh
|
|
51
|
+
# Start a new picker session. No arguments.
|
|
52
|
+
#
|
|
53
|
+
# skills/jenga/scripts/render-picker.sh <state_file> "<raw_reply>"
|
|
54
|
+
# Continue an existing picker session. <state_file> is the path
|
|
55
|
+
# printed on STDERR by the start-mode invocation (or by a previous
|
|
56
|
+
# error-turn continue-mode invocation — the path is stable across
|
|
57
|
+
# retries). <raw_reply> is the user's raw chat text for this turn,
|
|
58
|
+
# passed as a single argument (quote it).
|
|
59
|
+
#
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# OUTPUT CONTRACT (deliberately NOT uniform JSON — see rationale below)
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# Unlike board-scan.sh / resolve-id.sh / cascade-resolve.sh (which always
|
|
64
|
+
# emit pure JSON on stdout), this script splits its output by *audience*,
|
|
65
|
+
# because it is the one script in this family with a human in the loop for
|
|
66
|
+
# most of its turns:
|
|
67
|
+
#
|
|
68
|
+
# - Start mode, and continue-mode ERROR turns:
|
|
69
|
+
# STDOUT = plain human-readable text (the checklist, or an error
|
|
70
|
+
# message + retry instructions). This is exactly what the
|
|
71
|
+
# calling agent relays verbatim to the user — nothing to
|
|
72
|
+
# parse, nothing to strip.
|
|
73
|
+
# STDERR = a single line: `STATE_FILE: <absolute path>` — metadata
|
|
74
|
+
# for the calling AGENT to capture for the next invocation.
|
|
75
|
+
# Never intended for the human to read; do not relay it.
|
|
76
|
+
#
|
|
77
|
+
# - Continue mode, RESOLVED (fully valid) turn:
|
|
78
|
+
# STDOUT = a single JSON object (schema below) — the machine-readable
|
|
79
|
+
# handoff to the confirmation-tree renderer (E45_S02_T02).
|
|
80
|
+
# This is NOT meant to be relayed to the user verbatim.
|
|
81
|
+
# STDERR = empty (state file has been removed; nothing to carry
|
|
82
|
+
# forward).
|
|
83
|
+
#
|
|
84
|
+
# - Continue mode, CANCELLED (`cancel` reply, case-insensitive):
|
|
85
|
+
# STDOUT = plain human-readable cancellation acknowledgement.
|
|
86
|
+
# STDERR = empty (state file has been removed).
|
|
87
|
+
#
|
|
88
|
+
# RESOLVED JSON schema:
|
|
89
|
+
# {
|
|
90
|
+
# "status": "resolved",
|
|
91
|
+
# "selected_numbers": [3, 17, 42], // as typed, de-duplicated,
|
|
92
|
+
# // in first-seen order
|
|
93
|
+
# "resolved_ids": ["E01", "E04_S05_T06"],
|
|
94
|
+
# "resolved_ids_csv": "E01,E04_S05_T06" // ready to pass directly
|
|
95
|
+
# // as cascade-resolve.sh's
|
|
96
|
+
# // single argument
|
|
97
|
+
# }
|
|
98
|
+
#
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# SELECTION GRAMMAR (deliberately distinct from resolve-id.sh's ID grammar)
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
# A reply is a comma-separated list of positive integers referring to the
|
|
103
|
+
# numbers printed in THIS session's checklist (whitespace around commas and
|
|
104
|
+
# numbers is insignificant; purely empty segments from stray/trailing
|
|
105
|
+
# commas are ignored, not rejected). This is NOT the `E##_S##_T##` fuzzy-ID
|
|
106
|
+
# text grammar `resolve-id.sh` implements — that grammar belongs to the
|
|
107
|
+
# separate `/jenga <ids>` explicit-argument entry mode (E45_S01_T02), which
|
|
108
|
+
# bypasses this picker entirely. Reusing resolve-id.sh here would require
|
|
109
|
+
# it to parse a second, unrelated micro-grammar (bare numbers) its own
|
|
110
|
+
# header contract says nothing about, so this script builds and validates
|
|
111
|
+
# its own numbering-to-item mapping directly from the state file snapshot
|
|
112
|
+
# instead of calling into resolve-id.sh.
|
|
113
|
+
#
|
|
114
|
+
# Validation is ALL-OR-NOTHING per turn: if a reply contains any non-numeric
|
|
115
|
+
# token or any number outside the valid 1..N range for this session, the
|
|
116
|
+
# ENTIRE turn is rejected as an error turn (state file left untouched) —
|
|
117
|
+
# there is no partial application of the valid numbers in a mixed-validity
|
|
118
|
+
# reply, and no accumulation of a selection across multiple turns. A turn is
|
|
119
|
+
# either fully consumed (resolved) or fully retried (error) — this keeps the
|
|
120
|
+
# loop's semantics simple and satisfies the requirement that invalid/out-of-
|
|
121
|
+
# range numbers produce a clear error rather than being silently dropped.
|
|
122
|
+
#
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
# STATE FILE
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
# Created under `mktemp -t jenga-picker-XXXXXX.json`. It is a SELF-CONTAINED
|
|
127
|
+
# SNAPSHOT — each numbered entry stores the full board-scan.sh item object
|
|
128
|
+
# (id, type, title, status, summary, epic_id, story_id) as it looked at
|
|
129
|
+
# start-of-picker time, not just the id. This means an error-turn retry, or
|
|
130
|
+
# a re-print, never needs to re-invoke board-scan.sh and can never disagree
|
|
131
|
+
# with what the user was actually shown, even if the live board changes
|
|
132
|
+
# mid-session.
|
|
133
|
+
#
|
|
134
|
+
# If the state file is missing or unreadable when continue mode is invoked
|
|
135
|
+
# (e.g. the OS temp dir was cleared), this is treated as a real setup
|
|
136
|
+
# problem (exit 2), not a per-input validation failure — the user has to
|
|
137
|
+
# restart the picker (start mode) from scratch.
|
|
138
|
+
#
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
# NO CASCADE / DEDUP LOGIC HERE
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
# Selecting an epic's number and one of its own descendant task's numbers
|
|
143
|
+
# in the same reply is allowed; `resolved_ids` will contain both exactly as
|
|
144
|
+
# typed. Expansion of a parent selection to its eligible descendants,
|
|
145
|
+
# cross-level deduplication, and terminal-status filtering are the job of
|
|
146
|
+
# `cascade-resolve.sh` (E45_S01_T03, already built) and the confirmation-
|
|
147
|
+
# tree renderer (E45_S02_T02, not yet built) — this script's only
|
|
148
|
+
# responsibility is reporting exactly which board IDs the user typed a
|
|
149
|
+
# number for.
|
|
150
|
+
#
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
# EXIT CODES
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
# 0 start mode rendered successfully; OR continue mode fully resolved
|
|
155
|
+
# 1 continue mode: at least one invalid/out-of-range number in the
|
|
156
|
+
# reply — an error turn was emitted, retry with the same state file
|
|
157
|
+
# 2 usage error, or a real setup problem (board-scan.sh / python3
|
|
158
|
+
# unavailable, state file missing/corrupt)
|
|
159
|
+
# 3 continue mode: user replied "cancel" — picker session aborted,
|
|
160
|
+
# state file removed
|
|
161
|
+
#
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
set -euo pipefail
|
|
165
|
+
|
|
166
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
167
|
+
BOARD_SCAN="$SCRIPT_DIR/board-scan.sh"
|
|
168
|
+
|
|
169
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
170
|
+
echo "Error: python3 is required by render-picker.sh" >&2
|
|
171
|
+
exit 2
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
if [ $# -eq 0 ]; then
|
|
175
|
+
MODE="start"
|
|
176
|
+
elif [ $# -eq 2 ]; then
|
|
177
|
+
MODE="continue"
|
|
178
|
+
STATE_FILE="$1"
|
|
179
|
+
RAW_REPLY="$2"
|
|
180
|
+
else
|
|
181
|
+
echo "Usage:" >&2
|
|
182
|
+
echo " render-picker.sh # start a new picker session" >&2
|
|
183
|
+
echo " render-picker.sh <state_file> \"<raw_reply>\" # continue an existing session" >&2
|
|
184
|
+
exit 2
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
if [ "$MODE" = "start" ]; then
|
|
188
|
+
if [ ! -x "$BOARD_SCAN" ]; then
|
|
189
|
+
echo "Error: board-scan.sh not found or not executable at $BOARD_SCAN" >&2
|
|
190
|
+
exit 2
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
BOARD_JSON="$("$BOARD_SCAN")" || {
|
|
194
|
+
echo "Error: board-scan.sh failed" >&2
|
|
195
|
+
exit 2
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
STATE_FILE="$(mktemp -t jenga-picker-XXXXXX.json)"
|
|
199
|
+
|
|
200
|
+
# The rendering/state-writing logic is written to a temp .py file rather
|
|
201
|
+
# than piped in via `python3 -` because the board JSON is delivered on
|
|
202
|
+
# stdin — `python3 -` would consume stdin itself as the script source,
|
|
203
|
+
# leaving nothing for the script's own sys.stdin.read() to see. Same
|
|
204
|
+
# pattern as resolve-id.sh.
|
|
205
|
+
PY_SCRIPT="$(mktemp -t render-picker-start-XXXXXX.py)"
|
|
206
|
+
trap 'rm -f "$PY_SCRIPT"' EXIT
|
|
207
|
+
|
|
208
|
+
cat > "$PY_SCRIPT" <<'PY'
|
|
209
|
+
import json
|
|
210
|
+
import sys
|
|
211
|
+
from datetime import datetime, timezone
|
|
212
|
+
|
|
213
|
+
state_file_path = sys.argv[1]
|
|
214
|
+
board_json = sys.stdin.read()
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
items = json.loads(board_json)
|
|
218
|
+
except Exception as e:
|
|
219
|
+
print(f"Error: could not parse board-scan.sh output as JSON: {e}", file=sys.stderr)
|
|
220
|
+
sys.exit(2)
|
|
221
|
+
|
|
222
|
+
epics = [i for i in items if i.get("type") == "epic"]
|
|
223
|
+
stories = [i for i in items if i.get("type") == "story"]
|
|
224
|
+
tasks = [i for i in items if i.get("type") == "task"]
|
|
225
|
+
|
|
226
|
+
valid_epic_ids = {e["id"] for e in epics}
|
|
227
|
+
valid_story_ids = {s["id"] for s in stories}
|
|
228
|
+
|
|
229
|
+
stories_by_epic = {}
|
|
230
|
+
unlinked_stories = []
|
|
231
|
+
for s in stories:
|
|
232
|
+
epic_id = s.get("epic_id")
|
|
233
|
+
if epic_id in valid_epic_ids:
|
|
234
|
+
stories_by_epic.setdefault(epic_id, []).append(s)
|
|
235
|
+
else:
|
|
236
|
+
unlinked_stories.append(s)
|
|
237
|
+
|
|
238
|
+
tasks_by_story = {}
|
|
239
|
+
unlinked_tasks = []
|
|
240
|
+
for t in tasks:
|
|
241
|
+
story_id = t.get("story_id")
|
|
242
|
+
if story_id in valid_story_ids:
|
|
243
|
+
tasks_by_story.setdefault(story_id, []).append(t)
|
|
244
|
+
else:
|
|
245
|
+
unlinked_tasks.append(t)
|
|
246
|
+
|
|
247
|
+
TYPE_TAG = {"epic": "EPIC", "story": "STORY", "task": "TASK"}
|
|
248
|
+
INDENT = {"epic": "", "story": " ", "task": " "}
|
|
249
|
+
|
|
250
|
+
numbering = {}
|
|
251
|
+
lines = []
|
|
252
|
+
counter = 1
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def render_item(item, level):
|
|
256
|
+
global counter
|
|
257
|
+
n = counter
|
|
258
|
+
counter += 1
|
|
259
|
+
numbering[str(n)] = item
|
|
260
|
+
indent = INDENT[level]
|
|
261
|
+
title = item.get("title") or "(untitled)"
|
|
262
|
+
status = item.get("status") or "(no status)"
|
|
263
|
+
summary = item.get("summary") or "(no summary)"
|
|
264
|
+
lines.append(f"{indent}{n}. [{TYPE_TAG[level]}] {item['id']} — {title} (status: {status})")
|
|
265
|
+
lines.append(f"{indent} {summary}")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
for e in epics:
|
|
269
|
+
render_item(e, "epic")
|
|
270
|
+
for s in stories_by_epic.get(e["id"], []):
|
|
271
|
+
render_item(s, "story")
|
|
272
|
+
for t in tasks_by_story.get(s["id"], []):
|
|
273
|
+
render_item(t, "task")
|
|
274
|
+
|
|
275
|
+
if unlinked_stories:
|
|
276
|
+
lines.append("")
|
|
277
|
+
lines.append("-- Unlinked Stories (no matching epic on the board) --")
|
|
278
|
+
for s in unlinked_stories:
|
|
279
|
+
render_item(s, "story")
|
|
280
|
+
for t in tasks_by_story.get(s["id"], []):
|
|
281
|
+
render_item(t, "task")
|
|
282
|
+
|
|
283
|
+
if unlinked_tasks:
|
|
284
|
+
lines.append("")
|
|
285
|
+
lines.append("-- Unlinked Tasks (no matching story on the board) --")
|
|
286
|
+
for t in unlinked_tasks:
|
|
287
|
+
render_item(t, "story") # top-level indent for an orphaned task
|
|
288
|
+
|
|
289
|
+
total = len(numbering)
|
|
290
|
+
|
|
291
|
+
state = {
|
|
292
|
+
"version": 1,
|
|
293
|
+
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
294
|
+
"total_items": total,
|
|
295
|
+
"numbering": numbering,
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
with open(state_file_path, "w", encoding="utf-8") as f:
|
|
299
|
+
json.dump(state, f, indent=2)
|
|
300
|
+
f.write("\n")
|
|
301
|
+
|
|
302
|
+
header = [
|
|
303
|
+
"=" * 68,
|
|
304
|
+
" /jenga — Board Picker",
|
|
305
|
+
"=" * 68,
|
|
306
|
+
"Select items by number, comma-separated. You may mix levels freely",
|
|
307
|
+
'— e.g. "3,17,42" can select an epic, a story, and a task all in',
|
|
308
|
+
"the same reply. Selecting a parent (epic/story) later cascades to",
|
|
309
|
+
"its eligible descendants during confirmation.",
|
|
310
|
+
"",
|
|
311
|
+
'Reply "cancel" at any time to abort the picker.',
|
|
312
|
+
"-" * 68,
|
|
313
|
+
"",
|
|
314
|
+
]
|
|
315
|
+
|
|
316
|
+
if total == 0:
|
|
317
|
+
body = ["(The board is empty — there is nothing to select.)"]
|
|
318
|
+
else:
|
|
319
|
+
body = lines
|
|
320
|
+
|
|
321
|
+
footer = [
|
|
322
|
+
"",
|
|
323
|
+
"-" * 68,
|
|
324
|
+
f"Total: {len(epics)} epic(s), {len(stories)} stor{'y' if len(stories) == 1 else 'ies'}, "
|
|
325
|
+
f"{len(tasks)} task(s) ({total} item(s) numbered 1-{total})." if total else "Nothing to select.",
|
|
326
|
+
"",
|
|
327
|
+
'Reply with your selection (comma-separated numbers), or "cancel".',
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
print("\n".join(header + body + footer))
|
|
331
|
+
print(f"STATE_FILE: {state_file_path}", file=sys.stderr)
|
|
332
|
+
PY
|
|
333
|
+
|
|
334
|
+
python3 "$PY_SCRIPT" "$STATE_FILE" <<< "$BOARD_JSON"
|
|
335
|
+
exit 0
|
|
336
|
+
fi
|
|
337
|
+
|
|
338
|
+
# --------------------------------------------------------------------------
|
|
339
|
+
# continue mode
|
|
340
|
+
# --------------------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
if [ ! -f "$STATE_FILE" ]; then
|
|
343
|
+
echo "Error: state file not found at $STATE_FILE" >&2
|
|
344
|
+
echo "The picker session may have expired (e.g. temp dir was cleared)." >&2
|
|
345
|
+
echo "Start a new picker session with: render-picker.sh" >&2
|
|
346
|
+
exit 2
|
|
347
|
+
fi
|
|
348
|
+
|
|
349
|
+
PY_SCRIPT="$(mktemp -t render-picker-continue-XXXXXX.py)"
|
|
350
|
+
trap 'rm -f "$PY_SCRIPT"' EXIT
|
|
351
|
+
|
|
352
|
+
cat > "$PY_SCRIPT" <<'PY'
|
|
353
|
+
import json
|
|
354
|
+
import re
|
|
355
|
+
import sys
|
|
356
|
+
|
|
357
|
+
state_file_path = sys.argv[1]
|
|
358
|
+
raw_reply = sys.argv[2]
|
|
359
|
+
|
|
360
|
+
try:
|
|
361
|
+
with open(state_file_path, encoding="utf-8") as f:
|
|
362
|
+
state = json.load(f)
|
|
363
|
+
except Exception as e:
|
|
364
|
+
print(f"Error: could not read/parse state file at {state_file_path}: {e}", file=sys.stderr)
|
|
365
|
+
sys.exit(2)
|
|
366
|
+
|
|
367
|
+
numbering = state.get("numbering", {})
|
|
368
|
+
total = state.get("total_items", len(numbering))
|
|
369
|
+
|
|
370
|
+
if raw_reply.strip().lower() == "cancel":
|
|
371
|
+
import os
|
|
372
|
+
try:
|
|
373
|
+
os.remove(state_file_path)
|
|
374
|
+
except OSError:
|
|
375
|
+
pass
|
|
376
|
+
print("Picker session cancelled. No selection was made.")
|
|
377
|
+
sys.exit(3)
|
|
378
|
+
|
|
379
|
+
raw_tokens = raw_reply.split(",")
|
|
380
|
+
tokens = [tok.strip() for tok in raw_tokens]
|
|
381
|
+
tokens = [tok for tok in tokens if tok != ""]
|
|
382
|
+
|
|
383
|
+
if not tokens:
|
|
384
|
+
print("Error: no selection numbers found in your reply.")
|
|
385
|
+
print("")
|
|
386
|
+
print('Reply with comma-separated numbers from the checklist above (e.g. "3,17,42"), or "cancel".')
|
|
387
|
+
print(f"STATE_FILE: {state_file_path}", file=sys.stderr)
|
|
388
|
+
sys.exit(1)
|
|
389
|
+
|
|
390
|
+
invalid = []
|
|
391
|
+
valid_numbers = []
|
|
392
|
+
for tok in tokens:
|
|
393
|
+
if not re.fullmatch(r"[0-9]+", tok):
|
|
394
|
+
invalid.append((tok, "not a whole number"))
|
|
395
|
+
continue
|
|
396
|
+
if tok not in numbering:
|
|
397
|
+
invalid.append((tok, f"out of range (valid: 1-{total})"))
|
|
398
|
+
continue
|
|
399
|
+
valid_numbers.append(int(tok))
|
|
400
|
+
|
|
401
|
+
if invalid:
|
|
402
|
+
print("Error: your reply contains invalid selection number(s):")
|
|
403
|
+
for tok, reason in invalid:
|
|
404
|
+
print(f" - '{tok}': {reason}")
|
|
405
|
+
print("")
|
|
406
|
+
print(f"Valid selection numbers for this session are 1-{total}.")
|
|
407
|
+
print('Reply again with a corrected, comma-separated list (e.g. "3,17,42"), or "cancel".')
|
|
408
|
+
print(f"STATE_FILE: {state_file_path}", file=sys.stderr)
|
|
409
|
+
sys.exit(1)
|
|
410
|
+
|
|
411
|
+
# All-or-nothing success: de-duplicate while preserving first-seen order.
|
|
412
|
+
seen = set()
|
|
413
|
+
selected_numbers = []
|
|
414
|
+
resolved_ids = []
|
|
415
|
+
for n in valid_numbers:
|
|
416
|
+
if n in seen:
|
|
417
|
+
continue
|
|
418
|
+
seen.add(n)
|
|
419
|
+
selected_numbers.append(n)
|
|
420
|
+
resolved_ids.append(numbering[str(n)]["id"])
|
|
421
|
+
|
|
422
|
+
import os
|
|
423
|
+
try:
|
|
424
|
+
os.remove(state_file_path)
|
|
425
|
+
except OSError:
|
|
426
|
+
pass
|
|
427
|
+
|
|
428
|
+
result = {
|
|
429
|
+
"status": "resolved",
|
|
430
|
+
"selected_numbers": selected_numbers,
|
|
431
|
+
"resolved_ids": resolved_ids,
|
|
432
|
+
"resolved_ids_csv": ",".join(resolved_ids),
|
|
433
|
+
}
|
|
434
|
+
print(json.dumps(result, indent=2))
|
|
435
|
+
sys.exit(0)
|
|
436
|
+
PY
|
|
437
|
+
|
|
438
|
+
python3 "$PY_SCRIPT" "$STATE_FILE" "$RAW_REPLY"
|
|
439
|
+
exit $?
|