@jenga-ai/agent 1.3.0 → 3.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 (175) hide show
  1. package/README.md +93 -256
  2. package/agents/developer.md +9 -8
  3. package/agents/scrum-master.md +57 -23
  4. package/agents/tester.md +51 -5
  5. package/hooks/on_session_end.sh +13 -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 +197 -0
  9. package/lib/skill-allow-list.json +42 -0
  10. package/package.json +17 -13
  11. package/scripts/apply-j-prefix.sh +243 -0
  12. package/scripts/consume-context-digest.sh +103 -0
  13. package/scripts/generate-j-alias.sh +333 -0
  14. package/scripts/postinstall.js +25 -0
  15. package/scripts/sweep-stale-context-digests.sh +132 -0
  16. package/scripts/validate-board.sh +5 -0
  17. package/scripts/write-context-digest.sh +230 -0
  18. package/skills/{brainstorm → j-brainstorm}/SKILL.md +9 -2
  19. package/skills/{btw → j-btw}/SKILL.md +9 -2
  20. package/skills/{clearify → j-clearify}/SKILL.md +9 -2
  21. package/skills/{close-story → j-close-story}/SKILL.md +92 -13
  22. package/skills/j-close-story/scripts/check-privatized.sh +345 -0
  23. package/skills/{close-story → j-close-story}/scripts/check-story-closeable.sh +1 -1
  24. package/skills/{close-story → j-close-story}/scripts/extract-task-diff-stats.sh +1 -1
  25. package/skills/{commit → j-commit}/SKILL.md +9 -2
  26. package/skills/j-continue/SKILL.md +36 -0
  27. package/skills/{deep-dive → j-deep-dive}/SKILL.md +9 -8
  28. package/skills/{dev-done → j-dev-done}/SKILL.md +11 -4
  29. package/skills/{dev-done → j-dev-done}/scripts/classify-commit-outcome.sh +4 -4
  30. package/skills/{distribute → j-distribute}/SKILL.md +17 -10
  31. package/skills/{distribute → j-distribute}/scripts/distribute-changes.sh +1 -1
  32. package/skills/{do → j-do}/SKILL.md +111 -14
  33. package/skills/j-doc/README.md +155 -0
  34. package/skills/{doc → j-doc}/SKILL.md +55 -18
  35. package/skills/j-doc/authoring-notes.md +72 -0
  36. package/skills/j-doc/scripts/resolve_last_update.py +149 -0
  37. package/skills/{doc-sync → j-doc-sync}/SKILL.md +9 -2
  38. package/skills/{dooo → j-dooo}/SKILL.md +9 -2
  39. package/skills/j-error/SKILL.md +36 -0
  40. package/skills/{evaluate → j-evaluate}/SKILL.md +9 -2
  41. package/skills/j-examplify/SKILL.md +49 -0
  42. package/skills/{help → j-help}/SKILL.md +9 -2
  43. package/skills/{idea → j-idea}/SKILL.md +10 -3
  44. package/skills/{idea → j-idea}/assets/idea_handoff_template.md +1 -1
  45. package/skills/{improve → j-improve}/SKILL.md +9 -2
  46. package/skills/{init → j-init}/SKILL.md +21 -8
  47. package/skills/j-init/assets/scope-thresholds_template.json +7 -0
  48. package/skills/j-jbp/SKILL.md +32 -0
  49. package/skills/j-lgtm/SKILL.md +28 -0
  50. package/skills/{pi-plan → j-pi-plan}/SKILL.md +10 -3
  51. package/skills/{proceed → j-proceed}/SKILL.md +9 -2
  52. package/skills/{publish → j-publish}/SKILL.md +47 -40
  53. package/skills/{publish → j-publish}/adapters/droplet.md +1 -1
  54. package/skills/{publish → j-publish}/adapters/mobile-ios.md +3 -3
  55. package/skills/{publish → j-publish}/adapters/npm-ci.md +29 -7
  56. package/skills/{publish → j-publish}/adapters/npm.md +8 -8
  57. package/skills/{publish → j-publish}/assets/ci-contract.md +2 -2
  58. package/skills/{publish → j-publish}/schemas/publish.schema.json +1 -1
  59. package/skills/{publish → j-publish}/scripts/npm_ci_pipeline.sh +21 -1
  60. package/skills/{publish → j-publish}/scripts/npm_stage_inspect.sh +34 -1
  61. package/skills/{publish → j-publish}/scripts/npm_stage_pipeline.sh +9 -4
  62. package/skills/{publish → j-publish}/scripts/publish_deploy.sh +4 -4
  63. package/skills/{publish → j-publish}/scripts/validate_npm_stage_env.sh +1 -1
  64. package/skills/{publish → j-publish}/wizards/droplet.md +1 -1
  65. package/skills/{publish → j-publish}/wizards/mobile-ios.md +1 -1
  66. package/skills/{publish → j-publish}/wizards/npm-ci.md +1 -1
  67. package/skills/{publish → j-publish}/wizards/npm.md +1 -1
  68. package/skills/{reconcile → j-reconcile}/SKILL.md +12 -5
  69. package/skills/{reconcile → j-reconcile}/scripts/detect-unlinked-code.sh +2 -2
  70. package/skills/{reconcile → j-reconcile}/scripts/resolve-reconcile-scope.sh +3 -3
  71. package/skills/{reconcile-origin → j-reconcile-origin}/SKILL.md +13 -6
  72. package/skills/{redo → j-redo}/SKILL.md +9 -2
  73. package/skills/{skillify → j-skillify}/SKILL.md +10 -3
  74. package/skills/{spinoff → j-spinoff}/SKILL.md +9 -2
  75. package/skills/{status → j-status}/SKILL.md +9 -2
  76. package/skills/j-todo/SKILL.md +92 -0
  77. package/skills/{todo → j-todo}/assets/todo_handoff_template.md +1 -1
  78. package/skills/j-todo/scripts/add_trivial_task.sh +216 -0
  79. package/skills/j-todo/scripts/update_story_tasks.py +87 -0
  80. package/skills/{uncharted → j-uncharted}/SKILL.md +35 -28
  81. package/skills/{uncharted → j-uncharted}/assets/UNDERSTANDING_DOC_TEMPLATE.md +2 -2
  82. package/skills/{uncharted → j-uncharted}/scripts/detect-dependencies.sh +1 -1
  83. package/skills/{uncharted → j-uncharted}/scripts/detect-tests.sh +1 -1
  84. package/skills/{uncharted → j-uncharted}/scripts/directory-triage.sh +3 -3
  85. package/skills/{uncharted → j-uncharted}/scripts/elicitation-state.sh +3 -3
  86. package/skills/{uncharted → j-uncharted}/scripts/enumerate-target.sh +1 -1
  87. package/skills/{uncharted → j-uncharted}/scripts/import-source.sh +1 -1
  88. package/skills/{uncharted → j-uncharted}/scripts/inspect-provenance.sh +1 -1
  89. package/skills/{uncharted → j-uncharted}/scripts/resolve-segment-target.sh +5 -5
  90. package/skills/{uncharted → j-uncharted}/scripts/run-engine.sh +1 -1
  91. package/skills/{uncharted → j-uncharted}/scripts/validate-proposed-items.sh +2 -2
  92. package/skills/{uncharted → j-uncharted}/scripts/write-backfilled-epics.sh +1 -1
  93. package/skills/j-wtf/SKILL.md +27 -0
  94. package/skills/jenga/SKILL.md +1 -1
  95. package/skills/jenga/scripts/render-confirmation.sh +55 -18
  96. package/skills/jenga-permission-level/SKILL.md +1 -1
  97. package/templates/SCRUM_BOARD_SCHEMA.md +33 -2
  98. package/templates/agent-context.md.tpl +32 -9
  99. package/templates/copilot-instructions.md.tpl +66 -11
  100. package/skills/continue/SKILL.md +0 -29
  101. package/skills/error/SKILL.md +0 -29
  102. package/skills/examplify/SKILL.md +0 -42
  103. package/skills/init/assets/scope-thresholds_template.json +0 -7
  104. package/skills/jbp/SKILL.md +0 -25
  105. package/skills/lgtm/SKILL.md +0 -21
  106. package/skills/todo/SKILL.md +0 -48
  107. package/skills/wtf/SKILL.md +0 -20
  108. /package/skills/{close-story → j-close-story}/scripts/compute-scope-divergence.sh +0 -0
  109. /package/skills/{close-story → j-close-story}/scripts/extract-diff-stats.sh +0 -0
  110. /package/skills/{close-story → j-close-story}/scripts/update-task-frontmatter.sh +0 -0
  111. /package/skills/{commit → j-commit}/assets/user_instructions_template.md +0 -0
  112. /package/skills/{distribute → j-distribute}/CONFIG_SCHEMA.md +0 -0
  113. /package/skills/{distribute → j-distribute}/scripts/check-version.sh +0 -0
  114. /package/skills/{distribute → j-distribute}/scripts/commit-version-bump.sh +0 -0
  115. /package/skills/{do → j-do}/assets/intent-vs-diff-prompt.md +0 -0
  116. /package/skills/{do → j-do}/assets/sender_template.json +0 -0
  117. /package/skills/{doc → j-doc}/assets/path-objectives.yaml +0 -0
  118. /package/skills/{doc-sync → j-doc-sync}/assets/default_excludes.txt +0 -0
  119. /package/skills/{doc-sync → j-doc-sync}/assets/doc_targets.md +0 -0
  120. /package/skills/{evaluate → j-evaluate}/assets/evaluation_invokation_template.yml +0 -0
  121. /package/skills/{evaluate → j-evaluate}/assets/evaluation_rapport_template.md +0 -0
  122. /package/skills/{idea → j-idea}/assets/idea_template.md +0 -0
  123. /package/skills/{init → j-init}/assets/.gitignore_template +0 -0
  124. /package/skills/{init → j-init}/assets/PROJECT_SUMMARY_template.md +0 -0
  125. /package/skills/{init → j-init}/assets/directory_structure.txt +0 -0
  126. /package/skills/{init → j-init}/assets/strategy_stub_template.md +0 -0
  127. /package/skills/{init → j-init}/assets/test-config_template.json +0 -0
  128. /package/skills/{init → j-init}/assets/workflow_template.json +0 -0
  129. /package/skills/{init → j-init}/scripts/apply-project-visibility.sh +0 -0
  130. /package/skills/{init → j-init}/scripts/detect-existing-codebase.sh +0 -0
  131. /package/skills/{init → j-init}/scripts/init.sh +0 -0
  132. /package/skills/{pi-plan → j-pi-plan}/assets/epic.json +0 -0
  133. /package/skills/{pi-plan → j-pi-plan}/assets/story_template.md +0 -0
  134. /package/skills/{publish → j-publish}/assets/ExportOptions.plist.template +0 -0
  135. /package/skills/{publish → j-publish}/assets/ownership-matrix.md +0 -0
  136. /package/skills/{publish → j-publish}/assets/publish.example.json +0 -0
  137. /package/skills/{publish → j-publish}/assets/publish.example.npm-ci.json +0 -0
  138. /package/skills/{publish → j-publish}/assets/publish.example.npm.json +0 -0
  139. /package/skills/{publish → j-publish}/assets/secrets-guide.md +0 -0
  140. /package/skills/{publish → j-publish}/schemas/fixtures/npm-ci-minimal.json +0 -0
  141. /package/skills/{publish → j-publish}/schemas/fixtures/npm-ci-with-empty-secrets.json +0 -0
  142. /package/skills/{publish → j-publish}/schemas/fixtures/npm-ci-with-workflow-path.json +0 -0
  143. /package/skills/{publish → j-publish}/scripts/check_target_config.sh +0 -0
  144. /package/skills/{publish → j-publish}/scripts/droplet_pipeline.sh +0 -0
  145. /package/skills/{publish → j-publish}/scripts/finalize_changelog.sh +0 -0
  146. /package/skills/{publish → j-publish}/scripts/generate_release_notes.sh +0 -0
  147. /package/skills/{publish → j-publish}/scripts/ios_pipeline.sh +0 -0
  148. /package/skills/{publish → j-publish}/scripts/npm_pipeline.sh +0 -0
  149. /package/skills/{publish → j-publish}/scripts/publish_common.sh +0 -0
  150. /package/skills/{publish → j-publish}/scripts/reconcile_tags.sh +0 -0
  151. /package/skills/{publish → j-publish}/scripts/run_gates.sh +0 -0
  152. /package/skills/{publish → j-publish}/scripts/setup_wizard.sh +0 -0
  153. /package/skills/{publish → j-publish}/scripts/show_history.sh +0 -0
  154. /package/skills/{publish → j-publish}/scripts/suggest_semver_bump.sh +0 -0
  155. /package/skills/{publish → j-publish}/scripts/validate_config.sh +0 -0
  156. /package/skills/{publish → j-publish}/scripts/validate_droplet_env.sh +0 -0
  157. /package/skills/{publish → j-publish}/scripts/validate_ios_env.sh +0 -0
  158. /package/skills/{publish → j-publish}/scripts/validate_npm_ci_env.sh +0 -0
  159. /package/skills/{publish → j-publish}/scripts/validate_npm_env.sh +0 -0
  160. /package/skills/{publish → j-publish}/scripts/write_ledger_entry.sh +0 -0
  161. /package/skills/{reconcile → j-reconcile}/assets/report_format.md +0 -0
  162. /package/skills/{reconcile-origin → j-reconcile-origin}/scripts/reconcile-origin.sh +0 -0
  163. /package/skills/{skillify → j-skillify}/assets/init-new/SKILL.md +0 -0
  164. /package/skills/{skillify → j-skillify}/assets/init-new/assets/.gitignore_template +0 -0
  165. /package/skills/{skillify → j-skillify}/assets/init-new/assets/PROJECT_SUMMARY_template.md +0 -0
  166. /package/skills/{skillify → j-skillify}/assets/init-new/assets/directory_structure.txt +0 -0
  167. /package/skills/{skillify → j-skillify}/assets/init-new/assets/test-config_template.json +0 -0
  168. /package/skills/{skillify → j-skillify}/assets/init-new/assets/workflow_template.json +0 -0
  169. /package/skills/{skillify → j-skillify}/assets/init-new/scripts/init.sh +0 -0
  170. /package/skills/{skillify → j-skillify}/assets/init-old/SKILL.md +0 -0
  171. /package/skills/{status → j-status}/assets/output_format.md +0 -0
  172. /package/skills/{todo → j-todo}/assets/todo_template.md +0 -0
  173. /package/skills/{uncharted → j-uncharted}/assets/SEGMENT_PROPOSAL_TEMPLATE.md +0 -0
  174. /package/skills/{uncharted → j-uncharted}/scripts/apply-subsystem-cap.sh +0 -0
  175. /package/skills/{uncharted → j-uncharted}/scripts/discover-subsystems.sh +0 -0
