@jenga-ai/agent 1.2.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +97 -91
  2. package/agents/developer.md +26 -7
  3. package/agents/scrum-master.md +57 -22
  4. package/agents/tester.md +68 -4
  5. package/hooks/on_session_end.sh +40 -1
  6. package/lib/generate-agent-context.js +18 -1
  7. package/lib/generate-copilot-instructions.js +18 -1
  8. package/lib/generate-skill-allow-list.js +191 -0
  9. package/lib/skill-allow-list.json +43 -0
  10. package/package.json +35 -20
  11. package/scripts/apply-j-prefix.sh +230 -0
  12. package/scripts/consume-context-digest.sh +103 -0
  13. package/scripts/postinstall.js +25 -0
  14. package/scripts/sweep-stale-context-digests.sh +132 -0
  15. package/scripts/validate-board.sh +5 -0
  16. package/scripts/write-context-digest.sh +230 -0
  17. package/skills/brainstorm/SKILL.md +1 -1
  18. package/skills/btw/SKILL.md +1 -1
  19. package/skills/clearify/SKILL.md +1 -1
  20. package/skills/close-story/SKILL.md +78 -6
  21. package/skills/close-story/scripts/check-privatized.sh +345 -0
  22. package/skills/commit/SKILL.md +12 -2
  23. package/skills/continue/SKILL.md +1 -1
  24. package/skills/deep-dive/SKILL.md +1 -1
  25. package/skills/dev-done/SKILL.md +46 -0
  26. package/skills/dev-done/scripts/classify-commit-outcome.sh +114 -0
  27. package/skills/distribute/SKILL.md +1 -1
  28. package/skills/do/SKILL.md +100 -10
  29. package/skills/doc/README.md +155 -0
  30. package/skills/doc/SKILL.md +43 -13
  31. package/skills/doc/authoring-notes.md +72 -0
  32. package/skills/doc/scripts/resolve_last_update.py +149 -0
  33. package/skills/doc-sync/SKILL.md +1 -1
  34. package/skills/dooo/SKILL.md +1 -1
  35. package/skills/error/SKILL.md +1 -1
  36. package/skills/evaluate/SKILL.md +1 -1
  37. package/skills/examplify/SKILL.md +1 -1
  38. package/skills/help/SKILL.md +1 -1
  39. package/skills/idea/SKILL.md +1 -1
  40. package/skills/improve/SKILL.md +1 -1
  41. package/skills/init/SKILL.md +8 -7
  42. package/skills/init/assets/scope-thresholds_template.json +7 -0
  43. package/skills/init/scripts/init.sh +6 -0
  44. package/skills/j-init/SKILL.md +168 -0
  45. package/skills/j-init/assets/.gitignore_template +15 -0
  46. package/skills/j-init/assets/PROJECT_SUMMARY_template.md +13 -0
  47. package/skills/j-init/assets/directory_structure.txt +14 -0
  48. package/skills/j-init/assets/scope-thresholds_template.json +7 -0
  49. package/skills/j-init/assets/strategy_stub_template.md +38 -0
  50. package/skills/j-init/assets/test-config_template.json +4 -0
  51. package/skills/j-init/assets/workflow_template.json +30 -0
  52. package/skills/j-init/scripts/apply-project-visibility.sh +176 -0
  53. package/skills/j-init/scripts/detect-existing-codebase.sh +166 -0
  54. package/skills/j-init/scripts/init.sh +116 -0
  55. package/skills/jbp/SKILL.md +1 -1
  56. package/skills/jenga/SKILL.md +1 -1
  57. package/skills/jenga/scripts/render-confirmation.sh +55 -18
  58. package/skills/jenga-permission-level/SKILL.md +1 -1
  59. package/skills/lgtm/SKILL.md +1 -1
  60. package/skills/pi-plan/SKILL.md +1 -1
  61. package/skills/proceed/SKILL.md +1 -1
  62. package/skills/publish/SKILL.md +67 -1
  63. package/skills/publish/adapters/npm-ci.md +60 -4
  64. package/skills/publish/adapters/npm.md +18 -0
  65. package/skills/publish/assets/ci-contract.md +27 -0
  66. package/skills/publish/assets/publish.example.json +27 -0
  67. package/skills/publish/schemas/publish.schema.json +20 -0
  68. package/skills/publish/scripts/npm_ci_pipeline.sh +50 -1
  69. package/skills/publish/scripts/npm_stage_inspect.sh +829 -0
  70. package/skills/publish/scripts/npm_stage_pipeline.sh +427 -0
  71. package/skills/publish/scripts/publish_common.sh +16 -0
  72. package/skills/publish/scripts/show_history.sh +12 -5
  73. package/skills/publish/scripts/validate_npm_stage_env.sh +184 -0
  74. package/skills/publish/scripts/write_ledger_entry.sh +92 -2
  75. package/skills/reconcile/SKILL.md +122 -12
  76. package/skills/reconcile/assets/report_format.md +17 -0
  77. package/skills/reconcile/scripts/resolve-reconcile-scope.sh +489 -0
  78. package/skills/reconcile-origin/SKILL.md +1 -1
  79. package/skills/redo/SKILL.md +1 -1
  80. package/skills/skillify/SKILL.md +1 -1
  81. package/skills/spinoff/SKILL.md +1 -1
  82. package/skills/status/SKILL.md +1 -1
  83. package/skills/todo/SKILL.md +40 -3
  84. package/skills/todo/scripts/add_trivial_task.sh +216 -0
  85. package/skills/todo/scripts/update_story_tasks.py +87 -0
  86. package/skills/uncharted/SKILL.md +201 -22
  87. package/skills/uncharted/scripts/directory-triage.sh +342 -0
  88. package/skills/uncharted/scripts/elicitation-state.sh +457 -0
  89. package/skills/wtf/SKILL.md +1 -1
  90. package/templates/SCRUM_BOARD_SCHEMA.md +90 -2
  91. package/templates/agent-context.md.tpl +47 -12
  92. package/templates/copilot-instructions.md.tpl +36 -9
  93. package/mcp/router/README.md +0 -19
  94. package/mcp/router/embedder.js +0 -23
  95. package/mcp/router/index.js +0 -204
  96. package/mcp/router/matcher.js +0 -87
  97. package/mcp/router/package-lock.json +0 -1048
  98. package/mcp/router/package.json +0 -11
  99. package/mcp/router/skill-index.js +0 -104
  100. package/skills/route/SKILL.md +0 -180
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:
@@ -407,6 +453,24 @@ There is no default analytics run. Analytics only happen when explicitly scoped
407
453
 
