@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.
Files changed (117) hide show
  1. package/README.md +10 -7
  2. package/agents/developer.md +82 -2
  3. package/agents/scrum-master.md +215 -21
  4. package/agents/tester.md +90 -8
  5. package/hooks/on_session_end.sh +171 -20
  6. package/mcp/router/embedder.js +1 -1
  7. package/mcp/training_runner/index.js +239 -0
  8. package/mcp/training_runner/package-lock.json +1065 -0
  9. package/mcp/training_runner/package.json +15 -0
  10. package/package.json +14 -16
  11. package/scripts/check-permission-level.sh +107 -0
  12. package/scripts/check-publicignore-match.sh +122 -0
  13. package/scripts/check-worktree-liveness.sh +193 -0
  14. package/scripts/generate-rapport-manifest.sh +43 -0
  15. package/scripts/idea_manager.sh +47 -0
  16. package/scripts/install-worktree-commit-guard.sh +134 -0
  17. package/scripts/jenga-permission-level-switch.sh +109 -0
  18. package/scripts/smoke-harness.sh +139 -0
  19. package/scripts/validate-board.sh +62 -0
  20. package/scripts/with-lock.sh +158 -0
  21. package/scripts/worktree-remove-guard.sh +204 -0
  22. package/skills/clearify/SKILL.md +52 -0
  23. package/skills/close-story/SKILL.md +203 -0
  24. package/skills/close-story/scripts/check-story-closeable.sh +195 -0
  25. package/skills/close-story/scripts/compute-scope-divergence.sh +128 -0
  26. package/skills/close-story/scripts/extract-diff-stats.sh +48 -0
  27. package/skills/close-story/scripts/extract-task-diff-stats.sh +97 -0
  28. package/skills/close-story/scripts/update-task-frontmatter.sh +103 -0
  29. package/skills/commit/SKILL.md +30 -3
  30. package/skills/distribute/CONFIG_SCHEMA.md +148 -0
  31. package/skills/distribute/SKILL.md +173 -0
  32. package/skills/distribute/scripts/check-version.sh +74 -0
  33. package/skills/distribute/scripts/commit-version-bump.sh +108 -0
  34. package/skills/distribute/scripts/distribute-changes.sh +381 -0
  35. package/skills/do/SKILL.md +352 -1
  36. package/skills/do/assets/intent-vs-diff-prompt.md +69 -0
  37. package/skills/doc/assets/path-objectives.yaml +13 -0
  38. package/skills/doc-sync/SKILL.md +16 -0
  39. package/skills/doc-sync/assets/doc_targets.md +11 -0
  40. package/skills/idea/SKILL.md +56 -0
  41. package/skills/idea/assets/idea_handoff_template.md +26 -0
  42. package/skills/idea/assets/idea_template.md +3 -0
  43. package/skills/init/SKILL.md +101 -7
  44. package/skills/init/assets/directory_structure.txt +1 -0
  45. package/skills/init/assets/strategy_stub_template.md +38 -0
  46. package/skills/init/assets/workflow_template.json +1 -1
  47. package/skills/init/scripts/apply-project-visibility.sh +176 -0
  48. package/skills/init/scripts/detect-existing-codebase.sh +166 -0
  49. package/skills/init/scripts/init.sh +35 -1
  50. package/skills/jenga/SKILL.md +206 -14
  51. package/skills/jenga/scripts/board-scan.sh +238 -0
  52. package/skills/jenga/scripts/cascade-resolve.sh +297 -0
  53. package/skills/jenga/scripts/render-confirmation.sh +679 -0
  54. package/skills/jenga/scripts/render-picker.sh +439 -0
  55. package/skills/jenga/scripts/resolve-id.sh +367 -0
  56. package/skills/jenga-permission-level/SKILL.md +81 -0
  57. package/skills/proceed/SKILL.md +1 -1
  58. package/skills/publish/SKILL.md +8 -5
  59. package/skills/publish/assets/ci-contract.md +2 -2
  60. package/skills/publish/assets/ownership-matrix.md +1 -1
  61. package/skills/publish/scripts/finalize_changelog.sh +115 -0
  62. package/skills/publish/scripts/generate_release_notes.sh +475 -28
  63. package/skills/publish/scripts/npm_ci_pipeline.sh +44 -6
  64. package/skills/publish/scripts/publish_deploy.sh +38 -8
  65. package/skills/publish/scripts/run_gates.sh +2 -2
  66. package/skills/reconcile/SKILL.md +117 -5
  67. package/skills/reconcile/scripts/detect-unlinked-code.sh +741 -0
  68. package/skills/skillify/assets/init-new/assets/directory_structure.txt +5 -1
  69. package/skills/spinoff/SKILL.md +12 -7
  70. package/skills/todo/SKILL.md +2 -0
  71. package/skills/uncharted/SKILL.md +711 -0
  72. package/skills/uncharted/assets/SEGMENT_PROPOSAL_TEMPLATE.md +129 -0
  73. package/skills/uncharted/assets/UNDERSTANDING_DOC_TEMPLATE.md +160 -0
  74. package/skills/uncharted/scripts/apply-subsystem-cap.sh +573 -0
  75. package/skills/uncharted/scripts/detect-dependencies.sh +732 -0
  76. package/skills/uncharted/scripts/detect-tests.sh +553 -0
  77. package/skills/uncharted/scripts/discover-subsystems.sh +1029 -0
  78. package/skills/uncharted/scripts/enumerate-target.sh +470 -0
  79. package/skills/uncharted/scripts/import-source.sh +517 -0
  80. package/skills/uncharted/scripts/inspect-provenance.sh +573 -0
  81. package/skills/uncharted/scripts/resolve-segment-target.sh +640 -0
  82. package/skills/uncharted/scripts/run-engine.sh +655 -0
  83. package/skills/uncharted/scripts/validate-proposed-items.sh +125 -0
  84. package/skills/uncharted/scripts/write-backfilled-epics.sh +498 -0
  85. package/skills/wtf/SKILL.md +20 -0
  86. package/templates/CHANGELOG_TEMPLATE.md +13 -0
  87. package/templates/PROBLEM_RAPPORT_TEMPLATE.md +4 -1
  88. package/templates/SCRUM_BOARD_SCHEMA.md +206 -10
  89. package/templates/permission-levels/README.md +73 -0
  90. package/templates/permission-levels/level-1-locked.json +71 -0
  91. package/templates/permission-levels/level-2-guarded.json +64 -0
  92. package/templates/permission-levels/level-3-standard.json +62 -0
  93. package/templates/permission-levels/level-4-elevated.json +60 -0
  94. package/templates/permission-levels/level-5-unrestricted.json +58 -0
  95. package/skills/convert/SKILL.md +0 -124
  96. package/skills/convert/convert_cli.py +0 -235
  97. package/skills/convert/tests/sample.csv +0 -4
  98. package/skills/convert/tests/sample.json +0 -5
  99. package/skills/convert/tests/sample.jsonl +0 -3
  100. package/skills/convert/tests/sample.yaml +0 -18
  101. package/skills/convert/tests/sample_obj.csv +0 -2
  102. package/skills/convert/tests/sample_obj.json +0 -9
  103. package/skills/mirror-public/SKILL.md +0 -237
  104. package/skills/mirror-public/assets/config.json +0 -5
  105. package/skills/mirror-public/scripts/mirror.sh +0 -374
  106. package/skills/self-sync/SKILL.md +0 -73
  107. package/skills/self-sync/scripts/run.js +0 -136
  108. package/skills/train/SKILL.md +0 -116
  109. package/skills/train/assets/dashboard-templates/classifiers.html +0 -106
  110. package/skills/train/assets/dashboard-templates/nlp.html +0 -102
  111. package/skills/train/assets/dashboard-templates/transformers.html +0 -98
  112. package/skills/train/assets/results-parsers/__init__.py +0 -9
  113. package/skills/train/assets/results-parsers/classifiers.py +0 -84
  114. package/skills/train/assets/results-parsers/nlp.py +0 -88
  115. package/skills/train/assets/results-parsers/reporter.py +0 -154
  116. package/skills/train/assets/results-parsers/transformers.py +0 -120
  117. package/skills/train/train_cli.py +0 -786
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: jenga
3
- description: Fully automated board orchestrator. Decomposes any unbroken Epics into Stories, any unbroken Stories into Tasks, queues all unqueued Tasks into todo.md, then executes every eligible item — no user prompts — until the board is fully started.
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
6
6
  - orchestrate
