@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.
Files changed (100) hide show
  1. package/README.md +97 -91
  2. package/agents/developer.md +26 -7
  3. package/agents/scrum-master.md +57 -22
  4. package/agents/tester.md +68 -4
  5. package/hooks/on_session_end.sh +40 -1
  6. package/lib/generate-agent-context.js +18 -1
  7. package/lib/generate-copilot-instructions.js +18 -1
  8. package/lib/generate-skill-allow-list.js +191 -0
  9. package/lib/skill-allow-list.json +43 -0
  10. package/package.json +35 -20
  11. package/scripts/apply-j-prefix.sh +230 -0
  12. package/scripts/consume-context-digest.sh +103 -0
  13. package/scripts/postinstall.js +25 -0
  14. package/scripts/sweep-stale-context-digests.sh +132 -0
  15. package/scripts/validate-board.sh +5 -0
  16. package/scripts/write-context-digest.sh +230 -0
  17. package/skills/brainstorm/SKILL.md +1 -1
  18. package/skills/btw/SKILL.md +1 -1
  19. package/skills/clearify/SKILL.md +1 -1
  20. package/skills/close-story/SKILL.md +78 -6
  21. package/skills/close-story/scripts/check-privatized.sh +345 -0
  22. package/skills/commit/SKILL.md +12 -2
  23. package/skills/continue/SKILL.md +1 -1
  24. package/skills/deep-dive/SKILL.md +1 -1
  25. package/skills/dev-done/SKILL.md +46 -0
  26. package/skills/dev-done/scripts/classify-commit-outcome.sh +114 -0
  27. package/skills/distribute/SKILL.md +1 -1
  28. package/skills/do/SKILL.md +100 -10
  29. package/skills/doc/README.md +155 -0
  30. package/skills/doc/SKILL.md +43 -13
  31. package/skills/doc/authoring-notes.md +72 -0
  32. package/skills/doc/scripts/resolve_last_update.py +149 -0
  33. package/skills/doc-sync/SKILL.md +1 -1
  34. package/skills/dooo/SKILL.md +1 -1
  35. package/skills/error/SKILL.md +1 -1
  36. package/skills/evaluate/SKILL.md +1 -1
  37. package/skills/examplify/SKILL.md +1 -1
  38. package/skills/help/SKILL.md +1 -1
  39. package/skills/idea/SKILL.md +1 -1
  40. package/skills/improve/SKILL.md +1 -1
  41. package/skills/init/SKILL.md +8 -7
  42. package/skills/init/assets/scope-thresholds_template.json +7 -0
  43. package/skills/init/scripts/init.sh +6 -0
  44. package/skills/j-init/SKILL.md +168 -0
  45. package/skills/j-init/assets/.gitignore_template +15 -0
  46. package/skills/j-init/assets/PROJECT_SUMMARY_template.md +13 -0
  47. package/skills/j-init/assets/directory_structure.txt +14 -0
  48. package/skills/j-init/assets/scope-thresholds_template.json +7 -0
  49. package/skills/j-init/assets/strategy_stub_template.md +38 -0
  50. package/skills/j-init/assets/test-config_template.json +4 -0
  51. package/skills/j-init/assets/workflow_template.json +30 -0
  52. package/skills/j-init/scripts/apply-project-visibility.sh +176 -0
  53. package/skills/j-init/scripts/detect-existing-codebase.sh +166 -0
  54. package/skills/j-init/scripts/init.sh +116 -0
  55. package/skills/jbp/SKILL.md +1 -1
  56. package/skills/jenga/SKILL.md +1 -1
  57. package/skills/jenga/scripts/render-confirmation.sh +55 -18
  58. package/skills/jenga-permission-level/SKILL.md +1 -1
  59. package/skills/lgtm/SKILL.md +1 -1
  60. package/skills/pi-plan/SKILL.md +1 -1
  61. package/skills/proceed/SKILL.md +1 -1
  62. package/skills/publish/SKILL.md +67 -1
  63. package/skills/publish/adapters/npm-ci.md +60 -4
  64. package/skills/publish/adapters/npm.md +18 -0
  65. package/skills/publish/assets/ci-contract.md +27 -0
  66. package/skills/publish/assets/publish.example.json +27 -0
  67. package/skills/publish/schemas/publish.schema.json +20 -0
  68. package/skills/publish/scripts/npm_ci_pipeline.sh +50 -1
  69. package/skills/publish/scripts/npm_stage_inspect.sh +829 -0
  70. package/skills/publish/scripts/npm_stage_pipeline.sh +427 -0
  71. package/skills/publish/scripts/publish_common.sh +16 -0
  72. package/skills/publish/scripts/show_history.sh +12 -5
  73. package/skills/publish/scripts/validate_npm_stage_env.sh +184 -0
  74. package/skills/publish/scripts/write_ledger_entry.sh +92 -2
  75. package/skills/reconcile/SKILL.md +122 -12
  76. package/skills/reconcile/assets/report_format.md +17 -0
  77. package/skills/reconcile/scripts/resolve-reconcile-scope.sh +489 -0
  78. package/skills/reconcile-origin/SKILL.md +1 -1
  79. package/skills/redo/SKILL.md +1 -1
  80. package/skills/skillify/SKILL.md +1 -1
  81. package/skills/spinoff/SKILL.md +1 -1
  82. package/skills/status/SKILL.md +1 -1
  83. package/skills/todo/SKILL.md +40 -3
  84. package/skills/todo/scripts/add_trivial_task.sh +216 -0
  85. package/skills/todo/scripts/update_story_tasks.py +87 -0
  86. package/skills/uncharted/SKILL.md +201 -22
  87. package/skills/uncharted/scripts/directory-triage.sh +342 -0
  88. package/skills/uncharted/scripts/elicitation-state.sh +457 -0
  89. package/skills/wtf/SKILL.md +1 -1
  90. package/templates/SCRUM_BOARD_SCHEMA.md +90 -2
  91. package/templates/agent-context.md.tpl +47 -12
  92. package/templates/copilot-instructions.md.tpl +36 -9
  93. package/mcp/router/README.md +0 -19
  94. package/mcp/router/embedder.js +0 -23
  95. package/mcp/router/index.js +0 -204
  96. package/mcp/router/matcher.js +0 -87
  97. package/mcp/router/package-lock.json +0 -1048
  98. package/mcp/router/package.json +0 -11
  99. package/mcp/router/skill-index.js +0 -104
  100. package/skills/route/SKILL.md +0 -180
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env bash
2
+ # ---------------------------------------------------------------------------
3
+ # skills/init/scripts/detect-existing-codebase.sh
4
+ #
5
+ # Deterministic front half of the /init "detect existing project state" step.
6
+ # /init currently always scaffolds as though the target directory were empty.
7
+ # That is wrong whenever a directory already carries a Jenga scaffold, or
8
+ # already carries substantial application code that predates Jenga entirely
9
+ # -- in the second case, scaffolding fresh produces an empty board and a stub
10
+ # PROJECT_SUMMARY.md that actively misrepresent the project.
11
+ #
12
+ # This script answers exactly one question -- "what kind of directory is
13
+ # this?" -- and nothing else. It never scaffolds, never prompts, never runs
14
+ # /uncharted, and never modifies anything on disk. Deciding what to DO with
15
+ # the verdict is agent judgement and lives in skills/init/SKILL.md.
16
+ #
17
+ # ---------------------------------------------------------------------------
18
+ # VERDICTS -- exactly one is printed on stdout, nothing else
19
+ # ---------------------------------------------------------------------------
20
+ # empty The target is empty, or contains only boilerplate
21
+ # that does not count as "there is a project here":
22
+ # .git (the directory itself, pruned entirely -- not
23
+ # merely excluded by name), .gitignore, and top-level
24
+ # README*/LICENSE*/LICENCE* files (case-insensitive),
25
+ # plus anything git itself reports as ignored. A repo
26
+ # containing only a README is empty for this purpose.
27
+ # already-scaffolded The target already carries a Jenga scaffold: any one
28
+ # of project/board/, project/PROJECT_SUMMARY.md, or
29
+ # project/configs/workflow.json is present. Checked
30
+ # BEFORE the emptiness scan, so a scaffolded project
31
+ # that also happens to look sparse is never misreported
32
+ # as "empty".
33
+ # existing-codebase The target has real content (source files, configs,
34
+ # docs beyond the boilerplate list above, etc.) and is
35
+ # not already Jenga-scaffolded. This is the case /init
36
+ # currently mishandles: scaffolding fresh here silently
37
+ # discards the fact that pre-existing code exists.
38
+ #
39
+ # ---------------------------------------------------------------------------
40
+ # NOTES ON GITIGNORE HANDLING
41
+ # ---------------------------------------------------------------------------
42
+ # /init runs this check BEFORE `git init` in the fresh-scaffold path, so the
43
+ # target directory is very often not a git work tree yet. Gitignore-based
44
+ # filtering is therefore best-effort: it only runs when the target already
45
+ # has a `.git` directory AND `git` is on PATH. A standalone `.gitignore` file
46
+ # with no repository behind it still counts as boilerplate via the name-based
47
+ # exclusion below -- it just cannot hide any OTHER file, because there is no
48
+ # git to ask "is this ignored".
49
+ #
50
+ # README/LICENSE exclusion applies to TOP-LEVEL entries only. A directory
51
+ # whose only content is a buried `docs/README.md` two levels down is real
52
+ # structure, not boilerplate, and must not be reported as empty.
53
+ #
54
+ # ---------------------------------------------------------------------------
55
+ # Usage
56
+ # ---------------------------------------------------------------------------
57
+ # detect-existing-codebase.sh [target-dir]
58
+ #
59
+ # target-dir Directory to classify. Default: current directory.
60
+ #
61
+ # Options:
62
+ # -h, --help Show this help and exit 0.
63
+ #
64
+ # Exit codes:
65
+ # 0 A verdict was printed on stdout.
66
+ # 1 Usage error (unknown option, more than one positional argument).
67
+ # 2 target-dir does not exist or is not a directory.
68
+ #
69
+ # Requires: bash, find. git is used opportunistically for gitignore
70
+ # filtering when the target is already a git work tree; its absence never
71
+ # causes a failure.
72
+ # ---------------------------------------------------------------------------
73
+
74
+ set -euo pipefail
75
+
76
+ SELF="$(basename "$0")"
77
+
78
+ usage() {
79
+ sed -n '/^# Usage$/,/^# Requires:/p' "$0" | sed -e 's/^# \{0,1\}//' -e '/^-\{10,\}$/d'
80
+ }
81
+
82
+ die_usage() {
83
+ printf '%s: error: %s\n' "$SELF" "$*" >&2
84
+ echo >&2
85
+ usage >&2
86
+ exit 1
87
+ }
88
+
89
+ TARGET_DIR="."
90
+ HAVE_TARGET=0
91
+
92
+ while [ "$#" -gt 0 ]; do
93
+ case "$1" in
94
+ -h|--help) usage; exit 0 ;;
95
+ --)
96
+ shift
97
+ [ "$#" -le 1 ] || die_usage "at most one target directory is allowed"
98
+ if [ "$#" -eq 1 ]; then TARGET_DIR="$1"; HAVE_TARGET=1; fi
99
+ shift $#
100
+ ;;
101
+ -*)
102
+ die_usage "unknown option \"$1\"" ;;
103
+ *)
104
+ [ "$HAVE_TARGET" -eq 0 ] || die_usage "at most one target directory is allowed (got \"$TARGET_DIR\" and \"$1\")"
105
+ TARGET_DIR="$1"; HAVE_TARGET=1; shift ;;
106
+ esac
107
+ done
108
+
109
+ [ -d "$TARGET_DIR" ] || { printf '%s: error: not a directory: %s\n' "$SELF" "$TARGET_DIR" >&2; exit 2; }
110
+
111
+ TARGET_DIR="$(cd -- "$TARGET_DIR" && pwd -P)"
112
+
113
+ # --- already-scaffolded: checked first, independent of emptiness -----------
114
+ if [ -d "$TARGET_DIR/project/board" ] \
115
+ || [ -f "$TARGET_DIR/project/PROJECT_SUMMARY.md" ] \
116
+ || [ -f "$TARGET_DIR/project/configs/workflow.json" ]; then
117
+ echo "already-scaffolded"
118
+ exit 0
119
+ fi
120
+
121
+ # --- enumerate candidate files, pruning .git entirely -----------------------
122
+ ALL_FILES=$(mktemp "${TMPDIR:-/tmp}/detect-existing-codebase.XXXXXX")
123
+ IGNORED_FILES=$(mktemp "${TMPDIR:-/tmp}/detect-existing-codebase-ignored.XXXXXX")
124
+ cleanup() { rm -f "$ALL_FILES" "$IGNORED_FILES"; }
125
+ trap cleanup EXIT
126
+
127
+ find "$TARGET_DIR" -path "$TARGET_DIR/.git" -prune -o -type f -print > "$ALL_FILES"
128
+
129
+ # --- best-effort gitignore filtering ----------------------------------------
130
+ : > "$IGNORED_FILES"
131
+ if [ -d "$TARGET_DIR/.git" ] && command -v git >/dev/null 2>&1; then
132
+ git -C "$TARGET_DIR" check-ignore --stdin < "$ALL_FILES" > "$IGNORED_FILES" 2>/dev/null || true
133
+ fi
134
+
135
+ # --- classify each candidate -------------------------------------------------
136
+ SOURCE_FOUND=0
137
+ while IFS= read -r f; do
138
+ [ -n "$f" ] || continue
139
+
140
+ if [ -s "$IGNORED_FILES" ] && grep -Fxq -- "$f" "$IGNORED_FILES"; then
141
+ continue
142
+ fi
143
+
144
+ rel="${f#"$TARGET_DIR"/}"
145
+ base="$(basename -- "$f")"
146
+ dir_rel="$(dirname -- "$rel")"
147
+
148
+ if [ "$dir_rel" = "." ]; then
149
+ if [ "$base" = ".gitignore" ]; then
150
+ continue
151
+ fi
152
+ base_upper="$(printf '%s' "$base" | tr '[:lower:]' '[:upper:]')"
153
+ case "$base_upper" in
154
+ README*|LICENSE*|LICENCE*) continue ;;
155
+ esac
156
+ fi
157
+
158
+ SOURCE_FOUND=1
159
+ break
160
+ done < "$ALL_FILES"
161
+
162
+ if [ "$SOURCE_FOUND" -eq 1 ]; then
163
+ echo "existing-codebase"
164
+ else
165
+ echo "empty"
166
+ fi
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ ASSETS_DIR="$SCRIPT_DIR/../assets"
6
+ VISIBILITY_SCRIPT="$SCRIPT_DIR/apply-project-visibility.sh"
7
+
8
+ # ─── Resolve the package root that owns templates/ and lib/ ──────────────────
9
+ # postinstall.js mirrors only skills/ and agents/ into .claude/ and .agents/ —
10
+ # templates/ and lib/ are never copied there, so a script running from a
11
+ # mirrored copy (.claude/skills/init/scripts/ or .agents/skills/init/scripts/)
12
+ # cannot reach its siblings via a fixed ../../../ climb the way it can in this
13
+ # monorepo checkout, where init.sh actually lives at skills/init/scripts/ with
14
+ # templates/ and lib/ three levels up. Consumers instead have them inside the
15
+ # installed npm package.
16
+ if [[ -d "$SCRIPT_DIR/../../../templates" ]]; then
17
+ PKG_ROOT="$SCRIPT_DIR/../../.."
18
+ elif [[ -d "$PWD/node_modules/@jenga-ai/agent/templates" ]]; then
19
+ PKG_ROOT="$PWD/node_modules/@jenga-ai/agent"
20
+ else
21
+ echo "Error: could not locate the jenga-agent package root (templates/ not found via monorepo checkout or node_modules/@jenga-ai/agent)." >&2
22
+ exit 1
23
+ fi
24
+
25
+ # ─── 0. Resolve project_files_visibility ─────────────────────────────────────
26
+ # Defaults to `visible` — the only value that touches nothing on disk — so an
27
+ # unattended run can never silently relocate directories or edit .gitignore.
28
+ VISIBILITY="${JENGA_PROJECT_FILES_VISIBILITY:-visible}"
29
+
30
+ while [[ $# -gt 0 ]]; do
31
+ case "$1" in
32
+ --visibility) VISIBILITY="${2:-}"; shift 2 ;;
33
+ --visibility=*) VISIBILITY="${1#*=}"; shift ;;
34
+ *) echo "Unknown argument: $1" >&2
35
+ echo "Usage: $(basename "$0") [--visibility <visible|ignored>]" >&2
36
+ exit 1 ;;
37
+ esac
38
+ done
39
+
40
+ # Validate before scaffolding so a typo cannot leave a half-initialised project.
41
+ bash "$VISIBILITY_SCRIPT" --check-only "$VISIBILITY"
42
+
43
+ # ─── 1. Initialize git repository ────────────────────────────────────────────
44
+ echo "→ Initializing git repository..."
45
+ git init
46
+
47
+ # ─── 2. Create .gitignore ─────────────────────────────────────────────────────
48
+ echo "→ Copying .gitignore from template..."
49
+ cp "$ASSETS_DIR/.gitignore_template" .gitignore
50
+
51
+ # ─── 3. Scaffold directory structure ──────────────────────────────────────────
52
+ echo "→ Scaffolding directory structure..."
53
+ while IFS= read -r dir || [[ -n "$dir" ]]; do
54
+ [[ -z "$dir" || "$dir" == \#* ]] && continue
55
+ mkdir -p "$dir"
56
+ done < "$ASSETS_DIR/directory_structure.txt"
57
+
58
+ # ─── 4. Create project/PROJECT_SUMMARY.md ────────────────────────────────────
59
+ echo "→ Copying PROJECT_SUMMARY.md from template..."
60
+ cp "$ASSETS_DIR/PROJECT_SUMMARY_template.md" project/PROJECT_SUMMARY.md
61
+
62
+ # ─── 5. Create project/configs/workflow.json ─────────────────────────────────
63
+ echo "→ Copying workflow.json from template..."
64
+ cp "$ASSETS_DIR/workflow_template.json" project/configs/workflow.json
65
+
66
+ # ─── 6. Create project/configs/test-config.json stub ─────────────────────────
67
+ echo "→ Copying test-config.json from template..."
68
+ cp "$ASSETS_DIR/test-config_template.json" project/configs/test-config.json
69
+
70
+ # ─── 6.5. Create project/configs/scope-thresholds.json ───────────────────────
71
+ # Consumed by skills/jenga (Phase 0) and skills/do (Step 0); both halt if it's
72
+ # missing, so it must exist immediately after scaffold.
73
+ echo "→ Copying scope-thresholds.json from template..."
74
+ cp "$ASSETS_DIR/scope-thresholds_template.json" project/configs/scope-thresholds.json
75
+
76
+ # ─── 7. Create project/data/baselines.json ───────────────────────────────────
77
+ echo "→ Creating baselines.json..."
78
+ echo '{}' > project/data/baselines.json
79
+
80
+ # ─── 8. Create project/logs/events.json ──────────────────────────────────────
81
+ echo "→ Creating events.json..."
82
+ echo '[]' > project/logs/events.json
83
+
84
+ # ─── 9. Create docs/STRATEGY.md ──────────────────────────────────────────────
85
+ echo "→ Creating docs/STRATEGY.md (strategic brief for investors, partners, and the product team)..."
86
+ mkdir -p docs
87
+ cp "$ASSETS_DIR/strategy_stub_template.md" docs/STRATEGY.md
88
+
89
+ # ─── 10. Create CHANGELOG.md ──────────────────────────────────────────────────
90
+ echo "→ Creating CHANGELOG.md from template..."
91
+ cp "$PKG_ROOT/templates/CHANGELOG_TEMPLATE.md" CHANGELOG.md
92
+
93
+ # ─── 11. Apply project_files_visibility ──────────────────────────────────────
94
+ # Runs before the commit so the .gitignore entry (ignored) is captured in the
95
+ # initial commit.
96
+ echo "→ Applying project files visibility ($VISIBILITY)..."
97
+ bash "$VISIBILITY_SCRIPT" "$VISIBILITY" "$PWD"
98
+
99
+ # ─── 12. Generate CLAUDE.md / AGENTS.md ──────────────────────────────────────
100
+ # Unconditional — never gated on agentTarget (E41_S04). Applies the J-
101
+ # collision rule and idempotent managed-block updates; see
102
+ # lib/generate-agent-context.js (shared with the published `jenga init` CLI).
103
+ echo "→ Generating CLAUDE.md / AGENTS.md..."
104
+ if command -v node >/dev/null 2>&1; then
105
+ node "$PKG_ROOT/lib/generate-agent-context.js" "$PWD"
106
+ else
107
+ echo " Warning: node not found — skipped CLAUDE.md/AGENTS.md generation." >&2
108
+ fi
109
+
110
+ # ─── 13. Initial commit ──────────────────────────────────────────────────────
111
+ echo "→ Staging and committing scaffolded files..."
112
+ git add -A
113
+ git commit -m "init: scaffold project structure and workflow config"
114
+
115
+ echo ""
116
+ echo "✓ Project scaffold complete."
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: jbp
2
+ name: j:jbp
3
3
  description: Scaffold the project using the JengaBasePlate boilerplate repo template from https://github.com/samwelmunga/JengaBasePlate.git
4
4
  keywords:
5
5
  - jbp
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: jenga
2
+ name: j:jenga
3
3
  description: Interactive-by-default board orchestrator with a fully automated escape hatch. Bare `/jenga` renders a picker and confirmation tree before scoping the run; `/jenga <ids>` resolves an explicit fuzzy-ID scope and confirms it; `/jenga *` reproduces the original zero-prompt behavior — decomposing any unbroken Epics into Stories, any unbroken Stories into Tasks, queuing all unqueued Tasks into todo.md, then executing every eligible item with no user prompts — until the board is fully started.
4
4
  keywords:
5
5
  - jenga
@@ -97,12 +97,18 @@
97
97
  # ENTIRE turn is rejected as an error turn (state left untouched) — there
98
98
  # is no partial application of the valid numbers in a mixed-validity reply.
99
99
  #
100
- # Toggling is NOT cascading: unchecking a parent epic/story does not
101
- # automatically uncheck its children, and vice versa. Each displayed
102
- # number is an independent checkbox. This is a deliberate simplicity
103
- # choice — the task only requires "uncheck or re-check individual
104
- # presented items", not cascading selection logic (that expansion/cascade
105
- # concern already belongs to cascade-resolve.sh, per its own header).
100
+ # Toggling CASCADES downward (E45_S02_T03): checking or unchecking a
101
+ # story applies the same resulting state to all of its tasks; checking or
102
+ # unchecking an epic applies the same resulting state to all of its
103
+ # stories and their tasks. This is a deliberate, explicit reversal of the
104
+ # original non-cascading design (see this story's 2026-08-27 close-out) —
105
+ # the user hit a real case where unchecking a parent epic left its child
106
+ # story/tasks checked, requiring a manual follow-up uncheck command.
107
+ # Cascade is STRICTLY DOWNWARD (parent -> descendants) only: checking a
108
+ # leaf task does NOT imply its ancestors should also be checked, so a
109
+ # checked child under an unchecked parent is a valid (if visually
110
+ # inconsistent) state. Toggling a leaf task (no children) behaves exactly
111
+ # as it always has — no descendants to cascade into.
106
112
  #
107
113
  # ---------------------------------------------------------------------------
108
114
  # OUTPUT CONTRACT (deliberately NOT uniform JSON — see rationale below)
@@ -165,8 +171,15 @@
165
171
  # STATE FILE
166
172
  # ---------------------------------------------------------------------------
167
173
  # Created under `mktemp -t jenga-confirm-XXXXXX.json`. Stores:
168
- # - `numbering`: displayed number -> {item, level, checked} — the ONLY
169
- # field that mutates across turns is `checked`.
174
+ # - `numbering`: displayed number -> {item, level, checked, children} —
175
+ # `children` is a fixed list of the item's direct descendant numbers
176
+ # (a story's task numbers; an epic's story numbers), computed once at
177
+ # build time so continue-mode can cascade a toggle without
178
+ # re-deriving parent/child relationships from `item_id`/`epic_id`/
179
+ # `story_id` at toggle time. `children` is always `[]` for a leaf
180
+ # task. The only fields that mutate across turns are `checked` values
181
+ # (potentially many at once, via cascade — see TOGGLE COMMAND GRAMMAR
182
+ # above).
170
183
  # - `layout`: the fixed print order (item lines + header markers),
171
184
  # computed once at start. Continue mode never recomputes tree shape —
172
185
  # it only re-renders from `layout` + the live `checked` flags in
@@ -305,15 +318,17 @@ def pluralize_task(n):
305
318
  return f"{n} task{'' if n == 1 else 's'}"
306
319
 
307
320
 
308
- def add_item(item, level, indent):
321
+ def add_item(item, level, indent, parent_number=None):
309
322
  global counter
310
323
  n = str(counter)
311
324
  counter += 1
312
325
  suffix = ""
313
326
  if item.get("id") in undecomposed_ids:
314
327
  suffix = " [needs decomposition -- no children on the board yet]"
315
- numbering[n] = {"item": item, "level": level, "checked": True}
328
+ numbering[n] = {"item": item, "level": level, "checked": True, "children": []}
316
329
  layout.append({"kind": "item", "number": n, "level": level, "indent": indent, "suffix": suffix})
330
+ if parent_number is not None:
331
+ numbering[parent_number]["children"].append(n)
317
332
  return n
318
333
 
319
334
 
@@ -328,7 +343,7 @@ def append_suffix(extra):
328
343
 
329
344
 
330
345
  for e in epics:
331
- add_item(e, "epic", INDENT["epic"])
346
+ e_num = add_item(e, "epic", INDENT["epic"])
332
347
  child_stories = stories_by_epic.get(e["id"], [])
333
348
  story_count = len(child_stories)
334
349
  task_count = sum(len(tasks_by_story.get(s["id"], [])) for s in child_stories)
@@ -340,23 +355,23 @@ for e in epics:
340
355
  append_suffix(", ".join(parts))
341
356
 
342
357
  for s in child_stories:
343
- add_item(s, "story", INDENT["story"])
358
+ s_num = add_item(s, "story", INDENT["story"], parent_number=e_num)
344
359
  child_tasks = tasks_by_story.get(s["id"], [])
345
360
  if child_tasks:
346
361
  append_suffix(pluralize_task(len(child_tasks)))
347
362
  for t in child_tasks:
348
- add_item(t, "task", INDENT["task"])
363
+ add_item(t, "task", INDENT["task"], parent_number=s_num)
349
364
 
350
365
  if orphan_stories:
351
366
  layout.append({"kind": "header", "text": ""})
352
367
  layout.append({"kind": "header", "text": "-- Directly Selected Stories (no epic in this selection) --"})
353
368
  for s in orphan_stories:
354
- add_item(s, "story", INDENT["story"])
369
+ s_num = add_item(s, "story", INDENT["story"])
355
370
  child_tasks = tasks_by_story.get(s["id"], [])
356
371
  if child_tasks:
357
372
  append_suffix(pluralize_task(len(child_tasks)))
358
373
  for t in child_tasks:
359
- add_item(t, "task", INDENT["task"])
374
+ add_item(t, "task", INDENT["task"], parent_number=s_num)
360
375
 
361
376
  if orphan_tasks:
362
377
  layout.append({"kind": "header", "text": ""})
@@ -644,18 +659,40 @@ if invalid:
644
659
  print(f"STATE_FILE: {state_file_path}", file=sys.stderr)
645
660
  sys.exit(1)
646
661
 
662
+ def apply_cascade(num, value):
663
+ """Set `num`'s checked state to `value`, then recurse into every
664
+ number in its `children` list (a story's tasks; an epic's stories,
665
+ which recurse further into their own tasks). Strictly downward —
666
+ never touches a parent. A leaf task has an empty `children` list, so
667
+ this degenerates to exactly the old single-flag assignment for the
668
+ non-cascading base case."""
669
+ numbering[num]["checked"] = value
670
+ for child in numbering[num].get("children", []):
671
+ apply_cascade(child, value)
672
+
673
+
647
674
  # All-or-nothing application, de-duplicated (first-seen wins on dupes).
675
+ # Resulting values are computed from PRE-mutation state in a first pass,
676
+ # then applied (with cascade) in a second pass -- this avoids a
677
+ # parent-then-child (or child-then-parent) double-toggle artifact when a
678
+ # single reply lists both a parent and one of its own descendants, e.g.
679
+ # bare-number toggle "5,6" where 6 is a child of 5.
648
680
  seen = set()
681
+ pending = []
649
682
  for tok in valid_tokens:
650
683
  if tok in seen:
651
684
  continue
652
685
  seen.add(tok)
653
686
  if action == "check":
654
- numbering[tok]["checked"] = True
687
+ value = True
655
688
  elif action == "uncheck":
656
- numbering[tok]["checked"] = False
689
+ value = False
657
690
  else:
658
- numbering[tok]["checked"] = not numbering[tok]["checked"]
691
+ value = not numbering[tok]["checked"]
692
+ pending.append((tok, value))
693
+
694
+ for tok, value in pending:
695
+ apply_cascade(tok, value)
659
696
 
660
697
  state["numbering"] = numbering
661
698
  with open(state_file_path, "w", encoding="utf-8") as f:
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: jenga-permission-level
2
+ name: j:jenga-permission-level
3
3
  description: Report or switch the current session's 5-tier permission level (Locked/Guarded/Standard/Elevated/Unrestricted) without hand-editing settings.json.
4
4
  keywords:
5
5
  - "permission level"
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: lgtm
2
+ name: j:lgtm
3
3
  description: Approve and commit the current work, then continue to the next task. Shortcut that chains /commit followed by /continue.
4
4
  keywords:
5
5
  - lgtm
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: pi-plan
2
+ name: j:pi-plan
3
3
  description: Define or expand project Epics in PROJECT_SUMMARY.md. Use this at the start of a project to establish its foundation, AND whenever the user wants to add major new features, plan a significant new area of work, or make epic-level changes to an existing project. Trigger whenever you hear things like "new feature area", "big change", "new epic", "expand the project", "add a major capability", or "plan a new phase" — even mid-project.
4
4
  keywords:
5
5
  - epic
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: proceed
2
+ name: j:proceed
3
3
  description: Review project progress by checking epics and stories, optionally consulting PROJECT_SUMMARY.md and WARP.md, then continue executing the project plan.
4
4
  keywords:
5
5
  - proceed
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: publish
2
+ name: j:publish
3
3
  description: Configure, validate, and orchestrate scaffolded release workflows through a single `/publish` entry point with bounded sub-commands.
4
4
  keywords:
5
5
  - publish
@@ -9,6 +9,8 @@ keywords:
9
9
  - npm
10
10
  - registry
11
11
  - release notes
12
+ - staged publishing
13
+ - stage
12
14
  examples:
13
15
  - "publish setup --target staging-appstore"
14
16
  - "publish setup --type mobile-ios"
@@ -21,6 +23,9 @@ examples:
21
23
  - "publish deploy --target my-droplet --dry-run"
22
24
  - "publish history --limit 5"
23
25
  - "publish release-notes --target staging-appstore"
26
+ - "test a release before publishing"
27
+ - "publish stage --target npm-registry --dry-run"
28
+ - "approve a staged npm release"
24
29
  metadata:
25
30
  scope: multi-target-v2
26
31
  primary_target: multi
@@ -76,6 +81,7 @@ If config or env validation fails, the skill exits with code `4` and does not co
76
81
  |---|---|---|---|
77
82
  | `/publish setup` | Prepare or refresh target configuration | `skills/publish/scripts/setup_wizard.sh` | Supported types: `mobile-ios`, `npm`, `npm-ci`, `droplet` |
78
83
  | `/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 |
84
+ | `/publish stage` | Stage an npm release into npm's staged-publishing area, smoke-test it in isolation, then approve or reject it | `skills/publish/scripts/npm_stage_pipeline.sh` (the `publish` sub-command) and `skills/publish/scripts/npm_stage_inspect.sh` (`list`, `view`, `download`, `test`, `approve`, `reject`) | Supported for `npm` and `npm-ci` target types only |
79
85
  | `/publish history` | Read the canonical publish ledger | `skills/publish/scripts/show_history.sh` | Target-agnostic; filter by `--target <name>` |
80
86
  | `/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
87
 
@@ -170,6 +176,66 @@ Example invocations:
170
176
 
171
177
  Use `--dry-run` to rehearse a release, validate that gates pass, and confirm the generated release notes without publishing.
172
178
 
179
+ ### `/publish stage`
180
+
181
+ Staged publishing for `npm`/`npm-ci` targets only. Not available for
182
+ `mobile-ios` or `droplet` — those adapters have no staging area to dispatch
183
+ to. The lifecycle is **stage → test → approve**, with `reject` as the
184
+ discard path at any point after staging:
185
+
186
+ ```text
187
+ staged ──▶ stage_tested ──▶ approved (approve requires 2FA; refuses without
188
+ │ │ a passing test unless --force <reason>)
189
+ │ │
190
+ └─────────────┴──▶ rejected (discard path — available from any staged state)
191
+ ```
192
+
193
+ Two registry constraints trip people up and are worth stating plainly:
194
+
195
+ - **The package must already exist on the registry.** npm's staged-publishing
196
+ workflow only applies to a package that has had at least one normal,
197
+ non-staged publish already — `validate_npm_stage_env.sh` checks this
198
+ up front and exits `4` if it doesn't hold.
199
+ - **Approval requires 2FA; staging does not.** `npm stage publish` (the
200
+ `stage` sub-command below) can run non-interactively in CI. `npm stage
201
+ approve` cannot — npmjs.com requires an interactive one-time-password
202
+ step for approval, so it is always a human action even when staging was
203
+ automated (see the npm-ci adapter's OIDC staging section for the CI-stage,
204
+ human-approve split).
205
+
206
+ All seven sub-commands:
207
+
208
+ ```text
209
+ /publish stage publish <target> <path-to-publish.json> [--dry-run] [--non-interactive] [--otp <otp>]
210
+ /publish stage test <stage-id> [--keep] [--config <path>] [--dry-run] [--json]
211
+ /publish stage list [<package-spec>] [--config <path>] [--dry-run] [--json]
212
+ /publish stage view <stage-id> [--config <path>] [--dry-run] [--json]
213
+ /publish stage download <stage-id> [--out <dir>] [--config <path>] [--dry-run] [--json]
214
+ /publish stage approve <stage-id> [--otp <otp>] [--force <reason>] [--config <path>] [--dry-run] [--json]
215
+ /publish stage reject <stage-id> [--config <path>] [--dry-run] [--json]
216
+ ```
217
+
218
+ Implementation:
219
+
220
+ - `publish` → `bash skills/publish/scripts/npm_stage_pipeline.sh <target> <path-to-publish.json> [--dry-run] [--non-interactive] [--otp <otp>]` — six ordered phases (validate, gates, pack, stage, capture, ledger); writes a `staged` ledger entry on success.
221
+ - `test`, `list`, `view`, `download`, `approve`, `reject` → `bash skills/publish/scripts/npm_stage_inspect.sh <sub> [args] [--config <path>] [--dry-run] [--json]`
222
+ - `test` downloads the exact staged tarball into an isolated scratch directory outside the repo, installs it, runs the target's `npm.stage.smoke_cmd` (or the documented default check), and writes a `stage_tested` ledger entry (`pass`/`fail`).
223
+ - `approve` refuses to run unless a passing `test` is on record for that exact stage id, unless `--force <reason>` is given; writes an `approved` ledger entry.
224
+ - `reject` is the discard path omitted from npm's own staged-publishing docs page — documented here so it stays discoverable; writes a `rejected` ledger entry.
225
+
226
+ Example invocations:
227
+
228
+ ```bash
229
+ /publish stage publish npm-registry ./publish.json --dry-run
230
+ /publish stage test <stage-id>
231
+ /publish stage approve <stage-id>
232
+ /publish stage reject <stage-id>
233
+ ```
234
+
235
+ Use `/publish stage` to rehearse and smoke-test a release before it goes
236
+ live — the point of staging is that a bad tarball is caught in isolation
237
+ instead of being caught by users after `npm publish`.
238
+
173
239
  ### `/publish history`
174
240
 
175
241
  ```text
@@ -99,11 +99,33 @@ Run the adapter phases in this exact order:
99
99
  ```yaml
100
100
  permissions:
101
101
  id-token: write
102
- contents: read
102
+ contents: write
103
103
  ```
104
- and runs `npm publish --provenance` to attach a build provenance attestation
105
- to the published package. In `--dry-run` mode, print the rendered YAML to
106
- stdout and exit 0 without writing any file.
104
+ (`contents: write` — bumped from `read` — is required so the `publish` and
105
+ `stage` jobs can push the tags described below; this is the CI's own token
106
+ gaining write access to tag *its own* repository, nothing else, and no new
107
+ secret or credential is introduced) and runs `npm publish --provenance` to
108
+ attach a build provenance attestation to the published package. In
109
+ `--dry-run` mode, print the rendered YAML to stdout and exit 0 without
110
+ writing any file.
111
+
112
+ Each job also gains a final step, gated on `if: success()` so a failed
113
+ publish/stage never creates or moves a tag:
114
+ - **`publish` job — "Tag prod release and remove stage tag":** reads the
115
+ version from `package.json`, tags the checked-out commit `v<version>`
116
+ (no force — this tag is immutable; a genuine collision fails loudly
117
+ rather than silently moving a prod tag), pushes it, then best-effort
118
+ deletes the corresponding `v<version>-stage` tag both locally and on the
119
+ remote (tolerating its absence, e.g. a prod publish that skipped
120
+ staging).
121
+ - **`stage` job — "Tag stage release":** reads the version the same way
122
+ and tags the checked-out commit `v<version>-stage` — a **mutable**
123
+ pointer, force-created (`git tag -f`) and force-pushed, so re-staging the
124
+ same version moves the tag rather than failing.
125
+
126
+ Both new steps configure a `github-actions[bot]` git identity
127
+ (`user.name`/`user.email`) before tagging, since no prior step in the
128
+ workflow sets one.
107
129
 
108
130
  3. **`commit-workflow`** — write the generated YAML to `<workflow_path>`
109
131
  (creating the `.github/workflows/` directory if needed) and commit it with:
@@ -190,6 +212,40 @@ A successful non-dry-run produces:
190
212
  - A history entry in `project/logs/publish-history.json` written by
191
213
  `publish_deploy.sh` with `platform_state: "triggered"`
192
214
 
215
+ ## Staged Publishing
216
+
217
+ This target type also supports **staged publishing** via `/publish stage` —
218
+ npm's pre-publish staging area — with an OIDC-specific split between the
219
+ automated and human halves of the flow:
220
+
221
+ - **The actual `npm stage publish --provenance` call always runs inside
222
+ GitHub Actions, never on a local machine.** For `npm-ci` targets, phase 4
223
+ of `npm_stage_pipeline.sh` (run locally via `/publish stage publish`)
224
+ dispatches the `stage` job of the target's generated workflow (the same
225
+ `<workflow_path>` / OIDC Trusted Publisher link `npm_ci_pipeline.sh` uses
226
+ for `/publish deploy`, generated with a `mode: publish | stage`
227
+ `workflow_dispatch` input) via
228
+ `gh workflow run <workflow_filename> --repo <github_repo> -f mode=stage`,
229
+ then blocks on `gh run watch` until that run finishes. The `stage` job
230
+ itself runs a plain `npm stage publish --provenance --access <access>
231
+ --tag <dist_tag>` step, authorised through the same Trusted Publisher OIDC
232
+ link described above — no `NPM_TOKEN` is needed to stage, exactly as none
233
+ is needed to publish. Local `--dry-run` only prints the `gh workflow run`
234
+ command that would be dispatched; it never triggers a workflow run.
235
+ - **Approval is always a human, out-of-CI step.** `npm stage approve`
236
+ requires an interactive npm 2FA one-time password — there is no OIDC
237
+ equivalent for approval. A human runs
238
+ `bash skills/publish/scripts/npm_stage_inspect.sh test <stage-id>` (or
239
+ relies on the automated CI-staged test result) and then
240
+ `bash skills/publish/scripts/npm_stage_inspect.sh approve <stage-id> --otp <otp>`
241
+ from their own machine. `reject` (same script) is available to either
242
+ side to discard a staged candidate.
243
+
244
+ Same registry-existence precondition as a normal `npm-ci` deploy: staged
245
+ publishing only applies to a package that has already had at least one
246
+ non-staged publish (`validate_npm_stage_env.sh` checks this up front, exit
247
+ `4` if not).
248
+
193
249
  ## Post-deploy manual steps
194
250
 
195
251
  After a successful deploy trigger, the adapter prints the workflow run URL.