408
454
  ---
409
455
 
456
+ ## Investigative Mode
457
+
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.
459
+
460
+ **Hard constraints.** Investigative Mode is strictly read-only:
461
+ - No worktree is created for write purposes, no test files are written or modified, no test runs that mutate state, no dependency installs.
462
+ - No commits of any kind.
463
+ - No board status writes — this mode doesn't touch task/story/epic status at all, even though status writes are ordinarily your exclusive responsibility.
464
+ - No edits to `PROJECT_SUMMARY.md`, board files, or any other project artifact. The only output is the trace itself, returned to whoever dispatched you.
465
+
466
+ **Sandbox — reuse the existing worktree hooks, read-only.** Per the story decision behind this mode (see `project/documentation/plans/uncharted-interactive-elicitation.md` and its solution assessment, Problem 7 / Solution A), do not invent a new isolation mechanism. Mount a throwaway worktree via the same `WorktreeCreate` hook the developer agent uses for normal tasks, purely to get a disposable, isolated checkout to read from, and tear it down via `WorktreeRemove` once the investigation ends. This is convention-enforced, not filesystem-enforced: the hook mounts an ordinary writable worktree, and it is Investigative Mode's contract — not a permission bit — that keeps it read-only. Do not execute the test suite in a way that writes fixtures, snapshots, or coverage artifacts back into that worktree; reading existing test files and existing coverage output (if already present) is the mode's ceiling.
467
+
468
+ **What you trace — the distinct vantage point.** Where the developer's Investigative Mode pass traces what the code *does*, yours traces what the test suite *actually exercises and verifies* for that same flow: which tests touch it, what they assert (and what they merely execute without asserting), and where the coverage gap is — untested branches, unasserted side effects, error paths with no test at all, or a flow that "passes" only because nothing checks the part that matters. These are two distinct vantage points on the same flow, not two names for the same read; do not simply restate the developer's trace with "and there's a test for it" appended.
469
+
470
+ **Human-oracle-availability limitation.** The same limitation the developer faces applies to you, with an added facet: a passing test suite doesn't clarify intent either — a test can be green because it correctly verifies the right behavior, or green because it asserts nothing meaningful, and the test's own docstring or name can be as misleading as the code's. When you cannot determine from the tests (or their absence) what the intended behavior actually is, say so explicitly — report the uncertainty and the specific gap you couldn't close, rather than presenting a guess as a settled coverage verdict. This is an accepted, standing limitation of the mode, not something to engineer around by fabricating confidence.
471
+
472
+ ---
473
+
410
474
  ## Hooks