@@ -18,37 +18,219 @@ metadata:
18
18
 
19
19
  ## Purpose
20
20
 
21
- `/jenga` is a hands-free "commit to everything on the board" pipeline. It ensures the entire board is fully decomposed, fully queued, and fully executing — without any user interaction. It runs in four phases: **decompose → queue → execute → loop**.
21
+ `/jenga` is interactive by default — it never silently commits to the whole board without showing the user what it's about to run and letting them scope or edit that selection first. It has three entry modes:
22
+
23
+ - **Bare `/jenga`** (no argument) — renders a numbered picker of the full board, then an editable confirmation tree, before anything executes.
24
+ - **`/jenga <ids>`** (explicit comma-separated scope) — resolves the given IDs via the fuzzy-ID grammar, skipping the picker, then still shows the same editable confirmation tree before executing.
25
+ - **`/jenga *`** (literal wildcard) — the explicit escape hatch. Skips both the picker and the confirmation step entirely and reproduces the original hands-free "commit to everything on the board" pipeline: the entire board is fully decomposed, fully queued, and fully executing — without any user interaction, no user prompts. This is the only path where `/jenga` runs with no user prompts at all.
26
+
27
+ Once a run's scope is established (by confirmation, or unconditionally under `*`), `/jenga` runs the same underlying phases against that scope: **entry mode resolution → decompose → queue → execute → loop**.
22
28
 
23
29
  ## Instructions
24
30
 