@@ -63,7 +63,7 @@ This is the **very first thing** you do at the start of every session — before
63
63
 
64
64
  ## Drain Scrum Triggers Queue
65
65
 
66
- This is a self-contained procedure, not a session-start-only step. It may be invoked automatically at session start (see "Session Start — Queue Processing" below) **or** explicitly, mid-session, by another skill — for example `/jenga`'s Phase 4 loop, which runs as one long-lived scrum-master session and needs rollups to happen promptly after each wave of background agent completions rather than waiting for a future session start. Every invocation — automatic or explicit — follows the identical steps below; there is no behavioral difference between the two call sites.
66
+ This is a self-contained procedure, not a session-start-only step. It may be invoked automatically at session start (see "Session Start — Queue Processing" below) **or** explicitly, mid-session, by another skill — for example `j.jenga`'s Phase 4 loop, which runs as one long-lived scrum-master session and needs rollups to happen promptly after each wave of background agent completions rather than waiting for a future session start. Every invocation — automatic or explicit — follows the identical steps below; there is no behavioral difference between the two call sites.
67
67
 
68
68
  1. **Check `project/queue/scrum_triggers.jsonl`** — If the file exists and is non-empty, process each trigger in order:
69
69
  - `rapport_review`: Read each rapport file in `rapport_files` (skipping `*.IGNORE.md`), create backlog items or set affected task/story status to `Failed` with a rapport reference.