411
475
 
412
476
  Defined in agent frontmatter:
@@ -308,6 +308,33 @@ for HANDOFF_FILE in "$HANDOFF_DIR"/*.json; do
308
308
  echo "$TRIGGER" >> "$DEV_QUEUE"
309
309
  echo "[on_session_end] scrum-master → developer queue: implementation_assignment"
310
310
  fi
311
+
312
+ # A conversational architecture elicitation session (/uncharted
313
+ # onboard's default flow, or segment --mode investigate — E20_S08_T03)
314
+ # ended mid-run without converging. The session driving it is
315
+ # responsible for calling skills/uncharted/scripts/elicitation-state.sh
316
+ # pause and then writing this handoff with status "elicitation_paused"
317
+ # as its last action (see skills/uncharted/SKILL.md's Multi-Session
318
+ # Persistence subsection). This routes that pause into a resume
319
+ # signal for the next scrum-master session, per the existing
320
+ # SessionEnd/queue pattern rather than a new persistence mechanism
321
+ # (solution-assessment-uncharted-interactive-elicitation.md, Problem 11).
322
+ if [ "$HANDOFF_STATUS" = "elicitation_paused" ]; then
323
+ TRIGGER=$(jq -n \
324
+ --slurpfile h "$HANDOFF_FILE" \
325
+ --arg type "elicitation_resume" \
326
+ --arg date "$TIMESTAMP" \
327
+ '{
328
+ type: $type,
329
+ date: $date,
330
+ sender: { agent: "scrum-master", session_id: $h[0].session_id, date: $date },
331
+ elicitation_id: ($h[0].elicitation_id // ""),
332
+ state_file: ($h[0].state_file // ""),
333
+ message: "A conversational architecture elicitation session paused mid-run. Resume it from the persisted state file."
334
+ }')
335
+ echo "$TRIGGER" >> "$QUEUE_FILE"
336
+ echo "[on_session_end] scrum-master (elicitation_paused) → scrum-master queue: elicitation_resume"
337
+ fi
311
338
  ;;
312
339
 
313
340
  developer)
@@ -386,4 +413,16 @@ done
386
413
  # --- 5. Todo cleanup ---
387
414
  # Remove project/todo.md if it is effectively empty (only blanks, # Todo, and HTML comments).
388
415
  # Runs unconditionally on every session end regardless of agent type.
389
- 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,191 @@
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 either the pre-E50_S01 bare form or the post-rename "j:<name>" form
59
+ // (skills/*/SKILL.md was migrated in E50_S01_T02) — strip the prefix so the allow-list always
60
+ // holds bare identifiers, matching mcp/router/skill-index.js's `bareName` handling.
61
+ return rawName.replace(/^j:/i, "");
62
+ }
63
+
64
+ /**
65
+ * Scan `skillsDir` for every immediate `<name>/SKILL.md` and return a sorted, de-duplicated
66
+ * array of canonical skill identifiers (the frontmatter `name` value, treated as authoritative
67
+ * per docs/skill-authoring.md's "name must match the directory name" authoring rule — this
68
+ * function does not itself verify that match, only that `name` is present).
69
+ *
70
+ * A SKILL.md missing the `name` field is skipped with a warning, not fatal — mirrors
71
+ * mcp/router/skill-index.js's buildSkillIndex warning behavior for consistency across the two
72
+ * scanners.
73
+ *
74
+ * No disk write. Pure in-memory scan, for callers (e.g. the MCP router, E50_S02_T03) that want
75
+ * the identifier list without reading the generated artifact.
76
+ *
77
+ * @param {string} skillsDir - directory to scan (default: this repo/package's own skills/)
78
+ * @returns {string[]} sorted, de-duplicated skill identifiers
79
+ */
80
+ export function getSkillAllowListIdentifiers(skillsDir = DEFAULT_SKILLS_DIR) {
81
+ if (!existsSync(skillsDir)) return [];
82
+
83
+ const identifiers = new Set();
84
+
85
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
86
+ if (!entry.isDirectory()) continue;
87
+
88
+ const skillMdPath = join(skillsDir, entry.name, "SKILL.md");
89
+ if (!existsSync(skillMdPath)) continue;
90
+
91
+ let content;
92
+ try {
93
+ content = readFileSync(skillMdPath, "utf8");
94
+ } catch (e) {
95
+ process.stderr.write(`Warning: failed to read ${skillMdPath}: ${e.message} — skipped\n`);
96
+ continue;
97
+ }
98
+
99
+ const name = extractName(content);
100
+ if (!name) {
101
+ process.stderr.write(`Warning: ${skillMdPath} has no 'name' in frontmatter — skipped\n`);
102
+ continue;
103
+ }
104
+
105
+ identifiers.add(name);
106
+ }
107
+
108
+ return [...identifiers].sort();
109
+ }
110
+
111
+ /**
112
+ * Read the committed skill-allow-list.json artifact (produced by generateSkillAllowList /
113
+ * regenerated by postinstall.js and skills/self-sync/scripts/run.js) and return its `skills`
114
+ * array. Resolution order mirrors resolveTemplatePath's packageRoot-then-projectRoot candidate
115
+ * order in lib/generate-agent-context.js and lib/generate-copilot-instructions.js: the artifact
116
+ * normally lives at `<packageRoot>/lib/skill-allow-list.json` (the installed jenga-agent
117
+ * package's own committed copy), with `<projectRoot>/lib/skill-allow-list.json` as a fallback
118
+ * for the case where this repo IS the project (e.g. this monorepo's own dogfood run).
119
+ *
120
+ * This is the single read path both E50_S02_T04 generators (native Claude Code context files
121
+ * and Copilot/Codex prose-routing instructions) use to render the "trusted identifiers" list —
122
+ * neither one re-derives its own scan of skills/, per the drift lesson already documented in
123
+ * E41_S04 and referenced in this module's header comment.
124
+ *
125
+ * @param {string} projectRoot - project root directory
126
+ * @param {string} packageRoot - installed jenga-agent package root (default: this module's own
127
+ * package root)
128
+ * @returns {string[]} the artifact's `skills` array, or [] if the artifact is missing/unparseable
129
+ */
130
+ export function readSkillAllowList(projectRoot, packageRoot = DEFAULT_PACKAGE_ROOT) {
131
+ const candidates = [
132
+ join(packageRoot, "lib", "skill-allow-list.json"),
133
+ join(projectRoot, "lib", "skill-allow-list.json"),
134
+ ];
135
+ const path = candidates.find(existsSync);
136
+ if (!path) return [];
137
+
138
+ try {
139
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
140
+ return Array.isArray(parsed.skills) ? parsed.skills : [];
141
+ } catch (e) {
142
+ process.stderr.write(`Warning: failed to read/parse ${path}: ${e.message} — treating allow-list as empty\n`);
143
+ return [];
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Generate the canonical skill allow-list artifact at `outputPath`.
149
+ *
150
+ * The `skills` array is deterministic — sorted and de-duplicated, so repeat runs with no
151
+ * `skills/` change produce a byte-identical array. `generated_at` is NOT deterministic (it is a
152
+ * fresh timestamp on every run) — that is expected and intentional, not a bug: only the `skills`
153
+ * array itself is contracted to be stable.
154
+ *
155
+ * @param {string} skillsDir - directory to scan (default: this repo/package's own skills/)
156
+ * @param {string} outputPath - where to write the JSON artifact (default: lib/skill-allow-list.json)
157
+ * @returns {{written: boolean, path: string, skill_count: number}}
158
+ */
159
+ export function generateSkillAllowList(skillsDir = DEFAULT_SKILLS_DIR, outputPath = DEFAULT_OUTPUT_PATH) {
160
+ const skills = getSkillAllowListIdentifiers(skillsDir);
161
+
162
+ const artifact = {
163
+ generated_at: new Date().toISOString(),
164
+ skill_count: skills.length,
165
+ skills,
166
+ };
167
+
168
+ const outDir = dirname(outputPath);
169
+ if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
170
+
171
+ writeFileSync(outputPath, JSON.stringify(artifact, null, 2) + "\n", "utf8");
172
+
173
+ return { written: true, path: outputPath, skill_count: skills.length };
174
+ }
175
+
176
+ // CLI guard — allows `node lib/generate-skill-allow-list.js [skillsDir] [outputPath]`, intended
177
+ // to be callable from skills/self-sync/scripts/run.js and scripts/postinstall.js in the
178
+ // follow-up auto-regeneration task (E50_S02_T02).
179
+ //
180
+ // process.argv[1] is compared via realpath, not as a raw string — see the identical comment
181
+ // block in lib/generate-agent-context.js for why: Node resolves import.meta.url through symlinks
182
+ // when loading an ES module, but leaves process.argv[1] exactly as the shell passed it, so an
183
+ // invocation from a path under a symlinked directory (e.g. macOS's /tmp -> /private/tmp) would
184
+ // otherwise silently fail this comparison and skip generation with no error.
185
+ const invokedPath = process.argv[1] ? realpathSync(process.argv[1]) : null;
186
+ if (invokedPath === fileURLToPath(import.meta.url)) {
187
+ const skillsDirArg = process.argv[2] || DEFAULT_SKILLS_DIR;
188
+ const outputPathArg = process.argv[3] || DEFAULT_OUTPUT_PATH;
189
+ const result = generateSkillAllowList(skillsDirArg, outputPathArg);
190
+ console.log(`✓ ${result.path} (${result.skill_count} skills)`);
191
+ }
@@ -0,0 +1,43 @@
1
+ {
2
+ "generated_at": "2026-09-04T00:52:12.012Z",
3
+ "skill_count": 37,
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
+ "j-init",
26
+ "jbp",
27
+ "jenga",
28
+ "jenga-permission-level",
29
+ "lgtm",
30
+ "pi-plan",
31
+ "proceed",
32
+ "publish",
33
+ "reconcile",
34
+ "reconcile-origin",
35
+ "redo",
36
+ "skillify",
37
+ "spinoff",
38
+ "status",
39
+ "todo",
40
+ "uncharted",
41
+ "wtf"
42
+ ]
43
+ }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@jenga-ai/agent",
3
- "version": "1.2.4",
4
- "description": "Structured multi-agent development workflow for AI coding agents — scrum board, role-bounded scrum master / developer / tester agents, and 30+ slash-command skills. Works with Claude Code, GitHub Copilot, and Codex.",
3
+ "version": "2.0.0",
4
+ "description": "Structured multi-agent development workflow for AI coding agents \u2014 scrum board, role-bounded scrum master / developer / tester agents, and 30+ slash-command skills. Works with Claude Code, GitHub Copilot, and Codex.",
5
5
  "type": "module",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },
9
9
  "bin": {
10
- "jenga": "./bin/jenga.js"
10
+ "jenga": "bin/jenga.js"
11
11
  },
12
12
  "engines": {
13
13
  "node": ">=14.13.1"
@@ -16,12 +16,12 @@
16
16
  "postinstall": "node scripts/postinstall.js",
17
17
  "test": "bats tests/*.bats",
18
18
  "validate:npm-metadata": "bash scripts/validate_npm_metadata.sh",
19
- "ui:dev": "npm run ui:dev --prefix project/app",
20
- "ui:build": "npm run ui:build --prefix project/app",
21
- "api:start": "npm run api:start --prefix project/app",
22
- "api:dev": "npm run api:dev --prefix project/app",
23
- "dashboard:start": "npm run dashboard:start --prefix project/app",
24
- "dashboard:open": "npm run dashboard:open --prefix project/app"
19
+ "ui:dev": "npm run ui:dev --prefix project/app --",
20
+ "ui:build": "npm run ui:build --prefix project/app --",
21
+ "api:start": "npm run api:start --prefix project/app --",
22
+ "api:dev": "npm run api:dev --prefix project/app --",
23
+ "dashboard:start": "npm run dashboard:start --prefix project/app --",
24
+ "dashboard:open": "npm run dashboard:open --prefix project/app --"
25
25
  },
26
26
  "files": [
27
27
  "skills/",
@@ -31,7 +31,17 @@
31
31
  "templates/",
32
32
  "bin/",
33
33
  "lib/",
34
- "mcp/",
34
+ "mcp/execute-ticket/*.js",
35
+ "mcp/execute-ticket/package.json",
36
+ "mcp/help/*.js",
37
+ "mcp/help/package.json",
38
+ "mcp/router/*.js",
39
+ "mcp/router/package.json",
40
+ "mcp/router/package-lock.json",
41
+ "mcp/router/README.md",
42
+ "mcp/training_runner/*.js",
43
+ "mcp/training_runner/package.json",
44
+ "mcp/training_runner/package-lock.json",
35
45
  "README.md",
36
46
  "LICENSE"
37
47
  ],
@@ -59,7 +69,21 @@
59
69
  "skills",
60
70
  "project-management",
61
71
  "developer-tools",
62
- "cli"
72
+ "cli",
73
+ "jenga",
74
+ "jenga-ai",
75
+ "ai-coding",
76
+ "ai-coding-agent",
77
+ "coding-agent",
78
+ "coding-agents",
79
+ "ai-workflow",
80
+ "coding-workflow",
81
+ "development-workflow",
82
+ "software-development",
83
+ "developer-workflow",
84
+ "ai-framework",
85
+ "agentic-engineering",
86
+ "ai-engineering"
63
87
  ],
64
88
  "author": {
65
89
  "name": "Samwel Munga",
@@ -67,15 +91,6 @@
67
91
  "url": "https://knappkod.se/jenga-ai"
68
92
  },
69
93
  "license": "MIT",
70
- "dependencies": {
71
- "@huggingface/transformers": "^4.2.0"
72
- },
73
- "overrides": {
74
- "sharp": "^0.34.5"
75
- },
76
- "comments": {
77
- "audit": "sharp <0.35.0 and adm-zip <0.6.0 are transitive deps of @huggingface/transformers with no upstream fix available. Not exploitable in this context: only text feature-extraction pipeline is used — no image processing or ZIP handling at application boundary. Review when @huggingface/transformers ships a patched release."
78
- },
79
94
  "devDependencies": {
80
95
  "bats": "^1.13.0"
81
96
  }