31
+ ### Phase 0 — Load threshold config
32
+
33
+ Read `project/configs/scope-thresholds.json`.
34
+
35
+ If the file does not exist, emit:
36
+ ```
37
+ ERROR: project/configs/scope-thresholds.json not found. Cannot proceed.
38
+ ```
39
+ and halt. Do not fall back to any default values.
40
+
41
+ If the file is not valid JSON, emit:
42
+ ```
43
+ ERROR: project/configs/scope-thresholds.json is malformed (invalid JSON). Cannot proceed.
44
+ ```
45
+ and halt.
46
+
47
+ Extract the following named values for use throughout this skill:
48
+ - `inline_max_files` — maximum files a task may touch to qualify for inline execution scope
49
+ - `inline_max_lines` — maximum total lines changed for inline scope
50
+ - `story_max_files` — maximum files a task may touch to qualify for story-scope bundling
51
+ - `bundle_lock_ttl_minutes` — time-to-live in minutes for a story-scope bundle lock
52
+
53
+ These values must be read fresh on each invocation. Never use hardcoded fallbacks.
54
+
55
+ ### Phase 0.5 — Pre-flight Validation
56
+
57
+ Before accepting any task for decomposition or execution, the executing agent must validate the task's scope fields. The threshold values loaded in Phase 0 may be referenced in error messages for context, but are not required for the core validation rules below.
58
+
59
+ For each task read from the board, apply the following checks in order:
60
+
61
+ #### Rule 1 — Valid execution_scope value
62
+
63
+ If the task frontmatter contains an `execution_scope` field, its value must be one of: `task`, `story`, `epic`, `inline`.
64
+
65
+ If the value is anything else, halt immediately with:
66
+
67
+ ```
68
+ VALIDATION ERROR [<task_id>]: execution_scope "<value>" is not a valid scope. Allowed: task, story, epic, inline.
69
+ ```
70
+
71
+ Do not proceed with this task.
72
+
73
+ #### Rule 2 — scope_rationale must contain a measurable claim
74
+
75
+ If `execution_scope` is present, `scope_rationale` must also be present and must contain at least one digit (0–9) or the word "file" (case-insensitive).
76
+
77
+ If `scope_rationale` is absent, or present but contains no digit and does not contain the word "file", halt with:
78
+
79
+ ```
80
+ VALIDATION ERROR [<task_id>]: scope_rationale is missing or lacks a numeric/file-count claim. Provide a rationale that includes a digit (e.g. "touches 2 files") or the word "file".
81
+ ```
82
+
83
+ Do not proceed with this task.
84
+
85
+ #### Rule 3 — epic scope requires explicit human approval
86
+
87
+ If `execution_scope` is `"epic"`, the task must also have `epic_scope_approval: true` set explicitly in its frontmatter. A missing `epic_scope_approval` field and a value of `false` are both rejection conditions.
88
+
89
+ If `epic_scope_approval` is absent or is not exactly `true`, halt with:
90
+
91
+ ```
92
+ VALIDATION ERROR [<task_id>]: execution_scope=epic requires epic_scope_approval: true (set by human operator). This field must be added manually — it is never assigned autonomously.
93
+ ```
94
+
95
+ Do not proceed with this task.
96
+
97
+ #### Rule 4 — crucial_level: locked forces execution_scope: inline
98
+
99
+ If the task frontmatter contains `crucial_level: locked` (per `templates/SCRUM_BOARD_SCHEMA.md`'s Crucial Flag Fields), `execution_scope` for that task MUST be `inline` — only the current foreground/inline session can pause mid-run for a live confirmation; a backgrounded subagent has no live channel back to the user.
100
+
101
+ This rule **auto-corrects and continues**; unlike Rules 1-3, it never halts.
102
+
103
+ - If `execution_scope` is present and its value is anything other than `inline`, correct it to `inline` directly in the task file, and record a logged note of the correction by appending to that task's `override_justification` frontmatter field (the auditable mechanism for this rule — do not use `events.json` or any other location) a line of the form:
104
+
105
+ ```
106
+ override_justification: "Rule 4 auto-correction <date>: execution_scope forced from '<previous_value>' to 'inline' because crucial_level: locked."
107
+ ```
108
+
109
+ Then emit (non-fatally — do not halt):
110
+
111
+ ```
112
+ AUTO-CORRECTION [<task_id>]: crucial_level=locked requires execution_scope=inline; corrected from "<previous_value>" to "inline".
113
+ ```
114
+
115
+ - If `execution_scope` is absent entirely, set it to `inline` directly in the task frontmatter. Do **not** fall through to the Backward-compatibility default of `execution_scope: task` documented immediately below — a `locked` item overrides that default even when no other execution-scope fields are present. No `override_justification` note is required in this case, since there is no prior value being overridden.
116
+
117
+ Proceed to the next rule (or the next phase, if this was the last applicable rule) after applying the correction.
118
+
119
+ #### Backward compatibility — legacy tasks
120
+
121
+ If the task frontmatter contains **none** of `execution_scope`, `scope_rationale`, `needs_docs`, `jenga_assigned`, or `override_justification`, treat the task as a legacy task:
122
+
123
+ - Assume `execution_scope: task`
124
+ - Assume `needs_docs: true`
125
+ - Skip all three rules above and proceed without error.
126
+
127
+ #### Validation success
128
+
129
+ If all applicable rules pass (or the task is a legacy task), proceed to the next phase for that task without any error output.
130
+
131
+ ---
132
+
133
+ ### Phase 0.75 — Entry Mode Resolution
134
+
135
+ This phase determines **how `/jenga` was invoked** and, for two of the three entry modes, produces a **scoped set** — a confirmed list of board IDs (epics/stories/tasks) that Phases 1-4 must restrict themselves to. All board scanning, ID parsing, cascade expansion, and rendering used by this phase already live in `skills/jenga/scripts/` per this repo's "Scripts Over Inline Logic" principle — this phase never re-implements any of that logic inline. The executing agent's job here is limited to: invoking the right script with the right arguments, relaying its STDOUT verbatim to the user when the contract calls for that, capturing the `STATE_FILE:` line from STDERR for the next turn, and forwarding the user's raw reply back into the next invocation unmodified.
136
+
137
+ **Determine the invocation form** from the raw argument (if any) passed to `/jenga`:
138
+
139
+ - No argument at all → **bare branch**.
140
+ - The argument is the literal string `*` → **wildcard branch**.
141
+ - Any other non-empty argument → **scoped branch** (treat the whole argument as the comma-separated raw ID list).
142
+
143
+ #### Wildcard branch (`/jenga *`)
144
+
145
+ Skip both the picker and the confirmation step entirely. There is no scoped set — proceed to Phase 1 unrestricted, exactly as `/jenga` behaved before this phase existed.
146
+
147
+ #### Bare branch (`/jenga`)
148
+
149
+ 1. Invoke `skills/jenga/scripts/render-picker.sh` with no arguments (start mode). Relay its STDOUT (the numbered checklist) to the user verbatim — no summarizing, no reformatting. Capture the `STATE_FILE:` path from STDERR.
150
+ 2. Wait for the user's chat reply, then invoke `skills/jenga/scripts/render-picker.sh <state_file> "<raw_reply>"` (continue mode).
151
+ - **Error turn** (plain text on STDOUT, state file retained) — relay verbatim and return to step 2 for another reply.
152
+ - **Cancellation** — relay the cancellation acknowledgement and halt the entire `/jenga` run; do not proceed to any later phase.
153
+ - **Resolved** (JSON object on STDOUT, state file removed) — extract `resolved_ids_csv` and continue to the shared confirmation step below.
154
+
155
+ #### Scoped branch (`/jenga <ids>`)
156
+
157
+ 1. Invoke `skills/jenga/scripts/resolve-id.sh "<raw argument>"` directly — the picker is skipped entirely in this branch.
158
+ 2. Parse the JSON array response, one object per comma-delimited input segment.
159
+ - If **every** segment has `status: "resolved"`, collect their `resolved_id` values into a comma-separated list and continue to the shared confirmation step below.
160
+ - If **any** segment has `status: "rejected"`, halt this phase (do not proceed to confirmation or Phase 1) and report each rejected segment's `input` and `reason` to the user verbatim, per `resolve-id.sh`'s own contract — a partial or ambiguous ID is never guessed. The user must re-invoke `/jenga <ids>` with corrected input.
161
+
162
+ #### Shared confirmation step (bare and scoped branches only)
163
+
164
+ 1. Invoke `skills/jenga/scripts/render-confirmation.sh "<comma-separated resolved ids from whichever branch above>"` (start mode). Relay STDOUT (the confirmation tree) to the user verbatim. Capture the `STATE_FILE:` path from STDERR.
165
+ 2. Wait for the user's chat reply, then invoke `skills/jenga/scripts/render-confirmation.sh <state_file> "<raw_reply>"` (continue mode).
166
+ - **Toggle or error turn** (plain text on STDOUT, state file retained) — relay verbatim and return to step 2 for another reply.
167
+ - **Cancellation** — relay the cancellation acknowledgement and halt the entire `/jenga` run; do not proceed to any later phase.
168
+ - **Confirmed** (JSON object on STDOUT, state file removed) — this is the final **scoped set**. Take `resolved_ids` (or `resolved_ids_csv`) as the exact set of board IDs Phases 1-4 restrict themselves to for the rest of this run.
169
+ 3. **Handoff to cascade resolution** — do not invoke `cascade-resolve.sh` again here. `render-confirmation.sh` already invoked it internally to build the tree, and the CONFIRMED JSON's own `undecomposed` field is that same result already scoped down to the checked-only set. Use that `undecomposed` field directly to identify which epics/stories in the scoped set still need Phase 1/2 decomposition.
170
+
171
+ After this phase completes (bare and scoped branches via confirmation, wildcard branch immediately), proceed to Phase 1.
172
+
173
+ ---
174
+
25
175
  ### Phase 1 — Decompose Epics into Stories
26
176
 
27
- Read all files in `project/board/epics/`. For each Epic that has no corresponding story files in `project/board/stories/` (i.e. no files whose name starts with that Epic's ID), invoke `/do` via a **scrum-master sub-agent** to break it down into Stories.
177
+ If Phase 0.75 produced a scoped set, restrict this phase to epics that are members of that set (directly selected, or flagged in its `undecomposed` list). Under `/jenga *`, this phase is unrestricted, exactly as before.
178
+
179
+ Read all files in `project/board/epics/`. For each in-scope Epic that has no corresponding story files in `project/board/stories/` (i.e. no files whose name starts with that Epic's ID), invoke `/do` via a **scrum-master sub-agent** to break it down into Stories.
28
180
 
29
- Repeat until every Epic has at least one Story on the board.
181
+ Repeat until every in-scope Epic has at least one Story on the board.
30
182
 
31
183
  ### Phase 2 — Decompose Stories into Tasks
32
184
 
33
- Read all files in `project/board/stories/`. For each Story that has no corresponding task files in `project/board/tasks/` (i.e. no files whose name starts with that Story's ID), invoke `/do` via a **scrum-master sub-agent** to break it down into Tasks.
185
+ If Phase 0.75 produced a scoped set, restrict this phase to stories that are members of that set (directly selected, expanded from an in-scope epic, or flagged in its `undecomposed` list). Under `/jenga *`, this phase is unrestricted, exactly as before.
186
+
187
+ Read all files in `project/board/stories/`. For each in-scope Story that has no corresponding task files in `project/board/tasks/` (i.e. no files whose name starts with that Story's ID), invoke `/do` via a **scrum-master sub-agent** to break it down into Tasks.
34
188
 
35
- Repeat until every Story has at least one Task on the board.
189
+ Repeat until every in-scope Story has at least one Task on the board.
36
190
 
37
191
  ### Phase 3 — Queue all Tasks into `todo.md`
38
192
 
39
- Read all files in `project/board/tasks/`. For every Task not already listed in `project/todo.md`, append its ID (and title as a comment) to `project/todo.md`.
193
+ If Phase 0.75 produced a scoped set, restrict this phase to tasks that are members of that set (directly selected, or expanded from an in-scope epic/story). Under `/jenga *`, this phase is unrestricted, exactly as before.
194
+
195
+ Read all files in `project/board/tasks/`. For every in-scope Task not already listed in `project/todo.md`, append its ID (and title as a comment) to `project/todo.md`.
196
+
197
+ After this phase, `todo.md` reflects the full set of in-scope work (or the full board, under `*`).
198
+
199
+ ### Phase 3.5 — Story-bundle detection
200
+
201
+ Before dispatching individual tasks in Phase 4, check each story for bundle eligibility. This phase runs once after Phase 3 completes.
202
+
203
+ If Phase 0.75 produced a scoped set, restrict this phase to stories that are members of that set (directly selected, or expanded from an in-scope epic) — a story with tasks sitting in `todo.md` from an earlier, differently-scoped run but that is **not** a member of the current run's scoped set is skipped entirely by this phase (not considered for bundling, and not dispatched via the bundle path) so that Phase 4's own scoped-set exclusion is never bypassed by a bundle call issued here. Under `/jenga *`, this phase is unrestricted, exactly as before.
204
+
205
+ For each in-scope story that has one or more tasks listed in `todo.md`:
40
206
 
41
- After this phase, `todo.md` reflects the full set of work on the board.
207
+ 1. **Read the story file** — parse the `tasks:` frontmatter array to get the ordered list of task IDs.
208
+ 2. **Guard: empty task list** — if the `tasks:` list is empty (zero entries), this story is **not** eligible for the bundle path. Skip to per-task dispatch in Phase 4.
209
+ 3. **Read each task file** — for every task ID in the `tasks:` list, read the corresponding task file from `project/board/tasks/`.
210
+ 4. **Collect `execution_scope`** — extract the `execution_scope` field from each task's YAML frontmatter. If the field is absent or has any value other than `story`, treat that task as **not** story-scoped.
211
+ 5. **Guard: locked-task disqualifier (defense-in-depth)** — for each task file already read in step 3, also read `crucial_level` (per `templates/SCRUM_BOARD_SCHEMA.md`'s Crucial Flag Fields). If **any** task in the story's `tasks:` list has `crucial_level: locked`, this story is **not** eligible for the bundle path — skip to per-task dispatch in Phase 4 for this story, **regardless of that task's `execution_scope` value**, even if it already reads `inline`. This check is defense-in-depth alongside Phase 0.5's Rule 4 (which forces a locked task's own `execution_scope` to `inline` when Rule 4 processes it): it exists for the race window where Rule 4 hasn't (yet) corrected the task — e.g. the task was added to the story's `tasks:` list after Rule 4 last ran, or the file was edited by hand after validation. It is not a replacement for Rule 4.
212
+ 6. **Apply the all-or-nothing rule** — a story qualifies for the bundle path **only if every task** in its `tasks:` list has `execution_scope: story`. A single task with a different scope (or a missing field) disqualifies the entire story.
213
+ 7. **Route bundle candidates** — if all tasks in the story are `execution_scope: story` and the list is non-empty:
214
+ a. Emit:
215
+ ```
216
+ BUNDLE DETECTED: story <E##_S##> — <N> story-scoped tasks will execute as a bundle.
217
+ ```
218
+ where `<E##_S##>` is the story ID and `<N>` is the count of tasks in the list.
219
+ b. Call `/do <E##_S##>` once (with the story ID, not individual task IDs). This invokes the bundle execution path in `/do` (implemented in E32_S05_T02), which runs all tasks sequentially in one shared worktree.
220
+ c. **Mark these tasks as bundled** — record their task IDs so Phase 4 skips individual dispatch for them.
221
+ 8. **Non-bundle stories** — stories with a mixed scope, a zero-length task list, any task missing `execution_scope: story`, or any task with `crucial_level: locked` (step 5) use the normal per-task dispatch in Phase 4 without any change.
42
222
 
43
223
  ### Phase 4 — Execute
44
224
 
45
- Loop through `todo.md` and execute all eligible items, running independent ones in parallel:
225
+ Loop through `todo.md` and execute all eligible items, running independent ones in parallel. Use the threshold values loaded in Phase 0 (`inline_max_files`, `inline_max_lines`, `story_max_files`, `bundle_lock_ttl_minutes`) when applying execution-scope logic to each task. **Skip any task that was bundled in Phase 3.5** — those tasks will be handled by the `/do` story-bundle call already issued.
46
226
 
47
- 1. **Collect eligible items** — from `todo.md`, find all items whose board file has `status: Pending` and no unresolved dependencies. A dependency is resolved if the blocking item's status is at least `Running` or `Passed`.
227
+ 1. **Collect eligible items** — from `todo.md`, find all items whose board file has `status: Pending` and no unresolved dependencies, **excluding tasks already dispatched as part of a story bundle in Phase 3.5**. If Phase 0.75 produced a scoped set, also exclude any item not a member of that set — execution never runs outside the confirmed/resolved scope. Under `/jenga *`, no such exclusion applies. A dependency is resolved if the blocking item's status is at least `In Progress` or `Passed`.
48
228
  2. **Group by parallelism** — items with no shared dependencies and no overlapping output files can run concurrently. Items that depend on each other must be sequenced.
49
229
  3. **Invoke `/do` in parallel** — launch each independent item as a **background sub-agent** simultaneously. Do not wait for one to finish before starting another if they are independent.
50
- 4. **Mark Running** — update `status: Running` in each launched item's board file (YAML front-matter) immediately after launch.
51
- 5. **Wait and loop** — once all active background agents have completed, return to step 1 of this phase to pick up any newly unblocked items.
230
+ 4. **Mark In Progress** — update `status: In Progress` in each launched item's board file (YAML front-matter) immediately after launch.
231
+ 5. **Wait, drain, and loop** — once all active background agents in the wave have completed:
232
+ a. **Drain the scrum triggers queue** — invoke the `## Drain Scrum Triggers Queue` procedure from `agents/scrum-master.md` against `project/queue/scrum_triggers.jsonl`. `/jenga`'s orchestrating agent is the scrum-master, and this is the same session-start procedure applied mid-run: process any `rapport_review`, `status_review`, and `story_rollup` triggers written by the tester sub-sessions that just completed, then clear the file. This ensures rollups become visible on the board (story/epic status updates) before the next wave is collected, instead of sitting unprocessed until some future scrum-master session start.
233
+ b. **Return to step 1** of this phase to pick up any newly unblocked items — including items unblocked by the rollups just processed in (a).
52
234
 
53
235
  ### Exit condition
54
236
 
@@ -63,6 +245,16 @@ When no eligible candidates remain in Phase 4, exit and output:
63
245
  - **Epic with no stories after breakdown** — log a warning and continue to the next Epic; do not block the pipeline.
64
246
  - **Story with no tasks after breakdown** — log a warning and continue to the next Story.
65
247
  - **Task already in `todo.md`** — skip; do not duplicate.
66
- - **All tasks in `todo.md` already Running/Passed** — exits cleanly with the completion message.
67
- - **Unresolved dependencies** — item is skipped in Phase 4 until its blockers are at least `Running`.
248
+ - **All tasks in `todo.md` already In Progress/Passed** — exits cleanly with the completion message.
249
+ - **Unresolved dependencies** — item is skipped in Phase 4 until its blockers are at least `In Progress`.
68
250
  - **`/do` failure (background agent)** — treated as a skip; mark the item's status back to `Pending` and continue the loop with remaining candidates.
251
+ - **Story with zero tasks (empty `tasks:` list)** — does not enter the bundle path in Phase 3.5; tasks (if any appear in `todo.md` independently) are dispatched normally in Phase 4.
252
+ - **Story with mixed `execution_scope` values** — falls back entirely to per-task dispatch in Phase 4; no partial bundling occurs.
253
+ - **Task file missing `execution_scope` field** — treated as not story-scoped; the containing story is disqualified from the bundle path.
254
+ - **Story containing a `crucial_level: locked` task** — disqualified from the bundle path at Phase 3.5 step 5, independent of that task's `execution_scope`; falls back to per-task dispatch in Phase 4, where `/do` Section 4.2's locked-task dispatch guard (E39_S03_T04) provides the second enforcement layer before any worktree or subagent is created.
255
+ - **Bundle `/do` call failure** — treated as a skip for the entire bundle; mark all bundled tasks' status back to `Pending` and continue Phase 4 with remaining non-bundled candidates.
256
+ - **Picker cancelled (bare branch)** — the entire `/jenga` run halts immediately after relaying the cancellation acknowledgement; no phase past 0.75 runs, and nothing on the board is modified.
257
+ - **Confirmation cancelled (bare or scoped branch)** — same as picker cancellation: the entire `/jenga` run halts immediately; no scoped set is produced and no later phase runs.
258
+ - **`resolve-id.sh` rejects one or more segments (scoped branch)** — the whole invocation halts at Phase 0.75 with the rejected segments' reasons reported verbatim; no partial scope is assembled from the segments that did resolve, and no fallback guess is made for the rejected ones. The user must re-invoke `/jenga <ids>` with corrected input.
259
+ - **`/jenga *` (wildcard branch)** — never produces a scoped set; Phases 1-4 run fully unrestricted over the entire board, identical to `/jenga`'s behavior before Phase 0.75 existed.
260
+ - **Stale out-of-scope story queued in `todo.md` from an earlier run (scoped run only)** — Phase 3.5's scoped-set guard skips it entirely (not considered for bundling), so it cannot be dispatched via a bundle `/do <E##_S##>` call that would otherwise bypass Phase 4's own scoped-set exclusion; it remains untouched in `todo.md` until a future run's scope includes it.
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env bash
2
+ # ---------------------------------------------------------------------------
3
+ # skills/jenga/scripts/board-scan.sh
4
+ #
5
+ # Deterministic board inventory for `/jenga`'s interactive scope-selection
6
+ # flow (E45). Walks project/board/epics/, project/board/stories/, and
7
+ # project/board/tasks/ and emits a structured JSON array describing every
8
+ # item on the board.
9
+ #
10
+ # This is the SINGLE SOURCE OF TRUTH for board contents in E45. Per this
11
+ # repo's "Scripts Over Inline Logic" principle, no other script or agent
12
+ # instruction may re-scan the board independently — every consumer below
13
+ # reads this script's stdout instead:
14
+ #
15
+ # - E45_S01_T02 (fuzzy-ID grammar parser) resolves user-typed IDs
16
+ # against the `id`/`type` fields this script emits.
17
+ # - E45_S01_T03 (cascade resolver) expands an epic/story
18
+ # selection to eligible descendants using `epic_id`/`story_id`/`status`.
19
+ # - E45_S02_T01 (interactive picker) renders the numbered
20
+ # hierarchical checklist from this inventory.
21
+ # - E45_S02_T02 (confirmation-tree renderer) renders the nested
22
+ # Epic > Story > Task confirmation tree from this inventory.
23
+ #
24
+ # ---------------------------------------------------------------------------
25
+ # USAGE
26
+ # ---------------------------------------------------------------------------
27
+ # skills/jenga/scripts/board-scan.sh
28
+ #
29
+ # No arguments. Reads project/board/ under the resolved project root (see
30
+ # lib/resolve-project-dir.sh — honours CLAUDE_PROJECT_DIR / the git repo
31
+ # root / cwd, in that priority order).
32
+ #
33
+ # ---------------------------------------------------------------------------
34
+ # OUTPUT SCHEMA (stable — downstream scripts depend on these exact names)
35
+ # ---------------------------------------------------------------------------
36
+ # stdout is a single JSON array. Nothing else is ever written to stdout.
37
+ # Each element is an object:
38
+ #
39
+ # {
40
+ # "id": "E45_S01_T01", // the item's own board ID
41
+ # "type": "epic" | "story" | "task",
42
+ # "epic_id": "E45", // this item's epic: itself (epic),
43
+ # // its parent (story/task), or
44
+ # // null if the frontmatter omits it
45
+ # "story_id": "E45_S01", // this item's story: itself
46
+ # // (story), its parent (task),
47
+ # // or null for epics / if absent
48
+ # "title": "Board scanner script — structured inventory ...",
49
+ # "status": "Pending", // verbatim value of the
50
+ # // frontmatter `status:` field
51
+ # "summary": "Everything else in E45 reads this script's output ...",
52
+ # "file": "project/board/tasks/E45_S01_T01_board-scanner-script.md"
53
+ # }
54
+ #
55
+ # Field notes:
56
+ # - `epic_id` / `story_id` are ALWAYS PRESENT KEYS, whose value is `null`
57
+ # when not applicable/absent — never an omitted key. Consumers should
58
+ # use a null-safe read (e.g. `.epic_id // empty` in jq), not assume the
59
+ # key exists with a non-null value.
60
+ # - `summary` is a single-line, best-effort excerpt:
61
+ # * epic -> first non-empty, non-heading line after "## Purpose"
62
+ # * story -> first non-empty line after the "# Story: <Title>"
63
+ # heading and before the next "##" heading (the
64
+ # "As a ... I want ... so that ..." statement)
65
+ # * task -> first non-empty, non-heading line after "## Description"
66
+ # `summary` is "" (empty string, never absent) when no matching section
67
+ # is found.
68
+ # - `file` is repo-relative (relative to the resolved project root), using
69
+ # forward slashes, suitable for display or re-opening the source file.
70
+ # - Array order: epics first, then stories, then tasks; within each type,
71
+ # files are sorted lexically by filename (stable across runs).
72
+ #
73
+ # ---------------------------------------------------------------------------
74
+ # ERROR HANDLING
75
+ # ---------------------------------------------------------------------------
76
+ # A single board file that fails to parse (unreadable, or missing the `id`
77
+ # frontmatter key) is SKIPPED and reported as a warning on stderr — it does
78
+ # NOT abort the scan or corrupt stdout. This is a deliberate design choice:
79
+ # the picker and confirmation renderer need a scan that degrades gracefully
80
+ # on one malformed file rather than producing no output for the whole board.
81
+ #
82
+ # Exit codes:
83
+ # 0 scan completed (stdout is always valid JSON on this path, even if
84
+ # some files were skipped with stderr warnings, and even if the
85
+ # result is an empty array because no board files exist yet)
86
+ # 1 the board root (project/board/) does not exist at all, or python3
87
+ # is not available — both are real setup problems, not per-file noise
88
+ #
89
+ # ---------------------------------------------------------------------------
90
+
91
+ set -euo pipefail
92
+
93
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
94
+
95
+ # Resolve JENGA_PROJECT_DIR the same way every other script in this repo
96
+ # does (CLAUDE_PROJECT_DIR -> git toplevel -> cwd). Falls back to locating
97
+ # the repo root relative to this script if the shared helper is missing.
98
+ if [ -f "$SCRIPT_DIR/../../../lib/resolve-project-dir.sh" ]; then
99
+ # shellcheck source=lib/resolve-project-dir.sh
100
+ source "$SCRIPT_DIR/../../../lib/resolve-project-dir.sh"
101
+ elif [ -n "${CLAUDE_PROJECT_DIR:-}" ]; then
102
+ JENGA_PROJECT_DIR="$CLAUDE_PROJECT_DIR"
103
+ else
104
+ JENGA_PROJECT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || pwd)"
105
+ fi
106
+
107
+ BOARD_DIR="$JENGA_PROJECT_DIR/project/board"
108
+
109
+ if [ ! -d "$BOARD_DIR" ]; then
110
+ echo "Error: board directory not found at $BOARD_DIR" >&2
111
+ exit 1
112
+ fi
113
+
114
+ if ! command -v python3 >/dev/null 2>&1; then
115
+ echo "Error: python3 is required by board-scan.sh" >&2
116
+ exit 1
117
+ fi
118
+
119
+ python3 - "$JENGA_PROJECT_DIR" "$BOARD_DIR" <<'PY'
120
+ import json
121
+ import re
122
+ import sys
123
+ from pathlib import Path
124
+
125
+ project_root = Path(sys.argv[1])
126
+ board_dir = Path(sys.argv[2])
127
+
128
+ FRONTMATTER_RE = re.compile(r'^---\n(.*?)\n---\n?(.*)$', re.DOTALL)
129
+ TOP_LEVEL_KV_RE = re.compile(r'^([A-Za-z_][A-Za-z0-9_]*):[ \t]*(.*)$')
130
+
131
+
132
+ def parse_frontmatter(text):
133
+ """Return (fields_dict, body_text). Only top-level `key: value` lines
134
+ are captured (indented list items such as ` - E01_S02` are skipped
135
+ intentionally — this scanner reads each board file directly and does
136
+ not need to follow parent->children ID lists)."""
137
+ m = FRONTMATTER_RE.match(text)
138
+ if not m:
139
+ return {}, text
140
+ fm_text, body = m.group(1), m.group(2)
141
+ fields = {}
142
+ for line in fm_text.splitlines():
143
+ if not line or line[0] in (' ', '\t', '#'):
144
+ continue
145
+ km = TOP_LEVEL_KV_RE.match(line)
146
+ if not km:
147
+ continue
148
+ key, val = km.group(1), km.group(2).strip()
149
+ val = val.strip('"').strip("'")
150
+ fields[key] = val
151
+ return fields, body
152
+
153
+
154
+ def first_line_after_heading(body, heading_re):
155
+ """First non-empty line strictly after a line matching heading_re, up
156
+ to (not including) the next '#'-prefixed heading line."""
157
+ lines = body.splitlines()
158
+ for i, line in enumerate(lines):
159
+ if heading_re.match(line.strip()):
160
+ for candidate in lines[i + 1:]:
161
+ stripped = candidate.strip()
162
+ if not stripped:
163
+ continue
164
+ if stripped.startswith('#'):
165
+ return ""
166
+ return stripped
167
+ return ""
168
+ return ""
169
+
170
+
171
+ PURPOSE_HEADING_RE = re.compile(r'^##\s+Purpose\s*$', re.IGNORECASE)
172
+ DESCRIPTION_HEADING_RE = re.compile(r'^##\s+Description\s*$', re.IGNORECASE)
173
+ TITLE_HEADING_RE = re.compile(r'^#\s+Story:.*$', re.IGNORECASE)
174
+
175
+
176
+ def summarize(item_type, body):
177
+ if item_type == "epic":
178
+ return first_line_after_heading(body, PURPOSE_HEADING_RE)
179
+ if item_type == "task":
180
+ return first_line_after_heading(body, DESCRIPTION_HEADING_RE)
181
+ # story: first non-empty line after the "# Story: <Title>" heading and
182
+ # before the next "##" heading.
183
+ return first_line_after_heading(body, TITLE_HEADING_RE)
184
+
185
+
186
+ TYPE_DIRS = [
187
+ ("epic", board_dir / "epics"),
188
+ ("story", board_dir / "stories"),
189
+ ("task", board_dir / "tasks"),
190
+ ]
191
+
192
+ items = []
193
+
194
+ for item_type, dir_path in TYPE_DIRS:
195
+ if not dir_path.is_dir():
196
+ continue
197
+ for f in sorted(dir_path.glob("*.md")):
198
+ try:
199
+ text = f.read_text(encoding="utf-8")
200
+ except Exception as e:
201
+ print(f"Warning: skipping {f}: read error: {e}", file=sys.stderr)
202
+ continue
203
+
204
+ fields, body = parse_frontmatter(text)
205
+ item_id = fields.get("id", "")
206
+ if not item_id:
207
+ print(f"Warning: skipping {f}: missing 'id' in frontmatter", file=sys.stderr)
208
+ continue
209
+
210
+ if item_type == "epic":
211
+ epic_id = item_id
212
+ story_id = None
213
+ elif item_type == "story":
214
+ epic_id = fields.get("epic_id") or None
215
+ story_id = item_id
216
+ else:
217
+ epic_id = fields.get("epic_id") or None
218
+ story_id = fields.get("story_id") or None
219
+
220
+ try:
221
+ rel_file = f.relative_to(project_root).as_posix()
222
+ except ValueError:
223
+ rel_file = f.as_posix()
224
+
225
+ items.append({
226
+ "id": item_id,
227
+ "type": item_type,
228
+ "epic_id": epic_id,
229
+ "story_id": story_id,
230
+ "title": fields.get("title", ""),
231
+ "status": fields.get("status", ""),
232
+ "summary": summarize(item_type, body),
233
+ "file": rel_file,
234
+ })
235
+
236
+ json.dump(items, sys.stdout, indent=2)
237
+ sys.stdout.write("\n")
238
+ PY