@jenga-ai/agent 1.1.0 → 1.2.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.
Files changed (91) hide show
  1. package/README.md +7 -3
  2. package/agents/developer.md +82 -2
  3. package/agents/scrum-master.md +140 -21
  4. package/agents/tester.md +90 -8
  5. package/hooks/on_session_end.sh +171 -20
  6. package/package.json +1 -1
  7. package/scripts/check-permission-level.sh +107 -0
  8. package/scripts/check-publicignore-match.sh +122 -0
  9. package/scripts/check-worktree-liveness.sh +193 -0
  10. package/scripts/generate-rapport-manifest.sh +43 -0
  11. package/scripts/idea_manager.sh +47 -0
  12. package/scripts/install-worktree-commit-guard.sh +134 -0
  13. package/scripts/jenga-permission-level-switch.sh +109 -0
  14. package/scripts/smoke-harness.sh +139 -0
  15. package/scripts/validate-board.sh +62 -0
  16. package/scripts/with-lock.sh +158 -0
  17. package/scripts/worktree-remove-guard.sh +204 -0
  18. package/skills/clearify/SKILL.md +52 -0
  19. package/skills/commit/SKILL.md +13 -4
  20. package/skills/distribute/CONFIG_SCHEMA.md +60 -2
  21. package/skills/do/SKILL.md +48 -11
  22. package/skills/doc-sync/SKILL.md +16 -0
  23. package/skills/doc-sync/assets/doc_targets.md +11 -0
  24. package/skills/idea/SKILL.md +56 -0
  25. package/skills/idea/assets/idea_handoff_template.md +26 -0
  26. package/skills/idea/assets/idea_template.md +3 -0
  27. package/skills/init/SKILL.md +100 -7
  28. package/skills/init/assets/directory_structure.txt +1 -0
  29. package/skills/init/assets/workflow_template.json +1 -1
  30. package/skills/init/scripts/apply-project-visibility.sh +176 -0
  31. package/skills/init/scripts/detect-existing-codebase.sh +166 -0
  32. package/skills/init/scripts/init.sh +30 -1
  33. package/skills/jenga/SKILL.md +160 -17
  34. package/skills/jenga/scripts/board-scan.sh +238 -0
  35. package/skills/jenga/scripts/cascade-resolve.sh +297 -0
  36. package/skills/jenga/scripts/render-confirmation.sh +679 -0
  37. package/skills/jenga/scripts/render-picker.sh +439 -0
  38. package/skills/jenga/scripts/resolve-id.sh +367 -0
  39. package/skills/jenga-permission-level/SKILL.md +81 -0
  40. package/skills/proceed/SKILL.md +1 -1
  41. package/skills/publish/SKILL.md +8 -5
  42. package/skills/publish/assets/ci-contract.md +2 -2
  43. package/skills/publish/assets/ownership-matrix.md +1 -1
  44. package/skills/publish/scripts/finalize_changelog.sh +115 -0
  45. package/skills/publish/scripts/generate_release_notes.sh +475 -28
  46. package/skills/publish/scripts/npm_ci_pipeline.sh +44 -6
  47. package/skills/publish/scripts/publish_deploy.sh +38 -8
  48. package/skills/publish/scripts/run_gates.sh +2 -2
  49. package/skills/reconcile/SKILL.md +117 -5
  50. package/skills/reconcile/scripts/detect-unlinked-code.sh +741 -0
  51. package/skills/skillify/assets/init-new/assets/directory_structure.txt +5 -1
  52. package/skills/spinoff/SKILL.md +12 -7
  53. package/skills/todo/SKILL.md +2 -0
  54. package/skills/uncharted/SKILL.md +711 -0
  55. package/skills/uncharted/assets/SEGMENT_PROPOSAL_TEMPLATE.md +129 -0
  56. package/skills/uncharted/assets/UNDERSTANDING_DOC_TEMPLATE.md +160 -0
  57. package/skills/uncharted/scripts/apply-subsystem-cap.sh +573 -0
  58. package/skills/uncharted/scripts/detect-dependencies.sh +732 -0
  59. package/skills/uncharted/scripts/detect-tests.sh +553 -0
  60. package/skills/uncharted/scripts/discover-subsystems.sh +1029 -0
  61. package/skills/uncharted/scripts/enumerate-target.sh +470 -0
  62. package/skills/uncharted/scripts/import-source.sh +517 -0
  63. package/skills/uncharted/scripts/inspect-provenance.sh +573 -0
  64. package/skills/uncharted/scripts/resolve-segment-target.sh +640 -0
  65. package/skills/uncharted/scripts/run-engine.sh +655 -0
  66. package/skills/uncharted/scripts/validate-proposed-items.sh +125 -0
  67. package/skills/uncharted/scripts/write-backfilled-epics.sh +498 -0
  68. package/skills/wtf/SKILL.md +20 -0
  69. package/templates/CHANGELOG_TEMPLATE.md +13 -0
  70. package/templates/PROBLEM_RAPPORT_TEMPLATE.md +4 -1
  71. package/templates/SCRUM_BOARD_SCHEMA.md +157 -10
  72. package/templates/permission-levels/README.md +73 -0
  73. package/templates/permission-levels/level-1-locked.json +71 -0
  74. package/templates/permission-levels/level-2-guarded.json +64 -0
  75. package/templates/permission-levels/level-3-standard.json +62 -0
  76. package/templates/permission-levels/level-4-elevated.json +60 -0
  77. package/templates/permission-levels/level-5-unrestricted.json +58 -0
  78. package/skills/convert/SKILL.md +0 -124
  79. package/skills/convert/convert_cli.py +0 -235
  80. package/skills/convert/tests/sample.csv +0 -4
  81. package/skills/convert/tests/sample.json +0 -5
  82. package/skills/convert/tests/sample.jsonl +0 -3
  83. package/skills/convert/tests/sample.yaml +0 -18
  84. package/skills/convert/tests/sample_obj.csv +0 -2
  85. package/skills/convert/tests/sample_obj.json +0 -9
  86. package/skills/mirror-public/SKILL.md +0 -237
  87. package/skills/mirror-public/assets/config.json +0 -5
  88. package/skills/mirror-public/scripts/mirror.sh +0 -374
  89. package/skills/self-sync/SKILL.md +0 -73
  90. package/skills/self-sync/scripts/run.js +0 -136
  91. package/skills/strategy/SKILL.md +0 -312
