@jenga-ai/agent 1.3.0 → 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.
- package/README.md +97 -92
- 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 +191 -0
- package/lib/skill-allow-list.json +43 -0
- package/package.json +18 -4
- package/scripts/apply-j-prefix.sh +230 -0
- package/scripts/consume-context-digest.sh +103 -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/SKILL.md +1 -1
- package/skills/btw/SKILL.md +1 -1
- package/skills/clearify/SKILL.md +1 -1
- package/skills/close-story/SKILL.md +78 -6
- package/skills/close-story/scripts/check-privatized.sh +345 -0
- package/skills/commit/SKILL.md +1 -1
- package/skills/continue/SKILL.md +1 -1
- package/skills/deep-dive/SKILL.md +1 -1
- package/skills/dev-done/SKILL.md +1 -1
- package/skills/distribute/SKILL.md +1 -1
- package/skills/do/SKILL.md +100 -10
- package/skills/doc/README.md +155 -0
- package/skills/doc/SKILL.md +43 -13
- package/skills/doc/authoring-notes.md +72 -0
- package/skills/doc/scripts/resolve_last_update.py +149 -0
- package/skills/doc-sync/SKILL.md +1 -1
- package/skills/dooo/SKILL.md +1 -1
- package/skills/error/SKILL.md +1 -1
- package/skills/evaluate/SKILL.md +1 -1
- package/skills/examplify/SKILL.md +1 -1
- package/skills/help/SKILL.md +1 -1
- package/skills/idea/SKILL.md +1 -1
- package/skills/improve/SKILL.md +1 -1
- package/skills/init/SKILL.md +1 -1
- package/skills/init/assets/scope-thresholds_template.json +3 -3
- package/skills/j-init/SKILL.md +168 -0
- package/skills/j-init/assets/.gitignore_template +15 -0
- package/skills/j-init/assets/PROJECT_SUMMARY_template.md +13 -0
- package/skills/j-init/assets/directory_structure.txt +14 -0
- package/skills/j-init/assets/scope-thresholds_template.json +7 -0
- package/skills/j-init/assets/strategy_stub_template.md +38 -0
- package/skills/j-init/assets/test-config_template.json +4 -0
- package/skills/j-init/assets/workflow_template.json +30 -0
- package/skills/j-init/scripts/apply-project-visibility.sh +176 -0
- package/skills/j-init/scripts/detect-existing-codebase.sh +166 -0
- package/skills/j-init/scripts/init.sh +116 -0
- package/skills/jbp/SKILL.md +1 -1
- 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/skills/lgtm/SKILL.md +1 -1
- package/skills/pi-plan/SKILL.md +1 -1
- package/skills/proceed/SKILL.md +1 -1
- package/skills/publish/SKILL.md +1 -1
- package/skills/publish/adapters/npm-ci.md +26 -4
- package/skills/publish/scripts/npm_ci_pipeline.sh +21 -1
- package/skills/reconcile/SKILL.md +1 -1
- package/skills/reconcile-origin/SKILL.md +1 -1
- package/skills/redo/SKILL.md +1 -1
- package/skills/skillify/SKILL.md +1 -1
- package/skills/spinoff/SKILL.md +1 -1
- package/skills/status/SKILL.md +1 -1
- package/skills/todo/SKILL.md +40 -3
- package/skills/todo/scripts/add_trivial_task.sh +216 -0
- package/skills/todo/scripts/update_story_tasks.py +87 -0
- package/skills/uncharted/SKILL.md +1 -1
- package/skills/wtf/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 +25 -8
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,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": "
|
|
4
|
-
"description": "Structured multi-agent development workflow for AI coding agents
|
|
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": "
|
|
10
|
+
"jenga": "bin/jenga.js"
|
|
11
11
|
},
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.13.1"
|
|
@@ -69,7 +69,21 @@
|
|
|
69
69
|
"skills",
|
|
70
70
|
"project-management",
|
|
71
71
|
"developer-tools",
|
|
72
|
-
"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"
|
|
73
87
|
],
|
|
74
88
|
"author": {
|
|
75
89
|
"name": "Samwel Munga",
|