@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.
- package/README.md +93 -256
- package/agents/developer.md +9 -8
- package/agents/scrum-master.md +57 -23
- package/agents/tester.md +51 -5
- package/hooks/on_session_end.sh +13 -1
- package/lib/generate-agent-context.js +18 -1
- package/lib/generate-copilot-instructions.js +18 -1
- package/lib/generate-skill-allow-list.js +197 -0
- package/lib/skill-allow-list.json +42 -0
- package/package.json +17 -13
- package/scripts/apply-j-prefix.sh +243 -0
- package/scripts/consume-context-digest.sh +103 -0
- package/scripts/generate-j-alias.sh +333 -0
- package/scripts/postinstall.js +25 -0
- package/scripts/sweep-stale-context-digests.sh +132 -0
- package/scripts/validate-board.sh +5 -0
- package/scripts/write-context-digest.sh +230 -0
- package/skills/{brainstorm → j-brainstorm}/SKILL.md +9 -2
- package/skills/{btw → j-btw}/SKILL.md +9 -2
- package/skills/{clearify → j-clearify}/SKILL.md +9 -2
- package/skills/{close-story → j-close-story}/SKILL.md +92 -13
- package/skills/j-close-story/scripts/check-privatized.sh +345 -0
- package/skills/{close-story → j-close-story}/scripts/check-story-closeable.sh +1 -1
- package/skills/{close-story → j-close-story}/scripts/extract-task-diff-stats.sh +1 -1
- package/skills/{commit → j-commit}/SKILL.md +9 -2
- package/skills/j-continue/SKILL.md +36 -0
- package/skills/{deep-dive → j-deep-dive}/SKILL.md +9 -8
- package/skills/{dev-done → j-dev-done}/SKILL.md +11 -4
- package/skills/{dev-done → j-dev-done}/scripts/classify-commit-outcome.sh +4 -4
- package/skills/{distribute → j-distribute}/SKILL.md +17 -10
- package/skills/{distribute → j-distribute}/scripts/distribute-changes.sh +1 -1
- package/skills/{do → j-do}/SKILL.md +111 -14
- package/skills/j-doc/README.md +155 -0
- package/skills/{doc → j-doc}/SKILL.md +55 -18
- package/skills/j-doc/authoring-notes.md +72 -0
- package/skills/j-doc/scripts/resolve_last_update.py +149 -0
- package/skills/{doc-sync → j-doc-sync}/SKILL.md +9 -2
- package/skills/{dooo → j-dooo}/SKILL.md +9 -2
- package/skills/j-error/SKILL.md +36 -0
- package/skills/{evaluate → j-evaluate}/SKILL.md +9 -2
- package/skills/j-examplify/SKILL.md +49 -0
- package/skills/{help → j-help}/SKILL.md +9 -2
- package/skills/{idea → j-idea}/SKILL.md +10 -3
- package/skills/{idea → j-idea}/assets/idea_handoff_template.md +1 -1
- package/skills/{improve → j-improve}/SKILL.md +9 -2
- package/skills/{init → j-init}/SKILL.md +21 -8
- package/skills/j-init/assets/scope-thresholds_template.json +7 -0
- package/skills/j-jbp/SKILL.md +32 -0
- package/skills/j-lgtm/SKILL.md +28 -0
- package/skills/{pi-plan → j-pi-plan}/SKILL.md +10 -3
- package/skills/{proceed → j-proceed}/SKILL.md +9 -2
- package/skills/{publish → j-publish}/SKILL.md +47 -40
- package/skills/{publish → j-publish}/adapters/droplet.md +1 -1
- package/skills/{publish → j-publish}/adapters/mobile-ios.md +3 -3
- package/skills/{publish → j-publish}/adapters/npm-ci.md +29 -7
- package/skills/{publish → j-publish}/adapters/npm.md +8 -8
- package/skills/{publish → j-publish}/assets/ci-contract.md +2 -2
- package/skills/{publish → j-publish}/schemas/publish.schema.json +1 -1
- package/skills/{publish → j-publish}/scripts/npm_ci_pipeline.sh +21 -1
- package/skills/{publish → j-publish}/scripts/npm_stage_inspect.sh +34 -1
- package/skills/{publish → j-publish}/scripts/npm_stage_pipeline.sh +9 -4
- package/skills/{publish → j-publish}/scripts/publish_deploy.sh +4 -4
- package/skills/{publish → j-publish}/scripts/validate_npm_stage_env.sh +1 -1
- package/skills/{publish → j-publish}/wizards/droplet.md +1 -1
- package/skills/{publish → j-publish}/wizards/mobile-ios.md +1 -1
- package/skills/{publish → j-publish}/wizards/npm-ci.md +1 -1
- package/skills/{publish → j-publish}/wizards/npm.md +1 -1
- package/skills/{reconcile → j-reconcile}/SKILL.md +12 -5
- package/skills/{reconcile → j-reconcile}/scripts/detect-unlinked-code.sh +2 -2
- package/skills/{reconcile → j-reconcile}/scripts/resolve-reconcile-scope.sh +3 -3
- package/skills/{reconcile-origin → j-reconcile-origin}/SKILL.md +13 -6
- package/skills/{redo → j-redo}/SKILL.md +9 -2
- package/skills/{skillify → j-skillify}/SKILL.md +10 -3
- package/skills/{spinoff → j-spinoff}/SKILL.md +9 -2
- package/skills/{status → j-status}/SKILL.md +9 -2
- package/skills/j-todo/SKILL.md +92 -0
- package/skills/{todo → j-todo}/assets/todo_handoff_template.md +1 -1
- package/skills/j-todo/scripts/add_trivial_task.sh +216 -0
- package/skills/j-todo/scripts/update_story_tasks.py +87 -0
- package/skills/{uncharted → j-uncharted}/SKILL.md +35 -28
- package/skills/{uncharted → j-uncharted}/assets/UNDERSTANDING_DOC_TEMPLATE.md +2 -2
- package/skills/{uncharted → j-uncharted}/scripts/detect-dependencies.sh +1 -1
- package/skills/{uncharted → j-uncharted}/scripts/detect-tests.sh +1 -1
- package/skills/{uncharted → j-uncharted}/scripts/directory-triage.sh +3 -3
- package/skills/{uncharted → j-uncharted}/scripts/elicitation-state.sh +3 -3
- package/skills/{uncharted → j-uncharted}/scripts/enumerate-target.sh +1 -1
- package/skills/{uncharted → j-uncharted}/scripts/import-source.sh +1 -1
- package/skills/{uncharted → j-uncharted}/scripts/inspect-provenance.sh +1 -1
- package/skills/{uncharted → j-uncharted}/scripts/resolve-segment-target.sh +5 -5
- package/skills/{uncharted → j-uncharted}/scripts/run-engine.sh +1 -1
- package/skills/{uncharted → j-uncharted}/scripts/validate-proposed-items.sh +2 -2
- package/skills/{uncharted → j-uncharted}/scripts/write-backfilled-epics.sh +1 -1
- package/skills/j-wtf/SKILL.md +27 -0
- package/skills/jenga/SKILL.md +1 -1
- package/skills/jenga/scripts/render-confirmation.sh +55 -18
- package/skills/jenga-permission-level/SKILL.md +1 -1
- package/templates/SCRUM_BOARD_SCHEMA.md +33 -2
- package/templates/agent-context.md.tpl +32 -9
- package/templates/copilot-instructions.md.tpl +66 -11
- package/skills/continue/SKILL.md +0 -29
- package/skills/error/SKILL.md +0 -29
- package/skills/examplify/SKILL.md +0 -42
- package/skills/init/assets/scope-thresholds_template.json +0 -7
- package/skills/jbp/SKILL.md +0 -25
- package/skills/lgtm/SKILL.md +0 -21
- package/skills/todo/SKILL.md +0 -48
- package/skills/wtf/SKILL.md +0 -20
- /package/skills/{close-story → j-close-story}/scripts/compute-scope-divergence.sh +0 -0
- /package/skills/{close-story → j-close-story}/scripts/extract-diff-stats.sh +0 -0
- /package/skills/{close-story → j-close-story}/scripts/update-task-frontmatter.sh +0 -0
- /package/skills/{commit → j-commit}/assets/user_instructions_template.md +0 -0
- /package/skills/{distribute → j-distribute}/CONFIG_SCHEMA.md +0 -0
- /package/skills/{distribute → j-distribute}/scripts/check-version.sh +0 -0
- /package/skills/{distribute → j-distribute}/scripts/commit-version-bump.sh +0 -0
- /package/skills/{do → j-do}/assets/intent-vs-diff-prompt.md +0 -0
- /package/skills/{do → j-do}/assets/sender_template.json +0 -0
- /package/skills/{doc → j-doc}/assets/path-objectives.yaml +0 -0
- /package/skills/{doc-sync → j-doc-sync}/assets/default_excludes.txt +0 -0
- /package/skills/{doc-sync → j-doc-sync}/assets/doc_targets.md +0 -0
- /package/skills/{evaluate → j-evaluate}/assets/evaluation_invokation_template.yml +0 -0
- /package/skills/{evaluate → j-evaluate}/assets/evaluation_rapport_template.md +0 -0
- /package/skills/{idea → j-idea}/assets/idea_template.md +0 -0
- /package/skills/{init → j-init}/assets/.gitignore_template +0 -0
- /package/skills/{init → j-init}/assets/PROJECT_SUMMARY_template.md +0 -0
- /package/skills/{init → j-init}/assets/directory_structure.txt +0 -0
- /package/skills/{init → j-init}/assets/strategy_stub_template.md +0 -0
- /package/skills/{init → j-init}/assets/test-config_template.json +0 -0
- /package/skills/{init → j-init}/assets/workflow_template.json +0 -0
- /package/skills/{init → j-init}/scripts/apply-project-visibility.sh +0 -0
- /package/skills/{init → j-init}/scripts/detect-existing-codebase.sh +0 -0
- /package/skills/{init → j-init}/scripts/init.sh +0 -0
- /package/skills/{pi-plan → j-pi-plan}/assets/epic.json +0 -0
- /package/skills/{pi-plan → j-pi-plan}/assets/story_template.md +0 -0
- /package/skills/{publish → j-publish}/assets/ExportOptions.plist.template +0 -0
- /package/skills/{publish → j-publish}/assets/ownership-matrix.md +0 -0
- /package/skills/{publish → j-publish}/assets/publish.example.json +0 -0
- /package/skills/{publish → j-publish}/assets/publish.example.npm-ci.json +0 -0
- /package/skills/{publish → j-publish}/assets/publish.example.npm.json +0 -0
- /package/skills/{publish → j-publish}/assets/secrets-guide.md +0 -0
- /package/skills/{publish → j-publish}/schemas/fixtures/npm-ci-minimal.json +0 -0
- /package/skills/{publish → j-publish}/schemas/fixtures/npm-ci-with-empty-secrets.json +0 -0
- /package/skills/{publish → j-publish}/schemas/fixtures/npm-ci-with-workflow-path.json +0 -0
- /package/skills/{publish → j-publish}/scripts/check_target_config.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/droplet_pipeline.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/finalize_changelog.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/generate_release_notes.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/ios_pipeline.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/npm_pipeline.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/publish_common.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/reconcile_tags.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/run_gates.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/setup_wizard.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/show_history.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/suggest_semver_bump.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/validate_config.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/validate_droplet_env.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/validate_ios_env.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/validate_npm_ci_env.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/validate_npm_env.sh +0 -0
- /package/skills/{publish → j-publish}/scripts/write_ledger_entry.sh +0 -0
- /package/skills/{reconcile → j-reconcile}/assets/report_format.md +0 -0
- /package/skills/{reconcile-origin → j-reconcile-origin}/scripts/reconcile-origin.sh +0 -0
- /package/skills/{skillify → j-skillify}/assets/init-new/SKILL.md +0 -0
- /package/skills/{skillify → j-skillify}/assets/init-new/assets/.gitignore_template +0 -0
- /package/skills/{skillify → j-skillify}/assets/init-new/assets/PROJECT_SUMMARY_template.md +0 -0
- /package/skills/{skillify → j-skillify}/assets/init-new/assets/directory_structure.txt +0 -0
- /package/skills/{skillify → j-skillify}/assets/init-new/assets/test-config_template.json +0 -0
- /package/skills/{skillify → j-skillify}/assets/init-new/assets/workflow_template.json +0 -0
- /package/skills/{skillify → j-skillify}/assets/init-new/scripts/init.sh +0 -0
- /package/skills/{skillify → j-skillify}/assets/init-old/SKILL.md +0 -0
- /package/skills/{status → j-status}/assets/output_format.md +0 -0
- /package/skills/{todo → j-todo}/assets/todo_template.md +0 -0
- /package/skills/{uncharted → j-uncharted}/assets/SEGMENT_PROPOSAL_TEMPLATE.md +0 -0
- /package/skills/{uncharted → j-uncharted}/scripts/apply-subsystem-cap.sh +0 -0
- /package/skills/{uncharted → j-uncharted}/scripts/discover-subsystems.sh +0 -0
package/agents/scrum-master.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
-
|
|
373
|
+
- `j.todo` — add a new item
|
|
343
374
|
- `/amend` — update or refine an existing item
|
|
344
|
-
-
|
|
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
|
|
347
|
-
- **Purpose:** link board work to documentation files so
|
|
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
|
|
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
|
|
458
|
-
2. Capture the **current topic** as a
|
|
459
|
-
3. Capture **both** as
|
|
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
|
|
464
|
-
2. Before finalising, offer
|
|
465
|
-
3. Once the
|
|
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
|
|
469
|
-
2. Offer
|
|
470
|
-
3. Once the
|
|
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
|
|
474
|
-
2. Create a
|
|
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
|
|
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
|
|
487
|
-
- **User wants to pursue both in parallel**: Treat as Option C — create both
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
package/hooks/on_session_end.sh
CHANGED
|
@@ -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
|
|
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
|
+
}
|