@@ -0,0 +1,367 @@
1
+ #!/usr/bin/env bash
2
+ # ---------------------------------------------------------------------------
3
+ # skills/jenga/scripts/resolve-id.sh
4
+ #
5
+ # Deterministic fuzzy-ID grammar parser and board-backed resolver for
6
+ # `/jenga`'s interactive scope-selection flow (E45). Given the raw,
7
+ # comma-separated ID string a user typed for `/jenga <ids>` (or IDs
8
+ # translated back from picker selection numbers), this script parses each
9
+ # comma-delimited segment per the exact grammar negotiated with the user,
10
+ # then resolves the parsed candidate against the real board inventory
11
+ # produced by `skills/jenga/scripts/board-scan.sh` (E45_S01_T01).
12
+ #
13
+ # This script NEVER re-scans the board independently — it invokes
14
+ # board-scan.sh as a subprocess and reads its JSON stdout as the single
15
+ # source of truth for what IDs actually exist. See board-scan.sh's own
16
+ # header comment for its output schema.
17
+ #
18
+ # ---------------------------------------------------------------------------
19
+ # GRAMMAR (final — negotiated with the user, do not reinterpret)
20
+ # ---------------------------------------------------------------------------
21
+ # 1. Strip whitespace, underscores, and punctuation from the input;
22
+ # uppercase it. Only [A-Z0-9] characters survive this step.
23
+ # 2. Split each contiguous digit run into 2-digit chunks, left to right.
24
+ # A digit run whose length is not a multiple of 2 is REJECTED as
25
+ # invalid input (never silently truncated or padded).
26
+ # 3. A chunk is TAGGED to a level (epic/story/task) when the digit run it
27
+ # came from is immediately adjacent — directly before or directly
28
+ # after, no separator survives step 1 — to a single E/S/T letter.
29
+ # 4. Untagged chunks fill the remaining levels positionally, in
30
+ # epic -> story -> task order, left to right, skipping any level
31
+ # already claimed by an explicit tag.
32
+ # 5. The parsed candidate (full or partial) is resolved against the board
33
+ # inventory. A partial ID that does not uniquely resolve (e.g. a story
34
+ # chunk with no epic tag, when multiple epics share that story number)
35
+ # is REJECTED and reports that disambiguation is required — never
36
+ # guessed across epics.
37
+ #
38
+ # Negotiated examples (must resolve exactly as documented):
39
+ # "E01s02" -> E01_S02
40
+ # "e01 S04 t02" -> E01_S04_T02
41
+ # "0103" -> E01_S03 (two untagged chunks: epic=01, story=03)
42
+ #
43
+ # ---------------------------------------------------------------------------
44
+ # USAGE
45
+ # ---------------------------------------------------------------------------
46
+ # skills/jenga/scripts/resolve-id.sh "<comma-separated raw ID list>"
47
+ #
48
+ # Example:
49
+ # skills/jenga/scripts/resolve-id.sh "E01s02, e01 S04 t02, 0103"
50
+ #
51
+ # Each comma-delimited segment of the argument is parsed and resolved
52
+ # independently. Whitespace around commas is insignificant (stripped by the
53
+ # grammar itself in step 1, since spaces are whitespace).
54
+ #
55
+ # ---------------------------------------------------------------------------
56
+ # OUTPUT SCHEMA
57
+ # ---------------------------------------------------------------------------
58
+ # stdout is a single JSON array, one object per input segment, in input
59
+ # order. Nothing else is ever written to stdout.
60
+ #
61
+ # {
62
+ # "input": "E01s02", // the raw segment exactly as given
63
+ # "status": "resolved" | "rejected",
64
+ # "resolved_id": "E01_S02" | null,
65
+ # "reason": null | "<human-readable rejection reason>"
66
+ # }
67
+ #
68
+ # Rejection reasons include (not exhaustive, always human-readable):
69
+ # - "empty input after stripping whitespace/underscores/punctuation"
70
+ # - "malformed digit run: '<run>' has odd length <n> (not a multiple of 2)"
71
+ # - "too many untagged chunks: <n> chunks but only <m> open level(s)"
72
+ # - "no ID chunks found in input"
73
+ # - "conflicting tags: level '<L>' tagged more than once"
74
+ # - "not found on board: <id-or-partial-description>"
75
+ # - "ambiguous partial ID '<partial>': matches <n> candidates
76
+ # (<id1>, <id2>, ...) - disambiguation required"
77
+ #
78
+ # ---------------------------------------------------------------------------
79
+ # EXIT CODES
80
+ # ---------------------------------------------------------------------------
81
+ # 0 every segment resolved
82
+ # 1 at least one segment was rejected (stdout is still valid JSON;
83
+ # callers should inspect each element's "status", not rely on the
84
+ # exit code alone to know WHICH segment failed)
85
+ # 2 usage error (no argument given) or board-scan.sh / python3 failure
86
+ # — a real setup problem, not a per-segment parse failure
87
+ #
88
+ # ---------------------------------------------------------------------------
89
+
90
+ set -euo pipefail
91
+
92
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
93
+
94
+ # Unlike board-scan.sh, this script never reads project/board/ directly —
95
+ # it only needs SCRIPT_DIR to locate and invoke board-scan.sh, which does
96
+ # its own JENGA_PROJECT_DIR resolution internally. No project-root
97
+ # resolution is needed here.
98
+ BOARD_SCAN="$SCRIPT_DIR/board-scan.sh"
99
+
100
+ if [ $# -lt 1 ] || [ -z "${1:-}" ]; then
101
+ echo 'Usage: resolve-id.sh "<comma-separated raw ID list>"' >&2
102
+ exit 2
103
+ fi
104
+
105
+ RAW_INPUT="$1"
106
+
107
+ if [ ! -x "$BOARD_SCAN" ]; then
108
+ echo "Error: board-scan.sh not found or not executable at $BOARD_SCAN" >&2
109
+ exit 2
110
+ fi
111
+
112
+ if ! command -v python3 >/dev/null 2>&1; then
113
+ echo "Error: python3 is required by resolve-id.sh" >&2
114
+ exit 2
115
+ fi
116
+
117
+ # Capture the board inventory ONCE for this whole run — every segment is
118
+ # resolved against the same snapshot, and we never re-scan per segment.
119
+ BOARD_JSON="$("$BOARD_SCAN")" || {
120
+ echo "Error: board-scan.sh failed" >&2
121
+ exit 2
122
+ }
123
+
124
+ # The parsing/resolution logic is written to a temp .py file rather than
125
+ # piped in via `python3 -` because the board JSON is delivered on stdin —
126
+ # `python3 -` would consume stdin itself as the script source, leaving
127
+ # nothing for the script's own sys.stdin.read() to see.
128
+ PY_SCRIPT="$(mktemp -t resolve-id-XXXXXX.py)"
129
+ trap 'rm -f "$PY_SCRIPT"' EXIT
130
+
131
+ cat > "$PY_SCRIPT" <<'PY'
132
+ import json
133
+ import re
134
+ import sys
135
+
136
+ raw_input = sys.argv[1]
137
+ board_json = sys.stdin.read()
138
+
139
+ try:
140
+ board = json.loads(board_json)
141
+ except Exception as e:
142
+ print(f"Error: could not parse board-scan.sh output as JSON: {e}", file=sys.stderr)
143
+ sys.exit(2)
144
+
145
+ LEVELS = ("epic", "story", "task")
146
+ LEVEL_LETTER = {"epic": "E", "story": "S", "task": "T"}
147
+ LETTER_LEVEL = {"E": "epic", "S": "story", "T": "task"}
148
+
149
+
150
+ def strip_and_upper(segment):
151
+ """Grammar step 1: strip whitespace/underscores/punctuation, uppercase.
152
+ Only [A-Za-z0-9] survives."""
153
+ return re.sub(r"[^A-Za-z0-9]", "", segment).upper()
154
+
155
+
156
+ def tokenize(cleaned):
157
+ """Split the cleaned string into an ordered list of
158
+ (kind, value) tokens where kind is 'digits' or 'letters', preserving
159
+ adjacency (walks left to right, alternating runs)."""
160
+ tokens = []
161
+ for m in re.finditer(r"[0-9]+|[A-Z]+", cleaned):
162
+ val = m.group(0)
163
+ kind = "digits" if val.isdigit() else "letters"
164
+ tokens.append((kind, val))
165
+ return tokens
166
+
167
+
168
+ class RejectError(Exception):
169
+ def __init__(self, reason):
170
+ self.reason = reason
171
+
172
+
173
+ def parse_segment(cleaned):
174
+ """Returns a dict {level: '01'} chunk map (2-digit strings) covering
175
+ only the levels present in this candidate. Raises RejectError on any
176
+ grammar violation."""
177
+ if not cleaned:
178
+ raise RejectError("empty input after stripping whitespace/underscores/punctuation")
179
+
180
+ tokens = tokenize(cleaned)
181
+
182
+ digit_positions = [i for i, (kind, _) in enumerate(tokens) if kind == "digits"]
183
+ if not digit_positions:
184
+ raise RejectError("no ID chunks found in input")
185
+
186
+ # Expand every digit run into its 2-digit chunks (grammar step 2),
187
+ # rejecting any run whose length isn't a multiple of 2. Each run keeps
188
+ # its own ordered list of chunk dicts: {"value": "01", "tag": None}.
189
+ runs = [] # list of {"token_pos": i, "chunks": [chunk_dict, ...]}
190
+ for pos in digit_positions:
191
+ run = tokens[pos][1]
192
+ if len(run) % 2 != 0:
193
+ raise RejectError(
194
+ f"malformed digit run: '{run}' has odd length {len(run)} "
195
+ "(not a multiple of 2)"
196
+ )
197
+ run_chunks = [{"value": run[i:i + 2], "tag": None} for i in range(0, len(run), 2)]
198
+ runs.append({"token_pos": pos, "chunks": run_chunks})
199
+
200
+ run_by_token_pos = {r["token_pos"]: r for r in runs}
201
+
202
+ # Tag assignment (grammar step 3), one pass over LETTER tokens. A tag
203
+ # letter run must be a SINGLE E/S/T letter — a multi-letter run, or a
204
+ # letter that isn't E/S/T, is not a valid tag and is ignored (treated
205
+ # as noise, not a rejection).
206
+ #
207
+ # Adjacency priority when a single tag letter sits BETWEEN two digit
208
+ # runs (e.g. "E01S02" — the 'S' is simultaneously "after 01" and
209
+ # "before 02"): the letter tags the FOLLOWING run's FIRST chunk, never
210
+ # the preceding run's last chunk. This is what makes the negotiated
211
+ # examples parse correctly — "E01s02" reads as E=epic(01), S=story(02),
212
+ # not as a conflict between the two adjacencies of the same 'S'. A
213
+ # trailing letter with NO digit run after it (a true suffix tag, e.g.
214
+ # "0102T" with nothing following the T) falls back to tagging the
215
+ # PRECEDING run's LAST chunk, since there is no following run to prefer.
216
+ for i, (kind, val) in enumerate(tokens):
217
+ if kind != "letters" or len(val) != 1 or val not in LETTER_LEVEL:
218
+ continue
219
+ level = LETTER_LEVEL[val]
220
+
221
+ target_chunk = None
222
+ if i + 1 < len(tokens) and tokens[i + 1][0] == "digits":
223
+ target_chunk = run_by_token_pos[i + 1]["chunks"][0]
224
+ elif i - 1 >= 0 and tokens[i - 1][0] == "digits":
225
+ target_chunk = run_by_token_pos[i - 1]["chunks"][-1]
226
+
227
+ if target_chunk is None:
228
+ continue
229
+
230
+ if target_chunk["tag"] is not None and target_chunk["tag"] != level:
231
+ raise RejectError(
232
+ f"conflicting tags around chunk '{target_chunk['value']}': "
233
+ f"adjacent to both {LEVEL_LETTER[target_chunk['tag']]} and {val}"
234
+ )
235
+ target_chunk["tag"] = level
236
+
237
+ chunks = [c for r in runs for c in r["chunks"]]
238
+
239
+ # Assign tagged chunks to their levels, rejecting duplicate tags for the
240
+ # same level (grammar step 3/4) — e.g. two separate digit runs both
241
+ # tagged 'E'.
242
+ result = {}
243
+ tagged_chunks = [c for c in chunks if c["tag"] is not None]
244
+ untagged_chunks = [c for c in chunks if c["tag"] is None]
245
+
246
+ for c in tagged_chunks:
247
+ level = c["tag"]
248
+ if level in result:
249
+ raise RejectError(
250
+ f"conflicting tags: level '{LEVEL_LETTER[level]}' tagged more than once"
251
+ )
252
+ result[level] = c["value"]
253
+
254
+ # Untagged chunks fill remaining levels positionally, epic -> story ->
255
+ # task, left to right, skipping levels already claimed by a tag.
256
+ open_levels = [lvl for lvl in LEVELS if lvl not in result]
257
+ if len(untagged_chunks) > len(open_levels):
258
+ raise RejectError(
259
+ f"too many untagged chunks: {len(untagged_chunks)} chunks but "
260
+ f"only {len(open_levels)} open level(s)"
261
+ )
262
+ for c, level in zip(untagged_chunks, open_levels):
263
+ result[level] = c["value"]
264
+
265
+ return result
266
+
267
+
268
+ def candidate_id_fragments(level_map):
269
+ """Given {'epic': '01', 'story': '02'}, produce per-level ID fragments
270
+ ('epic': 'E01', 'story': 'S02', ...) used for board matching."""
271
+ return {lvl: f"{LEVEL_LETTER[lvl]}{val}" for lvl, val in level_map.items()}
272
+
273
+
274
+ def item_levels(item_id):
275
+ """Split a board id like 'E01_S02_T03' into {'epic': 'E01', ...}."""
276
+ parts = item_id.split("_")
277
+ levels = {}
278
+ if len(parts) >= 1 and parts[0].startswith("E"):
279
+ levels["epic"] = parts[0]
280
+ if len(parts) >= 2 and parts[1].startswith("S"):
281
+ levels["story"] = parts[1]
282
+ if len(parts) >= 3 and parts[2].startswith("T"):
283
+ levels["task"] = parts[2]
284
+ return levels
285
+
286
+
287
+ def resolve_against_board(level_map, board):
288
+ """Resolve a parsed level_map against the board inventory. Returns
289
+ (resolved_id, None) on unique success, or (None, reason) on failure.
290
+
291
+ The candidate's TYPE is the deepest level present in level_map (e.g.
292
+ {epic, story} targets a story, {epic, story, task} targets a task,
293
+ {story} alone still targets a story — just an under-specified one).
294
+ Restricting the match to items of exactly that `type` (the field
295
+ board-scan.sh already emits) is what keeps a story-level candidate
296
+ like "0103" (epic=01, story=03, no task) resolving to the STORY
297
+ 'E01_S03' itself rather than colliding with every task underneath it —
298
+ without this restriction, a levels-only filter would also match
299
+ 'E01_S03_T01', 'E01_S03_T02', etc., since they share the same
300
+ epic/story numbers.
301
+
302
+ For a genuinely partial candidate missing shallower levels (e.g. only
303
+ a story chunk with no epic tag), the same type-restricted filter
304
+ naturally requires uniqueness ACROSS epics, which is exactly the
305
+ disambiguation guarantee the grammar spec requires."""
306
+ if not level_map:
307
+ return None, "no ID chunks found in input"
308
+
309
+ frag = candidate_id_fragments(level_map)
310
+ deepest = max(level_map.keys(), key=LEVELS.index)
311
+
312
+ def matches(item):
313
+ if item.get("type") != deepest:
314
+ return False
315
+ levels = item_levels(item.get("id", ""))
316
+ for lvl, want in frag.items():
317
+ if levels.get(lvl) != want:
318
+ return False
319
+ return True
320
+
321
+ matches_list = [item for item in board if matches(item)]
322
+
323
+ partial_desc = ", ".join(f"{lvl}={v}" for lvl, v in frag.items())
324
+
325
+ if len(matches_list) == 1:
326
+ return matches_list[0]["id"], None
327
+ if len(matches_list) == 0:
328
+ return None, f"not found on board: {partial_desc}"
329
+
330
+ ids = sorted(m["id"] for m in matches_list)
331
+ preview = ", ".join(ids[:8]) + (", ..." if len(ids) > 8 else "")
332
+ return None, (
333
+ f"ambiguous partial ID '{partial_desc}': matches {len(matches_list)} "
334
+ f"candidates ({preview}) - disambiguation required"
335
+ )
336
+
337
+
338
+ results = []
339
+ any_rejected = False
340
+
341
+ segments = raw_input.split(",") if raw_input.strip() != "" else []
342
+
343
+ for raw_segment in segments:
344
+ entry = {"input": raw_segment, "status": None, "resolved_id": None, "reason": None}
345
+ try:
346
+ cleaned = strip_and_upper(raw_segment)
347
+ level_map = parse_segment(cleaned)
348
+ resolved_id, reason = resolve_against_board(level_map, board)
349
+ if resolved_id is not None:
350
+ entry["status"] = "resolved"
351
+ entry["resolved_id"] = resolved_id
352
+ else:
353
+ entry["status"] = "rejected"
354
+ entry["reason"] = reason
355
+ any_rejected = True
356
+ except RejectError as e:
357
+ entry["status"] = "rejected"
358
+ entry["reason"] = e.reason
359
+ any_rejected = True
360
+ results.append(entry)
361
+
362
+ print(json.dumps(results, indent=2))
363
+ sys.exit(1 if any_rejected else 0)
364
+ PY
365
+
366
+ python3 "$PY_SCRIPT" "$RAW_INPUT" <<< "$BOARD_JSON"
367
+ exit $?
@@ -0,0 +1,81 @@
1
+ ---
2
+ name: jenga-permission-level
3
+ description: Report or switch the current session's 5-tier permission level (Locked/Guarded/Standard/Elevated/Unrestricted) without hand-editing settings.json.
4
+ keywords:
5
+ - "permission level"
6
+ - "permission"
7
+ - "session level"
8
+ - "jenga permission level"
9
+ examples:
10
+ - "what permission level am I at?"
11
+ - "switch to permission level 4"
12
+ - "set permission level to elevated"
13
+ - "lock down permissions"
14
+ - "unlock full permissions for this session"
15
+ ---
16
+
17
+ # jenga-permission-level — Report or Switch Session Permission Level
18
+
19
+ ## Purpose
20
+
21
+ `/jenga-permission-level [1-5]` reports or switches the current session's permission level:
22
+
23
+ | Level | Name | Notes |
24
+ |-------|------|-------|
25
+ | 1 | Locked | |
26
+ | 2 | Guarded | Conceptual default — always reported as `Default: 2`, no matter the current session level. |
27
+ | 3 | Standard | |
28
+ | 4 | Elevated | |
29
+ | 5 | Unrestricted | |
30
+
31
+ `defaultMode` stays `acceptEdits` at every level; only the `permissions` block changes. Levels are backed by template files at `templates/permission-levels/level-<n>-<name>.json`, e.g. `templates/permission-levels/level-4-elevated.json` (naming convention: `<name>` is the lowercase level name from the table above — Locked/Guarded/Standard/Elevated/Unrestricted). Those templates are owned by story E33_S01 and are the input the switch script (see below) copies from.
32
+
33
+ ---
34
+
35
+ ## Instructions
36
+
37
+ ### 1. No argument — report status
38
+
39
+ Read `.jenga-permission-level.json` at the repo root. Treat a missing file, or a missing/unparseable `session_level` field, as `session_level: 2`.
40
+
41
+ Run:
42
+
43
+ ```bash
44
+ n=$(jq -r '.session_level // 2' .jenga-permission-level.json 2>/dev/null || echo 2)
45
+ echo "Default: 2 / Session: $n"
46
+ ```
47
+
48
+ Print exactly `Default: 2 / Session: <n>` (e.g. `Default: 2 / Session: 4`). Do not write any files in this path.
49
+
50
+ ### 2. Argument `<n>` provided — validate
51
+
52
+ Before doing anything else, validate the argument:
53
+
54
+ - It must match `^[1-5]$` (a single digit, 1 through 5).
55
+ - If it does not match — including non-numeric input, decimals, negative numbers, `0`, or `6` and above — **stop immediately**, print a clear error (e.g. `Error: permission level must be an integer between 1 and 5, got '<input>'`), and make **no file changes**. Do not invoke the switch script.
56
+
57
+ ### 3. Argument `<n>` valid — delegate to the switch script
58
+
59
+ Do not inline the copy/update logic here — per this repo's scripts-over-inline-logic convention, the switch is owned entirely by a dedicated script:
60
+
61
+ ```bash
62
+ bash scripts/jenga-permission-level-switch.sh <n>
63
+ ```
64
+
65
+ This script (created by a separate task, E33_S02_T02) is responsible for:
66
+ - Copying `templates/permission-levels/level-<n>-<name>.json` over both `.claude/settings.json` and `.agents/settings.json` as a **whole-file overwrite** — not a merge of the `permissions` block alone. Two fields vary by level and are expected to change on a switch: `permissions.deny` and `autoMode.allow`. All 5 templates carry byte-identical `defaultMode`, `env`, `hooks`, and `permissions.allow`; that invariant is what keeps the overwrite safe. Two consequences follow: any top-level key present in a destination file but absent from the templates is **silently dropped** by a switch, and any future per-level difference in `defaultMode`/`env`/`hooks` would take effect without warning. Keep the templates in sync with root `settings.json` for everything except those two varying fields.
67
+ - Creating or updating `.jenga-permission-level.json` at the repo root to `{"session_level": <n>}`.
68
+
69
+ Relay the script's stdout to the user and honor its exit code:
70
+ - Exit code `0` — report success, e.g. `Switched to level <n> (<name>).`
71
+ - Non-zero exit code — report the script's error output verbatim and treat the switch as failed; do not claim success.
72
+
73
+ If `scripts/jenga-permission-level-switch.sh` does not exist yet (e.g. E33_S02_T02 has not landed), report that clearly as a missing dependency rather than attempting to reimplement its logic inline.
74
+
75
+ ### Session End
76
+
77
+ When this skill's session concludes, emit the following signal on its own line so the Jenga Router clears the active session:
78
+
79
+ ```
80
+ [JENGA:SESSION_END:jenga-permission-level]
81
+ ```
@@ -23,7 +23,7 @@ metadata:
23
23
 
24
24
  3. **Determine the next action**:
25
25
  - If there are tasks in `Pending` or `In Progress` status that have not yet been assigned to the developer, identify them.
26
- - If outstanding tasks are ready for implementation, write a session handoff to `project/queue/.session_handoff.json` with `"status": "planning_complete"` so that `on_session_end.sh` routes them to the developer queue.
26
+ - If outstanding tasks are ready for implementation, write a session handoff to `project/queue/handoffs/scrum-master-<session_id>-<task_id>.json` (per-session path — see `templates/SCRUM_BOARD_SCHEMA.md`'s `handoffs/` section; use the first task ID, or `batch` if several) with `"status": "planning_complete"` so that `on_session_end.sh` routes them to the developer queue.
27
27
  - If all tasks are complete, check for epic/story rollup and update board statuses accordingly.
28
28
 
29
29
  4. **Report** a clear summary to the user: what is done, what is in progress, what is next — and which agent will handle it.
@@ -77,7 +77,7 @@ If config or env validation fails, the skill exits with code `4` and does not co
77
77
  | `/publish setup` | Prepare or refresh target configuration | `skills/publish/scripts/setup_wizard.sh` | Supported types: `mobile-ios`, `npm`, `npm-ci`, `droplet` |
78
78
  | `/publish deploy` | Run the full 11-step deploy orchestration | `skills/publish/scripts/publish_deploy.sh` | Dispatches to the adapter for the target's `type` (`mobile-ios`, `npm`, `npm-ci`, or `droplet`); `--dry-run` is honoured end-to-end |
79
79
  | `/publish history` | Read the canonical publish ledger | `skills/publish/scripts/show_history.sh` | Target-agnostic; filter by `--target <name>` |
80
- | `/publish release-notes` | Generate release notes without publishing | `skills/publish/scripts/generate_release_notes.sh` | Target-agnostic |
80
+ | `/publish release-notes` | Merge new release notes into the standing `CHANGELOG.md` (or a standalone draft via `--output`) without publishing | `skills/publish/scripts/generate_release_notes.sh` | Target-agnostic |
81
81
 
82
82
  ## Quality Gate Policy
83
83
 
@@ -131,7 +131,7 @@ Example invocations:
131
131
  ### `/publish deploy`
132
132
 
133
133
  ```text
134
- /publish deploy [--target <name>] [--config <path>] [--yes] [--dry-run] [--minor | --major] [--release-notes <path>]
134
+ /publish deploy [--target <name>] [--config <path>] [--yes] [--dry-run] [--minor | --major] [--notes-file <path>]
135
135
  ```
136
136
 
137
137
  Implementation: `bash skills/publish/scripts/publish_deploy.sh [flags...]`
@@ -142,12 +142,12 @@ Deploy flow:
142
142
  3. Validate target environment variables
143
143
  4. Print a best-effort scrum-board summary since the last publish tag
144
144
  5. Run pre-deploy gates
145
- 6. Generate a release-note draft and review it unless `--yes` is set
145
+ 6. Update the standing `CHANGELOG.md`'s `[Unreleased]` section with new entries since the last publish tag (or use the caller-supplied file instead, when release notes are supplied via the bypass flag), and review it unless `--yes` is set
146
146
  7. Suggest and confirm the semver bump (`patch` by default in non-interactive mode unless `--minor` or `--major` is passed)
147
147
  8. Print final confirmation: `Deploy v<x.y.z> to <target>? [y/N]`
148
148
  9. Execute the adapter pipeline for the target's `type` (`ios_pipeline.sh` for `mobile-ios`, `npm_pipeline.sh` for `npm`, `npm_ci_pipeline.sh` for `npm-ci`, `droplet_pipeline.sh` for `droplet`)
149
149
  10. Run post-deploy gates and downgrade the ledger state to `partial` on failure
150
- 11. Append the publish ledger entry, create the git tag (unless `--dry-run`), and print post-deploy manual steps
150
+ 11. Finalize `CHANGELOG.md` — stamp `[Unreleased]` to a versioned, dated entry and open a fresh empty `[Unreleased]` above it (skipped on `--dry-run`, and skipped when release notes were supplied via the bypass flag, since there is then no standing-file `[Unreleased]` section to finalize) — then append the publish ledger entry, create the git tag (unless `--dry-run`), and print post-deploy manual steps
151
151
 
152
152
  Non-interactive rule: when `--yes` is used, deploy must not auto-run setup after a failure. It exits `4` and surfaces the missing fields.
153
153
 
@@ -188,8 +188,11 @@ Implementation: `bash skills/publish/scripts/generate_release_notes.sh [--target
188
188
 
189
189
  Release-note rules:
190
190
  - The last publish tag is the highest semver tag on the current branch that also has a matching ledger entry in `project/logs/publish-history.json`.
191
- - If no prior ledger-backed tag exists, the draft includes `> First release — full history included`.
191
+ - **Default target (no `--output`):** the repo-root `CHANGELOG.md` — created from `templates/CHANGELOG_TEMPLATE.md` first if it doesn't exist yet (backward-compat for projects scaffolded before this convention). New Features/Bug Fixes/Other/Completed-task entries since the last publish tag are appended under the existing `## [Unreleased]` heading's subsections; everything else in the file (prior versioned entries, manual edits) is preserved. Entries already present are deduped (matched by commit short-sha or task id), so re-running with no new commits produces zero diff.
192
+ - **`--output <path>`:** writes a standalone, disposable draft to that path instead (the pre-E36 behavior) — `CHANGELOG.md` is not touched in this mode.
193
+ - If no prior ledger-backed tag exists, a `--output` draft includes `> First release — full history included`; the standing `CHANGELOG.md` merge mode has no equivalent banner (it just merges full history into `[Unreleased]` like any other run).
192
194
  - Scrum-board enrichment is best-effort only; missing or unreadable board data never fails the command.
195
+ - **`.publicignore` filtering:** when a repo-root `.publicignore` exists (the blocklist `/mirror-public` also reads), a candidate commit — or a completed task's associated commit(s), matched via the `task(E##_S##_T##):` commit-message convention — is dropped entirely when every changed file it touches is covered by that blocklist. A commit touching a mix of blocked and unblocked files is still logged normally; only full coverage excludes an entry. This exists because `CHANGELOG.md` itself is not blocklisted and ships to the public mirror, so an unfiltered entry referencing a private-only path (e.g. `project/board/`) would leak. Absent `.publicignore`, this is a strict no-op — unchanged from pre-E36_S02_T02 behavior. Matching reuses `skills/mirror-public/scripts/mirror.sh`'s `rsync --exclude-from` evaluation rather than a separate glob implementation, so a path classified "blocked" by `/mirror-public --dry-run` is classified "blocked" here too.
193
196
 
194
197
  ## Ledger & Tagging
195
198
 
@@ -18,7 +18,7 @@ Every `/publish` invocation must perform these steps before command-specific wor
18
18
  | Select deploy target | `--target <name>` | `PUBLISH_TARGET` | Yes | Flag takes precedence over env var |
19
19
  | Skip confirmation prompts | `--yes` | `PUBLISH_YES` | Yes | Env var truthy values: `1`, `true`, `yes`, `on` |
20
20
  | Override config path | `--config <path>` | — | No | Defaults to `project/configs/publish.json` |
21
- | Reuse prepared release notes | `--release-notes <path>` | — | No | Reserved for later release-note flow |
21
+ | Reuse prepared release notes | `--notes-file <path>` | — | No | Live: bypasses generation entirely and uses the supplied file directly, instead of the standing `CHANGELOG.md` flow |
22
22
  | Dry-run execution | `--dry-run` | `PUBLISH_DRY_RUN` | No | Prints allowlisted `xcodebuild` / `xcrun` commands without executing them |
23
23
 
24
24
  ## Required contract
@@ -58,7 +58,7 @@ These run only after the mandatory global gates succeed.
58
58
  These inputs remain optional in non-interactive mode:
59
59
 
60
60
  - `--config <path>` when the default config exists
61
- - `--release-notes <path>`
61
+ - `--notes-file <path>`
62
62
  - `--from-tag` and `--to-ref` on `/publish release-notes`
63
63
 
64
64
  ## Exit codes
@@ -8,7 +8,7 @@ The `/publish` skill is initiated by the operator running the CLI, but each acti
8
8
  | `/publish deploy --target staging` | Initiates | May run as part of test cycle | Never directly |
9
9
  | `/publish deploy --target production` | Initiates with explicit confirmation | Reviews ledger entry post-deploy | Reviews `/status` only |
10
10
  | `/publish history` | Reads for context | Reads for test baseline | Reads in `/status` review |
11
- | `/publish release-notes` | Generates and reviews draft | May read draft for test context | Reviews content for sprint summary |
11
+ | `/publish release-notes` | Updates and reviews CHANGELOG.md `[Unreleased]` | May read CHANGELOG.md for test context | Reviews content for sprint summary |
12
12
  | Gate failure resolution | Owns rework | Validates rework | Escalates if blocked |
13
13
  | Ledger entry disputes | Never modifies | Never modifies | Flags as rapport; human resolves |
14
14
 
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ source "$SCRIPT_DIR/publish_common.sh"
6
+
7
+ usage() {
8
+ cat <<'USAGE'
9
+ Usage: finalize_changelog.sh <version> [<changelog_path>]
10
+
11
+ Stamps the standing CHANGELOG.md's `## [Unreleased]` heading into a dated,
12
+ versioned heading (`## [<version>] — <date>`) and inserts a fresh, empty
13
+ `## [Unreleased]` section directly above it, matching
14
+ templates/CHANGELOG_TEMPLATE.md's subsection structure
15
+ (### Features / ### Bug Fixes / ### Other).
16
+
17
+ <changelog_path> defaults to <repo-root>/CHANGELOG.md.
18
+
19
+ This is a one-shot finalize step, meant to run once per confirmed deploy,
20
+ after the semver bump is confirmed and before the release is recorded to
21
+ the publish ledger. It does not merge or classify entries — that is
22
+ generate_release_notes.sh's job, run earlier in the deploy pipeline.
23
+ USAGE
24
+ }
25
+
26
+ [[ $# -ge 1 ]] || { usage >&2; exit 1; }
27
+ VERSION_RAW="$1"
28
+ CHANGELOG_PATH="${2:-$PUBLISH_REPO_ROOT/CHANGELOG.md}"
29
+
30
+ VERSION="$(publish_normalize_version "$VERSION_RAW")" || {
31
+ echo "finalize_changelog.sh: invalid version '$VERSION_RAW'." >&2
32
+ exit 1
33
+ }
34
+
35
+ [[ -f "$CHANGELOG_PATH" ]] || {
36
+ echo "finalize_changelog.sh: changelog not found at '$CHANGELOG_PATH'." >&2
37
+ exit 1
38
+ }
39
+
40
+ TODAY="$(date -u +"%Y-%m-%d")"
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # Plain indexed-array line editing, matching generate_release_notes.sh's
44
+ # bash 3.2 convention (no mapfile/declare -A/local -n; the stock macOS
45
+ # default shell lacks all three). Zero-element array expansions are guarded
46
+ # with explicit count checks before use under `set -u`, same as that file.
47
+ # ---------------------------------------------------------------------------
48
+
49
+ CL_LINES=()
50
+
51
+ _cl_load() {
52
+ CL_LINES=()
53
+ local line
54
+ while IFS= read -r line || [[ -n "$line" ]]; do
55
+ CL_LINES+=("$line")
56
+ done < "$1"
57
+ }
58
+
59
+ _cl_save() {
60
+ if (( ${#CL_LINES[@]} > 0 )); then
61
+ printf '%s\n' "${CL_LINES[@]}" > "$1"
62
+ else
63
+ : > "$1"
64
+ fi
65
+ }
66
+
67
+ _cl_load "$CHANGELOG_PATH"
68
+
69
+ UNRELEASED_INDEX=-1
70
+ CL_LINE_COUNT=${#CL_LINES[@]}
71
+ i=0
72
+ while (( i < CL_LINE_COUNT )); do
73
+ if [[ "${CL_LINES[$i]}" == "## [Unreleased]" ]]; then
74
+ UNRELEASED_INDEX=$i
75
+ break
76
+ fi
77
+ i=$((i + 1))
78
+ done
79
+
80
+ if (( UNRELEASED_INDEX < 0 )); then
81
+ echo "finalize_changelog.sh: '## [Unreleased]' section not found in $CHANGELOG_PATH — nothing to finalize." >&2
82
+ exit 1
83
+ fi
84
+
85
+ # Stamp the located heading in place.
86
+ CL_LINES[UNRELEASED_INDEX]="## [$VERSION] — $TODAY"
87
+
88
+ BEFORE=()
89
+ AFTER=()
90
+ if (( UNRELEASED_INDEX > 0 )); then
91
+ BEFORE=("${CL_LINES[@]:0:UNRELEASED_INDEX}")
92
+ fi
93
+ AFTER=("${CL_LINES[@]:UNRELEASED_INDEX}")
94
+
95
+ FRESH_UNRELEASED=(
96
+ "## [Unreleased]"
97
+ ""
98
+ "### Features"
99
+ ""
100
+ "### Bug Fixes"
101
+ ""
102
+ "### Other"
103
+ ""
104
+ )
105
+
106
+ CL_LINES=()
107
+ if (( ${#BEFORE[@]} > 0 )); then
108
+ CL_LINES+=("${BEFORE[@]}")
109
+ fi
110
+ CL_LINES+=("${FRESH_UNRELEASED[@]}")
111
+ CL_LINES+=("${AFTER[@]}")
112
+
113
+ _cl_save "$CHANGELOG_PATH"
114
+
115
+ printf '🏷️ CHANGELOG.md finalized: [Unreleased] stamped as [%s] — %s\n' "$VERSION" "$TODAY"