@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,297 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ---------------------------------------------------------------------------
|
|
3
|
+
# skills/jenga/scripts/cascade-resolve.sh
|
|
4
|
+
#
|
|
5
|
+
# Cascade resolver for `/jenga`'s interactive scope-selection flow (E45).
|
|
6
|
+
# Given a resolved board-ID set (exact `E##` / `E##_S##` / `E##_S##_T##`
|
|
7
|
+
# IDs — already resolved by the picker in E45_S02_T01 or by
|
|
8
|
+
# `resolve-id.sh` in E45_S01_T02; this script does NOT do fuzzy-ID
|
|
9
|
+
# parsing), expands any epic or story selection into its full set of
|
|
10
|
+
# eligible descendants:
|
|
11
|
+
#
|
|
12
|
+
# - An epic selection expands to all of its stories and their tasks.
|
|
13
|
+
# - A story selection expands to all of its tasks.
|
|
14
|
+
# - Expansion only includes descendants in a Pending/eligible state —
|
|
15
|
+
# terminal-status descendants (Passed, Passed with remarks, Failed,
|
|
16
|
+
# Rejected, Blocked, Done) are excluded.
|
|
17
|
+
# - A descendant epic/story that has not yet been broken down on the
|
|
18
|
+
# board (zero stories under an epic, zero tasks under a story) is
|
|
19
|
+
# flagged under `undecomposed` so jenga's existing Phase 1/2
|
|
20
|
+
# decomposition logic can pick it up. This script never decomposes
|
|
21
|
+
# anything itself — it only flags the need.
|
|
22
|
+
#
|
|
23
|
+
# Per this repo's "Scripts Over Inline Logic" principle and the sibling
|
|
24
|
+
# board-scan.sh's own header contract, this script consumes
|
|
25
|
+
# board-scan.sh's JSON output as the SINGLE SOURCE OF TRUTH for board
|
|
26
|
+
# contents — it does not re-scan project/board/ independently.
|
|
27
|
+
#
|
|
28
|
+
# Downstream consumer: E45_S02_T02 (confirmation-tree renderer) reads
|
|
29
|
+
# this script's output to render the nested Epic > Story > Task
|
|
30
|
+
# confirmation tree with per-level counts.
|
|
31
|
+
#
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# USAGE
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# skills/jenga/scripts/cascade-resolve.sh "<comma-separated-ids>"
|
|
36
|
+
#
|
|
37
|
+
# One argument: a comma-separated list of exact, already-resolved board
|
|
38
|
+
# IDs (e.g. "E01,E02_S03,E04_S05_T06"). Whitespace around commas/ids is
|
|
39
|
+
# trimmed. IDs are matched case-sensitively against the board (board IDs
|
|
40
|
+
# are always uppercase per templates/SCRUM_BOARD_SCHEMA.md).
|
|
41
|
+
#
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# OUTPUT SCHEMA (stable — downstream scripts depend on these exact names)
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# stdout is a single JSON object. Nothing else is ever written to stdout.
|
|
46
|
+
#
|
|
47
|
+
# {
|
|
48
|
+
# "items": [ <board-scan.sh item objects — see that script's header
|
|
49
|
+
# for the per-item schema — for every epic/story/task
|
|
50
|
+
# node in the resolved tree, deduplicated> ],
|
|
51
|
+
# "resolved_ids": ["E01", "E01_S02", "E01_S02_T01"],
|
|
52
|
+
# "counts": {"epics": 1, "stories": 1, "tasks": 1},
|
|
53
|
+
# "undecomposed": [
|
|
54
|
+
# {"id": "E02", "type": "epic", "missing": "stories"},
|
|
55
|
+
# {"id": "E02_S01", "type": "story", "missing": "tasks"}
|
|
56
|
+
# ],
|
|
57
|
+
# "warnings": [
|
|
58
|
+
# {"input": "E99", "reason": "not found on board"},
|
|
59
|
+
# {"input": "E01_S02_T09", "reason": "excluded: terminal status (Passed)"},
|
|
60
|
+
# {"input": "foo", "reason": "malformed ID (does not match E##, E##_S##, or E##_S##_T## shape)"}
|
|
61
|
+
# ]
|
|
62
|
+
# }
|
|
63
|
+
#
|
|
64
|
+
# Field notes:
|
|
65
|
+
# - `items` includes every node actually present in the resolved tree —
|
|
66
|
+
# epics/stories that were directly selected or expanded into, plus
|
|
67
|
+
# eligible (non-terminal) descendant stories/tasks. It is NOT limited
|
|
68
|
+
# to leaf tasks, because the confirmation-tree renderer needs the
|
|
69
|
+
# full Epic > Story > Task shape, not just an execution list.
|
|
70
|
+
# - `resolved_ids` is the same set as `items`, as a flat list of IDs
|
|
71
|
+
# (convenience for callers that only need IDs).
|
|
72
|
+
# - `counts` reflects unique nodes of each type present in `items` —
|
|
73
|
+
# i.e. what the confirmation tree will actually render — not a count
|
|
74
|
+
# of input arguments.
|
|
75
|
+
# - `undecomposed` entries mean "this epic/story has zero children on
|
|
76
|
+
# the board at all", distinct from "this epic/story has children but
|
|
77
|
+
# none are eligible" (which is not flagged — there is nothing to
|
|
78
|
+
# decompose, the work is simply already done or blocked).
|
|
79
|
+
# - `warnings` covers three non-fatal per-input problems: an ID not
|
|
80
|
+
# found on the board, a directly-selected task/story/epic excluded
|
|
81
|
+
# for being in a terminal status, and a malformed ID shape. None of
|
|
82
|
+
# these abort the run — valid inputs are still resolved and stdout is
|
|
83
|
+
# still a complete, valid JSON object.
|
|
84
|
+
#
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# ERROR HANDLING
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# Exit codes:
|
|
89
|
+
# 0 resolution completed (stdout is always valid JSON on this path,
|
|
90
|
+
# even if some inputs produced warnings, and even if the result is
|
|
91
|
+
# an empty items/resolved_ids set because every input was invalid)
|
|
92
|
+
# 1 a structural failure: no argument given, board-scan.sh itself
|
|
93
|
+
# failed or produced unparseable output, or python3 is unavailable
|
|
94
|
+
# — these are real setup problems, not per-input noise
|
|
95
|
+
#
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
set -euo pipefail
|
|
99
|
+
|
|
100
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
101
|
+
|
|
102
|
+
if [ $# -lt 1 ] || [ -z "${1// /}" ]; then
|
|
103
|
+
echo "Usage: cascade-resolve.sh \"<comma-separated-ids>\"" >&2
|
|
104
|
+
echo "Example: cascade-resolve.sh \"E01,E02_S03,E04_S05_T06\"" >&2
|
|
105
|
+
exit 1
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
RAW_IDS="$1"
|
|
109
|
+
|
|
110
|
+
# No project-dir resolution needed here: board-scan.sh (invoked below)
|
|
111
|
+
# already resolves JENGA_PROJECT_DIR itself (CLAUDE_PROJECT_DIR -> git
|
|
112
|
+
# toplevel -> cwd) and is the sole source of board contents for this
|
|
113
|
+
# script, per the "Scripts Over Inline Logic" / single-source-of-truth
|
|
114
|
+
# contract documented in its own header.
|
|
115
|
+
BOARD_SCAN="$SCRIPT_DIR/board-scan.sh"
|
|
116
|
+
|
|
117
|
+
if [ ! -x "$BOARD_SCAN" ]; then
|
|
118
|
+
echo "Error: board-scan.sh not found or not executable at $BOARD_SCAN" >&2
|
|
119
|
+
exit 1
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
123
|
+
echo "Error: python3 is required by cascade-resolve.sh" >&2
|
|
124
|
+
exit 1
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
BOARD_JSON_FILE="$(mktemp)"
|
|
128
|
+
trap 'rm -f "$BOARD_JSON_FILE"' EXIT
|
|
129
|
+
|
|
130
|
+
if ! "$BOARD_SCAN" > "$BOARD_JSON_FILE"; then
|
|
131
|
+
echo "Error: board-scan.sh failed" >&2
|
|
132
|
+
exit 1
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
python3 - "$RAW_IDS" "$BOARD_JSON_FILE" <<'PY'
|
|
136
|
+
import json
|
|
137
|
+
import re
|
|
138
|
+
import sys
|
|
139
|
+
|
|
140
|
+
raw_ids_arg = sys.argv[1]
|
|
141
|
+
board_json_file = sys.argv[2]
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
with open(board_json_file, encoding="utf-8") as f:
|
|
145
|
+
board_items = json.load(f)
|
|
146
|
+
except Exception as e:
|
|
147
|
+
print(f"Error: could not parse board-scan.sh output as JSON: {e}", file=sys.stderr)
|
|
148
|
+
sys.exit(1)
|
|
149
|
+
|
|
150
|
+
# Terminal statuses, verbatim from templates/SCRUM_BOARD_SCHEMA.md's
|
|
151
|
+
# Status Values table. An item in one of these statuses is never
|
|
152
|
+
# included as an expanded/eligible descendant.
|
|
153
|
+
TERMINAL_STATUSES = {
|
|
154
|
+
"Passed",
|
|
155
|
+
"Passed with remarks",
|
|
156
|
+
"Failed",
|
|
157
|
+
"Rejected",
|
|
158
|
+
"Blocked",
|
|
159
|
+
"Done",
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
EPIC_RE = re.compile(r'^E\d{2}$')
|
|
163
|
+
STORY_RE = re.compile(r'^E\d{2}_S\d{2}$')
|
|
164
|
+
TASK_RE = re.compile(r'^E\d{2}_S\d{2}_T\d{2}$')
|
|
165
|
+
|
|
166
|
+
by_id = {}
|
|
167
|
+
stories_by_epic = {}
|
|
168
|
+
tasks_by_story = {}
|
|
169
|
+
|
|
170
|
+
for item in board_items:
|
|
171
|
+
item_id = item.get("id", "")
|
|
172
|
+
if not item_id:
|
|
173
|
+
continue
|
|
174
|
+
by_id[item_id] = item
|
|
175
|
+
if item.get("type") == "story":
|
|
176
|
+
epic_id = item.get("epic_id")
|
|
177
|
+
if epic_id:
|
|
178
|
+
stories_by_epic.setdefault(epic_id, []).append(item)
|
|
179
|
+
elif item.get("type") == "task":
|
|
180
|
+
story_id = item.get("story_id")
|
|
181
|
+
if story_id:
|
|
182
|
+
tasks_by_story.setdefault(story_id, []).append(item)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def classify(item_id):
|
|
186
|
+
if TASK_RE.match(item_id):
|
|
187
|
+
return "task"
|
|
188
|
+
if STORY_RE.match(item_id):
|
|
189
|
+
return "story"
|
|
190
|
+
if EPIC_RE.match(item_id):
|
|
191
|
+
return "epic"
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
resolved = {} # id -> item, dedup-preserving insertion order
|
|
196
|
+
resolved_order = []
|
|
197
|
+
undecomposed = []
|
|
198
|
+
undecomposed_seen = set()
|
|
199
|
+
warnings = []
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def add_item(item):
|
|
203
|
+
item_id = item["id"]
|
|
204
|
+
if item_id not in resolved:
|
|
205
|
+
resolved[item_id] = item
|
|
206
|
+
resolved_order.append(item_id)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def flag_undecomposed(entry_id, entry_type, missing):
|
|
210
|
+
key = (entry_id, missing)
|
|
211
|
+
if key in undecomposed_seen:
|
|
212
|
+
return
|
|
213
|
+
undecomposed_seen.add(key)
|
|
214
|
+
undecomposed.append({"id": entry_id, "type": entry_type, "missing": missing})
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def expand_story(story_item, include_story_node=True):
|
|
218
|
+
"""Add an eligible story node and its eligible (non-terminal) tasks.
|
|
219
|
+
Flags the story as undecomposed if it has zero tasks at all."""
|
|
220
|
+
if include_story_node:
|
|
221
|
+
add_item(story_item)
|
|
222
|
+
story_id = story_item["id"]
|
|
223
|
+
tasks = tasks_by_story.get(story_id, [])
|
|
224
|
+
if not tasks:
|
|
225
|
+
flag_undecomposed(story_id, "story", "tasks")
|
|
226
|
+
return
|
|
227
|
+
for task_item in tasks:
|
|
228
|
+
if task_item.get("status") in TERMINAL_STATUSES:
|
|
229
|
+
continue
|
|
230
|
+
add_item(task_item)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
raw_inputs = [tok.strip() for tok in raw_ids_arg.split(",")]
|
|
234
|
+
raw_inputs = [tok for tok in raw_inputs if tok]
|
|
235
|
+
|
|
236
|
+
for input_id in raw_inputs:
|
|
237
|
+
item_type = classify(input_id)
|
|
238
|
+
if item_type is None:
|
|
239
|
+
warnings.append({
|
|
240
|
+
"input": input_id,
|
|
241
|
+
"reason": "malformed ID (does not match E##, E##_S##, or E##_S##_T## shape)",
|
|
242
|
+
})
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
item = by_id.get(input_id)
|
|
246
|
+
if item is None:
|
|
247
|
+
warnings.append({"input": input_id, "reason": "not found on board"})
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
if item_type == "epic":
|
|
251
|
+
# Always include the epic node itself for tree display, regardless
|
|
252
|
+
# of the epic's own status — cascading is about its descendants.
|
|
253
|
+
add_item(item)
|
|
254
|
+
stories = stories_by_epic.get(input_id, [])
|
|
255
|
+
if not stories:
|
|
256
|
+
flag_undecomposed(input_id, "epic", "stories")
|
|
257
|
+
continue
|
|
258
|
+
for story_item in stories:
|
|
259
|
+
if story_item.get("status") in TERMINAL_STATUSES:
|
|
260
|
+
continue
|
|
261
|
+
expand_story(story_item, include_story_node=True)
|
|
262
|
+
|
|
263
|
+
elif item_type == "story":
|
|
264
|
+
add_item(item)
|
|
265
|
+
expand_story(item, include_story_node=False)
|
|
266
|
+
|
|
267
|
+
else: # task
|
|
268
|
+
if item.get("status") in TERMINAL_STATUSES:
|
|
269
|
+
warnings.append({
|
|
270
|
+
"input": input_id,
|
|
271
|
+
"reason": f"excluded: terminal status ({item.get('status')})",
|
|
272
|
+
})
|
|
273
|
+
continue
|
|
274
|
+
add_item(item)
|
|
275
|
+
|
|
276
|
+
items_out = [resolved[i] for i in resolved_order]
|
|
277
|
+
counts = {"epics": 0, "stories": 0, "tasks": 0}
|
|
278
|
+
for it in items_out:
|
|
279
|
+
t = it.get("type")
|
|
280
|
+
if t == "epic":
|
|
281
|
+
counts["epics"] += 1
|
|
282
|
+
elif t == "story":
|
|
283
|
+
counts["stories"] += 1
|
|
284
|
+
elif t == "task":
|
|
285
|
+
counts["tasks"] += 1
|
|
286
|
+
|
|
287
|
+
output = {
|
|
288
|
+
"items": items_out,
|
|
289
|
+
"resolved_ids": resolved_order,
|
|
290
|
+
"counts": counts,
|
|
291
|
+
"undecomposed": undecomposed,
|
|
292
|
+
"warnings": warnings,
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
json.dump(output, sys.stdout, indent=2)
|
|
296
|
+
sys.stdout.write("\n")
|
|
297
|
+
PY
|