@@ -76,7 +76,7 @@ This is a self-contained procedure, not a session-start-only step. It may be inv
76
76
  6. **This is the only path** by which a mid-task agent request results in a `crucial_level` board write. Developer and tester never write `crucial_level`, `crucial_set_by`, or `crucial_note` directly to a board file themselves under any circumstance — they may only *request* the change via a `crucial_escalation` rapport, and the actual frontmatter write happens here, exclusively by scrum-master, closing the loop described in E39's Purpose section ("the actual frontmatter write still goes through scrum-master, never the subagent itself").
77
77
  - `status_review`: Review the scrum board for any tasks or stories whose status should be updated based on recent activity.
78
78
  - `story_rollup`: Check all tasks under the referenced story; if all are `Passed` or `Passed with remarks`, update the story status to `Passed` (or `Passed with remarks` if any remark exists). Then check epic rollup (see Rollup Logic).
79
- - `elicitation_resume`: A `/uncharted` conversational architecture elicitation session (`onboard`'s default flow, or `segment --mode investigate` — E20_S08_T03) ended mid-run without converging. Read `state_file` (`project/queue/elicitation-state/<elicitation_id>.json`, written by `skills/uncharted/scripts/elicitation-state.sh`) to see exactly where it left off — which nodes already converged, which are still pending or flagged, and any directory-triage/checkpoint data already confirmed — then resume the conversational flow documented in `skills/uncharted/SKILL.md`'s Multi-Session Persistence subsection from that point rather than restarting the elicitation from scratch. If the state file is missing or unreadable, report that to the user rather than silently starting a fresh elicitation under the same id.
79
+ - `elicitation_resume`: A `j.uncharted` conversational architecture elicitation session (`onboard`'s default flow, or `segment --mode investigate` — E20_S08_T03) ended mid-run without converging. Read `state_file` (`project/queue/elicitation-state/<elicitation_id>.json`, written by `skills/uncharted/scripts/elicitation-state.sh`) to see exactly where it left off — which nodes already converged, which are still pending or flagged, and any directory-triage/checkpoint data already confirmed — then resume the conversational flow documented in `skills/uncharted/SKILL.md`'s Multi-Session Persistence subsection from that point rather than restarting the elicitation from scratch. If the state file is missing or unreadable, report that to the user rather than silently starting a fresh elicitation under the same id.
80
80
  - After processing all triggers, **clear the file** by writing an empty file — do not leave processed triggers.
81
81
 
82
82
  2. **Check `project/queue/project_summary_updates.jsonl`** — If non-empty, review each proposed update and apply, revise, or reject it with a short note. Clear the file after processing.
@@ -197,6 +197,16 @@ Before writing each task file to `project/board/tasks/`, verify:
197
197
  4. `scope_rationale` is specific — not generic filler.
198
198
  5. If items 3 or 4 fail, change `execution_scope` to `task` and rewrite `scope_rationale` to reflect that fallback honestly.
199
199
 
200
+ ### Task-Folding Check
201
+
202
+ **Before creating a sibling task, verify it has independent work of its own.** This check is required before writing any task file to `project/board/tasks/`, in addition to the Breakdown Checklist above.
203
+
204
+ Ask: will this deliverable get done anyway as a side effect of an already-planned sibling task in the same story (e.g. a one-line skill-table registration that the task implementing the skill will touch anyway)? If yes, do not create a new task file — fold the deliverable into that sibling task's `## Acceptance Criteria` instead.
205
+
206
+ **Motivating example:** `E42_S04_T02` ("register `j.dev-done` in `CLAUDE.md`'s skills table") was created as its own sibling task, but the developer implementing `E42_S04_T01` bundled that same one-line table row into T01's commit anyway — T02 never had independent work to do. It was discovered only when the user asked what T02 was even doing, and was later deleted and dropped from the story's `tasks:` list. See `project/rapports/analysis/E42_S04-execution-overhead-postmortem.md`, Finding 4.
207
+
208
+ This check applies regardless of the sibling task's `execution_scope`.
209
+
200
210
  ---
201
211
 
202
212
  ## Execution Scope Assignment
@@ -213,6 +223,27 @@ Assign `inline` when **all** of the following are true:
213
223
 
214
224
  `needs_docs` for every `inline`-scoped task is always `false`.
215
225
 
226
+ ### `light` scope
227
+
228
+ `light` sits between `inline` and `task`: a single developer subagent pass with no worktree, self-verified via `scripts/smoke-harness.sh` in lieu of a separate tester invocation. If the smoke harness fails, execution falls back to `task` scope automatically at runtime — see `templates/SCRUM_BOARD_SCHEMA.md`'s Execution Scope Fields section for the full runtime contract.
229
+
230
+ Assign `light` when **any** of the following are true:
231
+ - The task exceeds `inline_max_files` or `inline_max_lines` (per `project/configs/scope-thresholds.json`), but remains a single, tightly-bounded change (one file, or a small handful of directly related files)
232
+ - The change is purely additive or config-level, like `inline`, but its estimated diff exceeds `inline_max_lines`
233
+ - The task introduces minor branching or a small conditional (not "non-trivial architecture" — that still requires `task`) that would otherwise disqualify it from `inline`, but the change is still self-contained enough to verify with a smoke-harness pass rather than a full tester cycle
234
+
235
+ **Distinguishing `light` from `inline`:** `light` is for changes too big or too branchy for `inline`'s caps (`inline_max_files`, `inline_max_lines`) — if the change fits within those caps with no logic branches, use `inline` instead.
236
+
237
+ **Distinguishing `light` from `task`:** assign `light`, not `task`, only when **all** of the following also hold:
238
+ - The task does **not** require worktree isolation — it can be implemented directly by a single developer subagent pass
239
+ - The task does **not** require independent tester verification — a `scripts/smoke-harness.sh` self-check is sufficient to catch regressions
240
+ - No shared-infrastructure contention exists (same contention concept as the `story` scope's mandatory contention check below — e.g. `package.json`, `settings.json`, `pyproject.toml`)
241
+ - The task has no cross-story dependencies and tester validation is not sensitive to the specific implementation approach chosen
242
+
243
+ If any of the `task`-scope triggers below apply (branching beyond "minor," shared infrastructure, cross-story dependencies, contention, or general uncertainty), do not assign `light` — use `task` instead.
244
+
245
+ **Fallback rule:** if the evidence for `light` isn't concrete — i.e. you cannot point to a specific reason the task exceeds `inline`'s caps while still being confidently worktree-free and tester-free — default to `task`, exactly as the general Fallback Rule above prescribes. Never assign `light` speculatively.
246
+
216
247
  ### `story` scope
217
248
 
218
249
  Assign `story` when **all** of the following are true:
@@ -339,12 +370,12 @@ When a request comes in:
339
370
  ### 3. Finalizing Items
340
371
  Once an item is sufficiently defined:
341
372
  - Use the appropriate command to register it on the scrum board:
342
- - `/todo` — add a new item
373
+ - `j.todo` — add a new item
343
374
  - `/amend` — update or refine an existing item
344
- - `/redo` — scrap and restart an item
375
+ - `j.redo` — scrap and restart an item
345
376
  - **Flag user-action prerequisites** — If the item requires the user to perform any action outside agent scope before or during implementation (e.g. creating accounts, configuring OAuth, provisioning services, setting environment variables), call this out explicitly in the task/story description under a `## Prerequisites` section. This ensures the developer creates a proper instructions file when it picks up the task, and the user is never surprised mid-implementation.
346
- - **Annotate documentation provenance when relevant** — When an epic, story, or task directly results in user-facing documentation updates, add an optional `docs` frontmatter field listing the affected documentation targets. This powers provenance tracking for the `/doc` skill.
347
- - **Purpose:** link board work to documentation files so `/doc` can resolve `last_update` frontmatter from real board history.
377
+ - **Annotate documentation provenance when relevant** — When an epic, story, or task directly results in user-facing documentation updates, add an optional `docs` frontmatter field listing the affected documentation targets. This powers provenance tracking for the `j.doc` skill.
378
+ - **Purpose:** link board work to documentation files so `j.doc` can resolve `last_update` frontmatter from real board history.
348
379
  - **When to add it:** use it when the item is expected to change docs such as `README.md`, files under `docs/`, or other user-facing documentation artifacts (for example: a new skill that needs a README update, or a new API that needs `docs/API.md`).
349
380
  - **How to populate it:** use repo-relative paths from the repository root, e.g. `docs: ["README.md", "docs/API.md"]`.
350
381
  - **Optionality:** do not add `docs` when no documentation target is directly affected; omitted `docs` is valid.
@@ -373,7 +404,7 @@ Before writing any new or amended story file to `project/board/stories/`, valida
373
404
  This gate applies to **all story creation and amendment operations** — no story file may be written to the board without passing all three checks.
374
405
 
375
406
  #### Triggering the Developer
376
- When board items are committed **and the user intends them for immediate implementation**, write a session handoff file to `project/queue/handoffs/scrum-master-<session_id>-<task_id>.json` — a unique path keyed by this session, not the old shared `project/queue/.session_handoff.json` slot, so that a session ending close to another agent's session can never clobber its handoff. Use the first entry of `task_ids` as `<task_id>` in the filename (or the literal string `batch` if `task_ids` is empty). `on_session_end.sh` forwards the work to the developer queue:
407
+ When board items are committed **and the user intends them for immediate implementation**, write a session handoff file to `project/queue/handoffs/scrum-master-<session_id>-<task_id>.json` — a unique path keyed by this session, not the old shared `project/queue/.session_handoff.json` slot, so that a session ending close to another agent's session can never clobber its handoff. Use the first entry of `task_ids` as `<task_id>` in the filename (or the literal string `batch` if `task_ids` is empty). Before writing the handoff, compose a short `resolved_context` digest of what was already resolved during breakdown for this task — which `templates/SCRUM_BOARD_SCHEMA.md` fields apply, which skill precedent governs, which epic/story-placement decisions were already made — and persist it by calling `scripts/write-context-digest.sh --agent scrum-master --session-id <session_id> --task-id <task_id>` with that content (stays under the ~100-line/few-hundred-token cap defined in `templates/SCRUM_BOARD_SCHEMA.md`'s `resolved_context` subsection; the script rejects oversized input rather than truncating it). Place the script's returned path in the handoff's `resolved_context` field. `on_session_end.sh` forwards the work to the developer queue:
377
408
 
378
409
  ```json
379
410
  {
@@ -383,10 +414,13 @@ When board items are committed **and the user intends them for immediate impleme
383
414
  "task_ids": ["<E##_S##_T##>", "..."],
384
415
  "story_id": "<E##_S##>",
385
416
  "epic_id": "<E##>",
417
+ "resolved_context": "<path returned by scripts/write-context-digest.sh, or omit if no digest was written>",
386
418
  "date": "<ISO 8601 UTC timestamp>"
387
419
  }
388
420
  ```
389
421
 
422
+ This digest is a starting point only, never a restriction: the developer may and should still read the full `templates/SCRUM_BOARD_SCHEMA.md`, relevant skill docs, or `CLAUDE.md` when the digest doesn't cover what it needs.
423
+
390
424
  If the user wants to defer implementation (e.g., brainstorming only, or items are backlogged for later), do **not** write the handoff file.
391
425
 
392
426
  ### 4. Definition of Done
@@ -407,7 +441,7 @@ If the user wants to defer implementation (e.g., brainstorming only, or items ar
407
441
 
408
442
  ## Brainstorm Mode
409
443
 
410
- When invoked via the `/brainstorm` skill, switch into **Brainstorm Mode**. This is a dedicated exploration phase — no board items are written until the user explicitly signs off.
444
+ When invoked via the `j.brainstorm` skill, switch into **Brainstorm Mode**. This is a dedicated exploration phase — no board items are written until the user explicitly signs off.
411
445
 
412
446
  In Brainstorm Mode, amplify the following behaviours:
413
447
 
@@ -454,28 +488,28 @@ Clarifications, follow-up details, and edge cases that serve the current story a
454
488
  When you detect a divergence, stop advancing the current thread and present the structured choice below. Use a calm, neutral tone — the goal is to keep the user in control, not to interrupt them:
455
489
 
456
490
  It looks like we're moving into a new topic. How would you like to handle it?
457
- 1. Capture the **new topic** as a `/todo` (I'll return to what we were working on)
458
- 2. Capture the **current topic** as a `/todo` (I'll continue with the new topic)
459
- 3. Capture **both** as `/todo` items (you choose which to continue first)
491
+ 1. Capture the **new topic** as a `j.todo` (I'll return to what we were working on)
492
+ 2. Capture the **current topic** as a `j.todo` (I'll continue with the new topic)
493
+ 3. Capture **both** as `j.todo` items (you choose which to continue first)
460
494
  4. Ignore it — tell me which topic to continue with
461
495
 
462
496
  ### Option A — Capture the Diverging Topic
463
- 1. Draft a `/todo` for the diverging topic. Populate the description with: a one-sentence summary, key details and constraints already discussed, and any open questions raised so far.
464
- 2. Before finalising, offer `/brainstorm` to fill in any missing **Prerequisites** (e.g. third-party accounts, environment setup, external approvals).
465
- 3. Once the `/todo` is saved, return to the primary story/epic context exactly where it was paused.
497
+ 1. Draft a `j.todo` for the diverging topic. Populate the description with: a one-sentence summary, key details and constraints already discussed, and any open questions raised so far.
498
+ 2. Before finalising, offer `j.brainstorm` to fill in any missing **Prerequisites** (e.g. third-party accounts, environment setup, external approvals).
499
+ 3. Once the `j.todo` is saved, return to the primary story/epic context exactly where it was paused.
466
500
 
467
501
  ### Option B — Capture the Primary Topic
468
- 1. Draft a `/todo` for the primary topic using the same context-surfacing approach: summary, details, open questions.
469
- 2. Offer `/brainstorm` to fill in missing Prerequisites before finalising.
470
- 3. Once the `/todo` is saved, pivot to the diverging topic.
502
+ 1. Draft a `j.todo` for the primary topic using the same context-surfacing approach: summary, details, open questions.
503
+ 2. Offer `j.brainstorm` to fill in missing Prerequisites before finalising.
504
+ 3. Once the `j.todo` is saved, pivot to the diverging topic.
471
505
 
472
506
  ### Option C — Capture Both
473
- 1. Create a `/todo` for the diverging topic (context summary + Prerequisites offer).
474
- 2. Create a `/todo` for the primary topic (context summary + Prerequisites offer).
507
+ 1. Create a `j.todo` for the diverging topic (context summary + Prerequisites offer).
508
+ 2. Create a `j.todo` for the primary topic (context summary + Prerequisites offer).
475
509
  3. Ask the user which topic to continue first.
476
510
 
477
511
  ### Context Surfacing
478
- Every `/todo` created through this flow must include in its description:
512
+ Every `j.todo` created through this flow must include in its description:
479
513
  - A one-sentence summary of the topic
480
514
  - Key details, constraints, or decisions already discussed
481
515
  - Open questions or unknowns raised so far
@@ -483,8 +517,8 @@ Every `/todo` created through this flow must include in its description:
483
517
  This is non-negotiable — it is the mechanism that prevents context loss.
484
518
 
485
519
  ### Edge Cases
486
- - **User declines both options (selects "Ignore it")**: Do not create any `/todo` items. Acknowledge briefly, then ask which topic to continue. Follow the user's direction without pressure.
487
- - **User wants to pursue both in parallel**: Treat as Option C — create both `/todo` items with full context summaries, then ask which to continue first.
520
+ - **User declines both options (selects "Ignore it")**: Do not create any `j.todo` items. Acknowledge briefly, then ask which topic to continue. Follow the user's direction without pressure.
521
+ - **User wants to pursue both in parallel**: Treat as Option C — create both `j.todo` items with full context summaries, then ask which to continue first.
488
522
  ---
489
523
 
490
524
  ## Mediator Mode
@@ -495,7 +529,7 @@ Activate Mediator Mode whenever the user is working on AI/ML model setup, traini
495
529
  - User asks which model architecture to use
496
530
  - User needs help choosing training hyperparameters or a framework
497
531
  - User wants to understand model evaluation results
498
- - User is about to run or configure a training job via the `/train` skill
532
+ - User is about to run or configure a training job via the `j.train` skill
499
533
 
500
534
  You do not need explicit instruction to enter Mediator Mode — detect the context and activate it automatically.
501
535
 
package/agents/tester.md CHANGED
@@ -155,7 +155,7 @@ When invoked to implement and/or run tests:
155
155
  e. Only after all DoD checkboxes are ticked (`- [x]`) may you proceed to step 7.
156
156
  7. Evaluate results and set the scrum board status accordingly (see Status Management)
157
157
  8. Trigger epic/story rollup if applicable (see Rollup Logic)
158
- 9. If there are unresolved findings, write a test rapport (see Rapport System)
158
+ 9. If there are unresolved findings, write a test rapport (see Rapport System) — **unless** the finding is clearly mechanical, in which case fix it in place instead; see "Trivial-Fix-Path (mechanical issues)" below before defaulting to a rapport.
159
159
  10. Report back with one of:
160
160
  - `"passed"` — all tests passed, no findings
161
161
  - `"passed with remarks"` — tests passed but findings exist; reference the rapport
@@ -163,6 +163,52 @@ When invoked to implement and/or run tests:
163
163
 
164
164
  Always include the sender object in the response.
165
165
 
166
+ ### Trivial-Fix-Path (mechanical issues)
167
+
168
+ **Trigger.** During steps 4-6 of "Invoked for test implementation and/or execution" above, you find a defect. Before defaulting to step 9's rapport path, classify it: is this **mechanical** (safe to fix in place) or does it **need a rapport** (a design decision or real risk is involved)? This classification is what determines which of the two remediation paths below you take — it is not optional bookkeeping, and it happens at the moment the defect is found, not retroactively.
169
+
170
+ **Motivating case.** `project/rapports/analysis/E42_S04-execution-overhead-postmortem.md` Finding 3: the tester found `skills/dev-done/scripts/classify-commit-outcome.sh` committed without its executable bit (`chmod +x`) plus a dead, unreachable error branch. Both were one-line-class fixes, but the only path available at the time was the full formal one — a `test_failure` rapport, a separate developer fix commit, a re-verification pass, and a second complete `j.self-sync` mirror run. That produced 3 of the task's 6 total commits for defects that needed no design judgment at all. This subsection exists so that pattern doesn't repeat.
171
+
172
+ **Qualifying examples (mechanical — fix in place).**
173
+ - A missing executable bit or other permission-bit error (e.g. a script committed without `chmod +x`) — the exact E42_S04 Finding 3 case.
174
+ - An unreachable/dead code branch that you have **directly exercised and confirmed dead** (not merely suspected) — e.g. an error-handling branch whose triggering condition can never occur given the surrounding logic, verified by tracing the actual call path, as in the second half of the E42_S04 Finding 3 case.
175
+ - A pure formatting slip with no behavior change — stray whitespace, a missing trailing newline, a lint-only violation that doesn't alter what the code does.
176
+
177
+ **Disqualifying examples (still needs a rapport — unaffected by this section).**
178
+ - Anything touching business logic — a conditional, a calculation, a control-flow change that affects output.
179
+ - Anything security-sensitive — auth, permissions checks (beyond a bare file-mode bit), input validation, secret handling.
180
+ - Anything touching data handling — persistence, migrations, data shape, serialization.
181
+ - Anything touching the shape of a public API, schema, or frontmatter contract.
182
+ - Anything requiring a judgment call about what the intended behavior actually is — if you have to guess at intent to decide the "right" fix, it is not mechanical.
183
+
184
+ When in doubt, treat the defect as disqualifying. The bar for "mechanical" is narrow on purpose — this path is an exception carved out of the rapport system, not a replacement for it.
185
+
186
+ **In-place fix mechanism.** Make the minimal fix directly in the worktree you are verifying, and commit it there. This follows the same rule already established in "Verification commit target" below: the commit lands on the branch under verification, in that task's worktree, never on `main` or any other branch — confirm with `git branch --show-current` before committing, same as any other verification-time commit. Use the same commit message convention used elsewhere in this file for verification-time fixes, e.g. `chore(<E##_S##_T##>): trivial fix — <short description>`.
187
+
188
+ **Audit trail.** A qualifying fix is never silent, even though no rapport is filed. Do both of the following:
189
+ 1. Append an event to `project/logs/events.json` with this shape, consistent with the file's existing `advisory_checkpoint`/`tool_approval` event shapes:
190
+
191
+ ```json
192
+ {
193
+ "event": "trivial_fix",
194
+ "agent": "tester",
195
+ "session_id": "<current session id>",
196
+ "task_id": "<E##_S##_T##>",
197
+ "story_id": "<E##_S##>",
198
+ "epic_id": "<E##>",
199
+ "commit_sha": "<sha of the in-place fix commit>",
200
+ "issue": "<what was found, e.g. 'missing chmod +x on scripts/foo.sh'>",
201
+ "note": "<one-line justification for why this qualifies as mechanical>",
202
+ "date": "<ISO 8601 UTC timestamp>"
203
+ }
204
+ ```
205
+
206
+ 2. Include a one-line note about the fix in whatever status/commit output you produce for this task (step 10's report, and the status note recorded per Status Management), so the fix is visible even to someone who never opens `events.json`.
207
+
208
+ **No rapport, no rework.** A qualifying trivial fix does **not** trigger a `test_failure` rapport and does **not** trigger a developer rework cycle. Verification simply continues from where it left off — proceed to step 7 (status evaluation) as if the defect had not blocked anything, with the fix itself noted per the audit trail above. Do not pause, do not hand back to the developer, do not wait for confirmation.
209
+
210
+ **No regression for anything else.** Anything that doesn't clearly qualify — including anything you're unsure about — is completely unaffected by this section: it goes through the existing rapport → rework cycle exactly as documented in "Test rapports (unresolved findings)" below, unchanged.
211
+
166
212
  ### Crucial Tier: `advisory`
167
213
 
168
214
  **Trigger.** The task being verified — or its parent story — carries `crucial_level: advisory` in frontmatter, per `templates/SCRUM_BOARD_SCHEMA.md`'s "Crucial Flag Fields (Story, Task)" section.
@@ -212,11 +258,11 @@ Append this as a new array entry — never overwrite existing log content. This
212
258
 
213
259
  **Trigger.** The task being verified — or its parent story — carries `crucial_level: locked` in frontmatter, per `templates/SCRUM_BOARD_SCHEMA.md`'s "Crucial Flag Fields (Story, Task)" section.
214
260
 
215
- **Effect — forced inline scope.** `execution_scope` is force-set to `inline` for any `locked` task, overriding whatever scope `/jenga`'s Execution Scope Assignment heuristics would otherwise assign — or auto-correcting a wrong value in place, with a logged `override_justification` note. The concrete mechanism is `skills/jenga/SKILL.md` Phase 0.5's **Rule 4 — `crucial_level: locked` forces `execution_scope: inline`** (added by E39_S03_T03). As tester, verify this field is actually `inline` on any `locked` item you're validating — a value that slipped through would itself be a defect worth flagging.
261
+ **Effect — forced inline scope.** `execution_scope` is force-set to `inline` for any `locked` task, overriding whatever scope `j.jenga`'s Execution Scope Assignment heuristics would otherwise assign — or auto-correcting a wrong value in place, with a logged `override_justification` note. The concrete mechanism is `skills/jenga/SKILL.md` Phase 0.5's **Rule 4 — `crucial_level: locked` forces `execution_scope: inline`** (added by E39_S03_T03). As tester, verify this field is actually `inline` on any `locked` item you're validating — a value that slipped through would itself be a defect worth flagging.
216
262
 
217
- **Effect — dispatch-time rejection of backgrounding.** A `locked` task can never be routed to a background subagent, a worktree-isolated session, or a bundled `/jenga` story-batch execution, regardless of its `execution_scope` value. This is enforced at two points, both added by E39_S03_T04: `skills/jenga/SKILL.md` Phase 3.5 step 5's **Guard: locked-task disqualifier (defense-in-depth)** and `skills/do/SKILL.md` Section 4.2's **Locked-task dispatch guard (defense-in-depth)**. This matters directly to you as tester: you must never yourself dispatch, recommend, or improvise a background subagent, a separate worktree-isolated session, or a bundled batch run in order to verify a `locked` item faster or in parallel with other work — verification of a `locked` item happens in the same foreground session the guards already pinned it to, same as implementation.
263
+ **Effect — dispatch-time rejection of backgrounding.** A `locked` task can never be routed to a background subagent, a worktree-isolated session, or a bundled `j.jenga` story-batch execution, regardless of its `execution_scope` value. This is enforced at two points, both added by E39_S03_T04: `skills/jenga/SKILL.md` Phase 3.5 step 5's **Guard: locked-task disqualifier (defense-in-depth)** and `skills/do/SKILL.md` Section 4.2's **Locked-task dispatch guard (defense-in-depth)**. This matters directly to you as tester: you must never yourself dispatch, recommend, or improvise a background subagent, a separate worktree-isolated session, or a bundled batch run in order to verify a `locked` item faster or in parallel with other work — verification of a `locked` item happens in the same foreground session the guards already pinned it to, same as implementation.
218
264
 
219
- **No agent-discretion obligation.** Unlike `advisory` (a reporting-cadence habit) and `gated` (a confirmation you must actively pause and perform), `locked` requires no judgment call from you. It is fully enforced by pre-flight validation (Rule 4) and dispatch-time guards (the Phase 3.5 and `/do` guards above) before the developer ever begins work — none of this depends on you noticing or remembering anything mid-verification. Your only obligation is to recognize that a `locked` task always runs (and was always verified) in the current foreground session, and to never suggest or perform a workaround that would route around that guarantee. If you find evidence during verification that a `locked` item was actually run in a backgrounded or worktree-isolated context, treat that as a guard failure worth flagging (see Rapport System), not something to silently pass.
265
+ **No agent-discretion obligation.** Unlike `advisory` (a reporting-cadence habit) and `gated` (a confirmation you must actively pause and perform), `locked` requires no judgment call from you. It is fully enforced by pre-flight validation (Rule 4) and dispatch-time guards (the Phase 3.5 and `j.do` guards above) before the developer ever begins work — none of this depends on you noticing or remembering anything mid-verification. Your only obligation is to recognize that a `locked` task always runs (and was always verified) in the current foreground session, and to never suggest or perform a workaround that would route around that guarantee. If you find evidence during verification that a `locked` item was actually run in a backgrounded or worktree-isolated context, treat that as a guard failure worth flagging (see Rapport System), not something to silently pass.
220
266
 
221
267
  ### Invoked for analysis or comparison testing
222
268
  When invoked to run an analysis or comparison:
@@ -409,7 +455,7 @@ There is no default analytics run. Analytics only happen when explicitly scoped
409
455
 
410
456
  ## Investigative Mode
411
457
 
412
- **Trigger.** You are sometimes dispatched not to validate a task's implementation, but purely to build understanding of what the existing test suite actually covers for a named flow or target — e.g. by the scrum-master during `/uncharted`'s conversational architecture elicitation, alongside the developer's Investigative Mode pass over the same flow. This is a distinct dispatch mode from the standard Sender Object / Task Intake / Status Management flow above, recognized by the request itself (you are asked to *trace coverage*, not to *validate a task*), not by any board field.
458
+ **Trigger.** You are sometimes dispatched not to validate a task's implementation, but purely to build understanding of what the existing test suite actually covers for a named flow or target — e.g. by the scrum-master during `j.uncharted`'s conversational architecture elicitation, alongside the developer's Investigative Mode pass over the same flow. This is a distinct dispatch mode from the standard Sender Object / Task Intake / Status Management flow above, recognized by the request itself (you are asked to *trace coverage*, not to *validate a task*), not by any board field.
413
459
 
414
460
  **Hard constraints.** Investigative Mode is strictly read-only:
415
461
  - No worktree is created for write purposes, no test files are written or modified, no test runs that mutate state, no dependency installs.
@@ -413,4 +413,16 @@ done
413
413
  # --- 5. Todo cleanup ---
414
414
  # Remove project/todo.md if it is effectively empty (only blanks, # Todo, and HTML comments).
415
415
  # Runs unconditionally on every session end regardless of agent type.
416
- bash "$PROJECT_DIR/scripts/todo_cleanup.sh"
416
+ bash "$PROJECT_DIR/scripts/todo_cleanup.sh"
417
+
418
+ # --- 6. Stale resolved_context digest sweep (E49_S01_T02) ---
419
+ # Backstop cleanup for project/queue/context/ digest files. The primary
420
+ # cleanup path is scripts/consume-context-digest.sh, called by the receiving
421
+ # subagent once it has read its digest — this sweep only catches a digest
422
+ # whose intended receiver never consumed it (abandoned dispatch, or a
423
+ # receiver that read the raw file directly and forgot to delete it). Age-based
424
+ # rather than routed-and-deleted-immediately like section 4 above, because a
425
+ # digest's consumer is a later session that may not have started yet when
426
+ # THIS session ends — see scripts/sweep-stale-context-digests.sh's header for
427
+ # the full rationale. Runs unconditionally, same as todo cleanup above.
428
+ bash "$PROJECT_DIR/scripts/sweep-stale-context-digests.sh"
@@ -29,6 +29,7 @@
29
29
  import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, realpathSync } from "fs";
30
30
  import { join, dirname } from "path";
31
31
  import { fileURLToPath } from "url";
32
+ import { readSkillAllowList } from "./generate-skill-allow-list.js";
32
33
 
33
34
  // This file lives at <package>/lib/generate-agent-context.js — one level up
34
35
  // is the installed jenga-agent package root, which holds templates/.
@@ -76,6 +77,20 @@ function buildSkillList(projectRoot) {
76
77
  return skillLines.length > 0 ? skillLines.join("\n") : "_No skills found._";
77
78
  }
78
79
 
80
+ /**
81
+ * Build the rendered {{ALLOWED_SKILL_IDS}} block: the canonical, committed
82
+ * `lib/skill-allow-list.json` artifact (E50_S02_T01/T02), rendered as a
83
+ * compact comma-joined inline-code list for the "Skill Identifier
84
+ * Allow-List" section of the generated context files (E50_S02_T04). Reads
85
+ * via the shared readSkillAllowList() helper — never re-derives its own
86
+ * scan of skills/, so this list always matches the artifact the MCP router
87
+ * (E50_S02_T03) and postinstall/self-sync regeneration also read from.
88
+ */
89
+ function buildAllowedSkillIds(projectRoot, packageRoot) {
90
+ const ids = readSkillAllowList(projectRoot, packageRoot);
91
+ return ids.length > 0 ? ids.map((id) => `\`${id}\``).join(", ") : "_No skills found._";
92
+ }
93
+
79
94
  function resolveTemplatePath(projectRoot, packageRoot) {
80
95
  const candidates = [
81
96
  join(packageRoot, "templates", "agent-context.md.tpl"),
@@ -167,13 +182,15 @@ export function generateAgentContext(projectRoot = process.cwd(), packageRoot =
167
182
 
168
183
  const tpl = readFileSync(tplPath, "utf8");
169
184
  const skillList = buildSkillList(projectRoot);
185
+ const allowedSkillIds = buildAllowedSkillIds(projectRoot, packageRoot);
170
186
  const written = [];
171
187
 
172
188
  for (const target of TARGETS) {
173
189
  const rendered = tpl
174
190
  .split("{{TARGET_FILENAME}}").join(target.filename)
175
191
  .split("{{SKILL_DISCOVERY_PATH}}").join(target.skillDiscoveryPath)
176
- .split("{{SKILL_LIST}}").join(skillList);
192
+ .split("{{SKILL_LIST}}").join(skillList)
193
+ .split("{{ALLOWED_SKILL_IDS}}").join(allowedSkillIds);
177
194
 
178
195
  const targetPath = join(projectRoot, target.filename);
179
196
  const jFilename = `J-${target.filename}`;
@@ -33,6 +33,7 @@
33
33
  import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs";
34
34
  import { join, dirname } from "path";
35
35
  import { fileURLToPath } from "url";
36
+ import { readSkillAllowList } from "./generate-skill-allow-list.js";
36
37
 
37
38
  // This file lives at <package>/lib/generate-copilot-instructions.js — one level up is the
38
39
  // installed jenga-agent package root, which holds templates/.
@@ -72,6 +73,19 @@ function buildSkillList(skillsDir) {
72
73
  return skillLines.length > 0 ? skillLines.join("\n") : "_No skills found._";
73
74
  }
74
75
 
76
+ /**
77
+ * Build the rendered {{ALLOWED_SKILL_IDS}} block: the canonical, committed
78
+ * `lib/skill-allow-list.json` artifact (E50_S02_T01/T02), rendered as a compact comma-joined
79
+ * inline-code list for the "Routing decision table" allow-list check (E50_S02_T04). Reads via
80
+ * the shared readSkillAllowList() helper — never re-derives its own scan of a skills
81
+ * directory, mirroring the sibling buildAllowedSkillIds in lib/generate-agent-context.js so
82
+ * both routing paths share the one committed artifact.
83
+ */
84
+ function buildAllowedSkillIds(projectRoot, packageRoot) {
85
+ const ids = readSkillAllowList(projectRoot, packageRoot);
86
+ return ids.length > 0 ? ids.map((id) => `\`${id}\``).join(", ") : "_No skills found._";
87
+ }
88
+
75
89
  /**
76
90
  * Replace only the JENGA:START..JENGA:END block in an existing copilot-instructions.md with the
77
91
  * freshly rendered one, preserving everything outside the markers. If no existing file, write
@@ -129,7 +143,10 @@ export function generateCopilotInstructions(
129
143
  ? (skillsDir.startsWith("/") ? skillsDir : join(projectRoot, skillsDir))
130
144
  : undefined;
131
145
  const skillList = buildSkillList(resolvedSkillsDir);
132
- const rendered = tpl.replace("{{SKILL_LIST}}", skillList);
146
+ const allowedSkillIds = buildAllowedSkillIds(projectRoot, packageRoot);
147
+ const rendered = tpl
148
+ .replace("{{SKILL_LIST}}", skillList)
149
+ .replace("{{ALLOWED_SKILL_IDS}}", allowedSkillIds);
133
150
 
134
151
  const githubDir = join(projectRoot, ".github");
135
152
  const copilotInstructionsPath = join(githubDir, "copilot-instructions.md");
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/generate-skill-allow-list.js — canonical skill allow-list generator
4
+ *
5
+ * Single source of truth for the `j:`-prefix anti-masquerading guard (E50_S02). Scans a skills
6
+ * directory for every `<name>/SKILL.md`, extracts each one's canonical `name:` frontmatter
7
+ * identifier, and writes a sorted, de-duplicated JSON artifact to `lib/skill-allow-list.json`.
8
+ *
9
+ * Why this exists: E50_S02's guard checks whether a `j:`-prefixed invocation matches a known
10
+ * list of genuine Jenga skill identifiers before treating it as trusted (see
11
+ * docs/skill-authoring.md's "Threat Model — the j: Allow-List Guard" section, landed by
12
+ * E50_S02_T05, for the guard's exact scope boundary). Every routing path that enforces that
13
+ * guard — the MCP router (E50_S02_T03) and native/prose routing enforcement (E50_S02_T04) — must
14
+ * read from this single artifact rather than re-deriving its own scan of `skills/`, mirroring the
15
+ * drift lesson already documented in E41_S04 (CLAUDE.md and AGENT.md hand-maintained skill lists
16
+ * had already drifted from each other before that epic unified them onto one generator).
17
+ *
18
+ * This module only builds the generator and produces the one-time committed artifact.
19
+ * Auto-regeneration at `/self-sync`/postinstall time is wired up separately by the follow-up
20
+ * task E50_S02_T02 — not in scope here.
21
+ *
22
+ * Consumers:
23
+ * - CLI guard at the bottom of this file (`node lib/generate-skill-allow-list.js`) — run once
24
+ * against this repo's own skills/ to produce the committed lib/skill-allow-list.json, and
25
+ * intended to be callable from skills/self-sync/scripts/run.js and scripts/postinstall.js in
26
+ * the follow-up task.
27
+ * - getSkillAllowListIdentifiers() — an in-memory-only helper for callers that want the
28
+ * identifier list without touching disk, e.g. the MCP router work in E50_S02_T03.
29
+ *
30
+ * ESM, Node built-ins only — mirrors lib/generate-agent-context.js and
31
+ * lib/generate-copilot-instructions.js.
32
+ */
33
+
34
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, realpathSync } from "fs";
35
+ import { join, dirname } from "path";
36
+ import { fileURLToPath } from "url";
37
+
38
+ const __dirname = dirname(fileURLToPath(import.meta.url));
39
+
40
+ // This file lives at <package>/lib/generate-skill-allow-list.js — one level up is the repo/
41
+ // package root, which holds skills/.
42
+ const DEFAULT_SKILLS_DIR = join(__dirname, "..", "skills");
43
+ const DEFAULT_OUTPUT_PATH = join(__dirname, "skill-allow-list.json");
44
+ const DEFAULT_PACKAGE_ROOT = join(__dirname, "..");
45
+
46
+ /**
47
+ * Extract the `name:` frontmatter field from a SKILL.md's content. Only the scalar `name` field
48
+ * is needed here (unlike mcp/router/skill-index.js's fuller frontmatter parser, which also
49
+ * handles array fields like `keywords`/`examples`) — a direct regex against the frontmatter
50
+ * block is sufficient and avoids re-implementing that broader parser for a single field.
51
+ */
52
+ function extractName(content) {
53
+ const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
54
+ if (!fmMatch) return null;
55
+ const nameMatch = fmMatch[1].match(/^name:\s*(.+)$/m);
56
+ if (!nameMatch) return null;
57
+ const rawName = nameMatch[1].trim().replace(/^["']|["']$/g, "");
58
+ // Frontmatter may carry the pre-E50_S01 bare form, the E50_S01 "j:<name>" form, or the
59
+ // current "j.<name>" form (E50_S07_T01 swapped the separator because GitHub Copilot CLI
60
+ // rejects ":" in a skill name) — strip whichever prefix is present so the allow-list always
61
+ // holds bare identifiers, matching mcp/router/skill-index.js's `bareName` handling.
62
+ //
63
+ // Both separators must stay accepted: this guard is the E50_S02 anti-masquerading check, and
64
+ // a normalizer that fails to strip silently populates the allow-list with prefixed entries
65
+ // ("j.brainstorm" instead of "brainstorm"), which no longer match what the guard compares
66
+ // against. That is a security-guard failure, not a cosmetic one.
67
+ return rawName.replace(/^j[.:]/i, "");
68
+ }
69
+
70
+ /**
71
+ * Scan `skillsDir` for every immediate `<name>/SKILL.md` and return a sorted, de-duplicated
72
+ * array of canonical skill identifiers (the frontmatter `name` value, treated as authoritative
73
+ * per docs/skill-authoring.md's "name must match the directory name" authoring rule — this
74
+ * function does not itself verify that match, only that `name` is present).
75
+ *
76
+ * A SKILL.md missing the `name` field is skipped with a warning, not fatal — mirrors
77
+ * mcp/router/skill-index.js's buildSkillIndex warning behavior for consistency across the two
78
+ * scanners.
79
+ *
80
+ * No disk write. Pure in-memory scan, for callers (e.g. the MCP router, E50_S02_T03) that want
81
+ * the identifier list without reading the generated artifact.
82
+ *
83
+ * @param {string} skillsDir - directory to scan (default: this repo/package's own skills/)
84
+ * @returns {string[]} sorted, de-duplicated skill identifiers
85
+ */
86
+ export function getSkillAllowListIdentifiers(skillsDir = DEFAULT_SKILLS_DIR) {
87
+ if (!existsSync(skillsDir)) return [];
88
+
89
+ const identifiers = new Set();
90
+
91
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
92
+ if (!entry.isDirectory()) continue;
93
+
94
+ const skillMdPath = join(skillsDir, entry.name, "SKILL.md");
95
+ if (!existsSync(skillMdPath)) continue;
96
+
97
+ let content;
98
+ try {
99
+ content = readFileSync(skillMdPath, "utf8");
100
+ } catch (e) {
101
+ process.stderr.write(`Warning: failed to read ${skillMdPath}: ${e.message} — skipped\n`);
102
+ continue;
103
+ }
104
+
105
+ const name = extractName(content);
106
+ if (!name) {
107
+ process.stderr.write(`Warning: ${skillMdPath} has no 'name' in frontmatter — skipped\n`);
108
+ continue;
109
+ }
110
+
111
+ identifiers.add(name);
112
+ }
113
+
114
+ return [...identifiers].sort();
115
+ }
116
+
117
+ /**
118
+ * Read the committed skill-allow-list.json artifact (produced by generateSkillAllowList /
119
+ * regenerated by postinstall.js and skills/self-sync/scripts/run.js) and return its `skills`
120
+ * array. Resolution order mirrors resolveTemplatePath's packageRoot-then-projectRoot candidate
121
+ * order in lib/generate-agent-context.js and lib/generate-copilot-instructions.js: the artifact
122
+ * normally lives at `<packageRoot>/lib/skill-allow-list.json` (the installed jenga-agent
123
+ * package's own committed copy), with `<projectRoot>/lib/skill-allow-list.json` as a fallback
124
+ * for the case where this repo IS the project (e.g. this monorepo's own dogfood run).
125
+ *
126
+ * This is the single read path both E50_S02_T04 generators (native Claude Code context files
127
+ * and Copilot/Codex prose-routing instructions) use to render the "trusted identifiers" list —
128
+ * neither one re-derives its own scan of skills/, per the drift lesson already documented in
129
+ * E41_S04 and referenced in this module's header comment.
130
+ *
131
+ * @param {string} projectRoot - project root directory
132
+ * @param {string} packageRoot - installed jenga-agent package root (default: this module's own
133
+ * package root)
134
+ * @returns {string[]} the artifact's `skills` array, or [] if the artifact is missing/unparseable
135
+ */
136
+ export function readSkillAllowList(projectRoot, packageRoot = DEFAULT_PACKAGE_ROOT) {
137
+ const candidates = [
138
+ join(packageRoot, "lib", "skill-allow-list.json"),
139
+ join(projectRoot, "lib", "skill-allow-list.json"),
140
+ ];
141
+ const path = candidates.find(existsSync);
142
+ if (!path) return [];
143
+
144
+ try {
145
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
146
+ return Array.isArray(parsed.skills) ? parsed.skills : [];
147
+ } catch (e) {
148
+ process.stderr.write(`Warning: failed to read/parse ${path}: ${e.message} — treating allow-list as empty\n`);
149
+ return [];
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Generate the canonical skill allow-list artifact at `outputPath`.
155
+ *
156
+ * The `skills` array is deterministic — sorted and de-duplicated, so repeat runs with no
157
+ * `skills/` change produce a byte-identical array. `generated_at` is NOT deterministic (it is a
158
+ * fresh timestamp on every run) — that is expected and intentional, not a bug: only the `skills`
159
+ * array itself is contracted to be stable.
160
+ *
161
+ * @param {string} skillsDir - directory to scan (default: this repo/package's own skills/)
162
+ * @param {string} outputPath - where to write the JSON artifact (default: lib/skill-allow-list.json)
163
+ * @returns {{written: boolean, path: string, skill_count: number}}
164
+ */
165
+ export function generateSkillAllowList(skillsDir = DEFAULT_SKILLS_DIR, outputPath = DEFAULT_OUTPUT_PATH) {
166
+ const skills = getSkillAllowListIdentifiers(skillsDir);
167
+
168
+ const artifact = {
169
+ generated_at: new Date().toISOString(),
170
+ skill_count: skills.length,
171
+ skills,
172
+ };
173
+
174
+ const outDir = dirname(outputPath);
175
+ if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
176
+
177
+ writeFileSync(outputPath, JSON.stringify(artifact, null, 2) + "\n", "utf8");
178
+
179
+ return { written: true, path: outputPath, skill_count: skills.length };
180
+ }
181
+
182
+ // CLI guard — allows `node lib/generate-skill-allow-list.js [skillsDir] [outputPath]`, intended
183
+ // to be callable from skills/self-sync/scripts/run.js and scripts/postinstall.js in the
184
+ // follow-up auto-regeneration task (E50_S02_T02).
185
+ //
186
+ // process.argv[1] is compared via realpath, not as a raw string — see the identical comment
187
+ // block in lib/generate-agent-context.js for why: Node resolves import.meta.url through symlinks
188
+ // when loading an ES module, but leaves process.argv[1] exactly as the shell passed it, so an
189
+ // invocation from a path under a symlinked directory (e.g. macOS's /tmp -> /private/tmp) would
190
+ // otherwise silently fail this comparison and skip generation with no error.
191
+ const invokedPath = process.argv[1] ? realpathSync(process.argv[1]) : null;
192
+ if (invokedPath === fileURLToPath(import.meta.url)) {
193
+ const skillsDirArg = process.argv[2] || DEFAULT_SKILLS_DIR;
194
+ const outputPathArg = process.argv[3] || DEFAULT_OUTPUT_PATH;
195
+ const result = generateSkillAllowList(skillsDirArg, outputPathArg);
196
+ console.log(`✓ ${result.path} (${result.skill_count} skills)`);
197
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "generated_at": "2026-09-06T15:53:37.575Z",
3
+ "skill_count": 36,
4
+ "skills": [
5
+ "brainstorm",
6
+ "btw",
7
+ "clearify",
8
+ "close-story",
9
+ "commit",
10
+ "continue",
11
+ "deep-dive",
12
+ "dev-done",
13
+ "distribute",
14
+ "do",
15
+ "doc",
16
+ "doc-sync",
17
+ "dooo",
18
+ "error",
19
+ "evaluate",
20
+ "examplify",
21
+ "help",
22
+ "idea",
23
+ "improve",
24
+ "init",
25
+ "jbp",
26
+ "jenga",
27
+ "jenga-permission-level",
28
+ "lgtm",
29
+ "pi-plan",
30
+ "proceed",
31
+ "publish",
32
+ "reconcile",
33
+ "reconcile-origin",
34
+ "redo",
35
+ "skillify",
36
+ "spinoff",
37
+ "status",
38
+ "todo",
39
+ "uncharted",
40
+ "wtf"
41
+ ]
42
+ }