@jenga-ai/agent 1.2.3 → 1.3.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 +4 -1
- package/agents/developer.md +18 -0
- package/agents/scrum-master.md +1 -0
- package/agents/tester.md +18 -0
- package/hooks/on_session_end.sh +27 -0
- package/lib/commands/init.js +23 -68
- package/lib/generate-copilot-instructions.js +142 -0
- package/package.json +18 -17
- package/scripts/postinstall.js +21 -0
- package/skills/commit/SKILL.md +11 -1
- package/skills/dev-done/SKILL.md +46 -0
- package/skills/dev-done/scripts/classify-commit-outcome.sh +114 -0
- package/skills/init/SKILL.md +7 -6
- package/skills/init/assets/scope-thresholds_template.json +7 -0
- package/skills/init/scripts/init.sh +6 -0
- package/skills/publish/SKILL.md +66 -0
- package/skills/publish/adapters/npm-ci.md +34 -0
- package/skills/publish/adapters/npm.md +18 -0
- package/skills/publish/assets/ci-contract.md +27 -0
- package/skills/publish/assets/publish.example.json +27 -0
- package/skills/publish/schemas/publish.schema.json +20 -0
- package/skills/publish/scripts/npm_ci_pipeline.sh +29 -0
- package/skills/publish/scripts/npm_stage_inspect.sh +829 -0
- package/skills/publish/scripts/npm_stage_pipeline.sh +427 -0
- package/skills/publish/scripts/publish_common.sh +16 -0
- package/skills/publish/scripts/show_history.sh +12 -5
- package/skills/publish/scripts/validate_npm_stage_env.sh +184 -0
- package/skills/publish/scripts/write_ledger_entry.sh +92 -2
- package/skills/reconcile/SKILL.md +121 -11
- package/skills/reconcile/assets/report_format.md +17 -0
- package/skills/reconcile/scripts/resolve-reconcile-scope.sh +489 -0
- package/skills/uncharted/SKILL.md +200 -21
- package/skills/uncharted/scripts/directory-triage.sh +342 -0
- package/skills/uncharted/scripts/elicitation-state.sh +457 -0
- package/templates/SCRUM_BOARD_SCHEMA.md +57 -0
- package/templates/agent-context.md.tpl +23 -11
- package/templates/copilot-instructions.md.tpl +21 -11
- package/mcp/router/README.md +0 -19
- package/mcp/router/embedder.js +0 -23
- package/mcp/router/index.js +0 -204
- package/mcp/router/matcher.js +0 -87
- package/mcp/router/package-lock.json +0 -1048
- package/mcp/router/package.json +0 -11
- package/mcp/router/skill-index.js +0 -104
- package/skills/route/SKILL.md +0 -180
package/README.md
CHANGED
|
@@ -25,11 +25,13 @@ npm install @jenga-ai/agent
|
|
|
25
25
|
| Agent | Skills & Agents | Root Context File |
|
|
26
26
|
|---|---|---|
|
|
27
27
|
| **Claude Code** | `.claude/` — mirrored automatically on install | `CLAUDE.md` — generated by `/init` today |
|
|
28
|
-
| **GitHub Copilot** | `.agents/` — mirrored automatically on install | `.github/copilot-instructions.md` —
|
|
28
|
+
| **GitHub Copilot** | `.agents/` — mirrored automatically on install | `.github/copilot-instructions.md` — bootstrapped at `npm install` time, refined by `jenga init` |
|
|
29
29
|
| **Codex** | `.agents/` — mirrored automatically on install | `AGENTS.md` — generated by `/init` today |
|
|
30
30
|
|
|
31
31
|
`CLAUDE.md` and `AGENTS.md` are generated unconditionally by `/init` — every agent gets a real, populated root-level context file out of the box, not just a pointer. If either file already exists as a genuine pre-existing user file, Jenga writes its own copy as `J-CLAUDE.md` / `J-AGENTS.md` instead and inserts a short reference into the existing file, leaving it otherwise untouched.
|
|
32
32
|
|
|
33
|
+
`.github/copilot-instructions.md` follows a different path: `scripts/postinstall.js` writes it unconditionally and non-interactively the moment `npm install` finishes, so Copilot has working `/skill-name` routing instructions even if a consumer's very first action is a Copilot slash command, before the separate `jenga init` CLI wizard has ever run. Running `jenga init` afterward refines the same file using the user's actual chosen skills path — it is never generated by the `/init` skill itself.
|
|
34
|
+
|
|
33
35
|
---
|
|
34
36
|
|
|
35
37
|
## The Problem It Solves
|
|
@@ -226,6 +228,7 @@ Skills live in `.agents/skills/<name>/SKILL.md`. Invoke with `/<name>` in your A
|
|
|
226
228
|
| `/do` | Execute tasks from the scrum board, drives the Developer agent through the full loop |
|
|
227
229
|
| `/dooo` | Parallel execution orchestrator — runs multiple tasks simultaneously via sub-agents |
|
|
228
230
|
| `/redo` | Rework a previous implementation by commit SHA or Epic/Story number |
|
|
231
|
+
| `/publish` | Configure, validate, and orchestrate scaffolded release workflows — `setup`, `deploy`, `stage` (npm/npm-ci pre-approval staged publishing), `history`, `release-notes` |
|
|
229
232
|
| `/error` | Guided troubleshooting — gathers context, investigates, and drives a fix |
|
|
230
233
|
| `/train` | Scaffold and run ML training jobs (new job from template or run existing) |
|
|
231
234
|
|
package/agents/developer.md
CHANGED
|
@@ -287,6 +287,24 @@ See `templates/PROBLEM_RAPPORT_TEMPLATE.md` for the required format. Commit the
|
|
|
287
287
|
|
|
288
288
|
---
|
|
289
289
|
|
|
290
|
+
## Investigative Mode
|
|
291
|
+
|
|
292
|
+
**Trigger.** You are sometimes dispatched not to implement a task, but purely to build understanding of existing code — e.g. by the scrum-master during `/uncharted`'s conversational architecture elicitation, when it needs to know what a named flow or target actually does before proposing graph nodes or asking the user to confirm/correct an understanding. This is a distinct dispatch mode from the standard Task Intake flow above, and it is recognized by the request itself (you are asked to *trace* or *investigate*, not to *implement*), not by any board field.
|
|
293
|
+
|
|
294
|
+
**Hard constraints.** Investigative Mode is strictly read-only:
|
|
295
|
+
- No worktree is created for write purposes, no application code is written or modified, no dependency installs or generated artifacts.
|
|
296
|
+
- No commits of any kind.
|
|
297
|
+
- No board status writes — task/story/epic status is the tester's exclusive responsibility, and Investigative Mode doesn't touch the board at all, not even a status you'd normally be permitted to leave alone.
|
|
298
|
+
- No edits to `PROJECT_SUMMARY.md`, board files, or any other project artifact. The only output is the trace itself, returned to whoever dispatched you.
|
|
299
|
+
|
|
300
|
+
**Sandbox — reuse the existing worktree hooks, read-only.** Per the story decision behind this mode (see `project/documentation/plans/uncharted-interactive-elicitation.md` and its solution assessment, Problem 7 / Solution A), do not invent a new isolation mechanism. Mount a throwaway worktree via the same `WorktreeCreate` hook used for normal tasks (see Worktree Management above) purely to get a disposable, isolated checkout to read from, and tear it down via `WorktreeRemove` once the investigation ends. This is convention-enforced, not filesystem-enforced: the hook mounts an ordinary writable worktree, and it is Investigative Mode's contract — not a permission bit — that keeps it read-only. Treat any temptation to write into that worktree (a scratch file, a quick local test run) as a violation of the mode, not a harmless side effect.
|
|
301
|
+
|
|
302
|
+
**What you trace.** For the named flow or target, follow what the code actually does: call paths, data flow, key decision points, error handling, and any conditions or configuration that change the behavior. Report this in plain language back to the dispatcher — this is not a new artifact type and is not written to disk as part of Investigative Mode itself; if the dispatching flow later decides the finding is worth persisting, that happens through its own normal mechanism (e.g. a graph write or a summary doc), not through you.
|
|
303
|
+
|
|
304
|
+
**Human-oracle-availability limitation.** For genuinely undocumented code, there is often no reliable code-level way to confirm what was *intended* — only what currently happens. Naming conventions can mislead, a branch that looks dead may be load-bearing for a caller you haven't found, and "this must be for X" is a guess dressed as a finding. When you hit this wall, say so explicitly — report the uncertainty, name what you did and didn't check, and stop short of presenting a guess as a settled fact. This is an accepted, standing limitation of the mode, not something to engineer around by fabricating confidence.
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
290
308
|
## Hooks
|
|
291
309
|
|
|
292
310
|
Defined in agent frontmatter:
|
package/agents/scrum-master.md
CHANGED
|
@@ -76,6 +76,7 @@ This is a self-contained procedure, not a session-start-only step. It may be inv
|
|
|
76
76
|
6. **This is the only path** by which a mid-task agent request results in a `crucial_level` board write. Developer and tester never write `crucial_level`, `crucial_set_by`, or `crucial_note` directly to a board file themselves under any circumstance — they may only *request* the change via a `crucial_escalation` rapport, and the actual frontmatter write happens here, exclusively by scrum-master, closing the loop described in E39's Purpose section ("the actual frontmatter write still goes through scrum-master, never the subagent itself").
|
|
77
77
|
- `status_review`: Review the scrum board for any tasks or stories whose status should be updated based on recent activity.
|
|
78
78
|
- `story_rollup`: Check all tasks under the referenced story; if all are `Passed` or `Passed with remarks`, update the story status to `Passed` (or `Passed with remarks` if any remark exists). Then check epic rollup (see Rollup Logic).
|
|
79
|
+
- `elicitation_resume`: A `/uncharted` conversational architecture elicitation session (`onboard`'s default flow, or `segment --mode investigate` — E20_S08_T03) ended mid-run without converging. Read `state_file` (`project/queue/elicitation-state/<elicitation_id>.json`, written by `skills/uncharted/scripts/elicitation-state.sh`) to see exactly where it left off — which nodes already converged, which are still pending or flagged, and any directory-triage/checkpoint data already confirmed — then resume the conversational flow documented in `skills/uncharted/SKILL.md`'s Multi-Session Persistence subsection from that point rather than restarting the elicitation from scratch. If the state file is missing or unreadable, report that to the user rather than silently starting a fresh elicitation under the same id.
|
|
79
80
|
- After processing all triggers, **clear the file** by writing an empty file — do not leave processed triggers.
|
|
80
81
|
|
|
81
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.
|
package/agents/tester.md
CHANGED
|
@@ -407,6 +407,24 @@ There is no default analytics run. Analytics only happen when explicitly scoped
|
|
|
407
407
|
|
|
408
408
|
---
|
|
409
409
|
|
|
410
|
+
## Investigative Mode
|
|
411
|
+
|
|
412
|
+
**Trigger.** You are sometimes dispatched not to validate a task's implementation, but purely to build understanding of what the existing test suite actually covers for a named flow or target — e.g. by the scrum-master during `/uncharted`'s conversational architecture elicitation, alongside the developer's Investigative Mode pass over the same flow. This is a distinct dispatch mode from the standard Sender Object / Task Intake / Status Management flow above, recognized by the request itself (you are asked to *trace coverage*, not to *validate a task*), not by any board field.
|
|
413
|
+
|
|
414
|
+
**Hard constraints.** Investigative Mode is strictly read-only:
|
|
415
|
+
- No worktree is created for write purposes, no test files are written or modified, no test runs that mutate state, no dependency installs.
|
|
416
|
+
- No commits of any kind.
|
|
417
|
+
- No board status writes — this mode doesn't touch task/story/epic status at all, even though status writes are ordinarily your exclusive responsibility.
|
|
418
|
+
- No edits to `PROJECT_SUMMARY.md`, board files, or any other project artifact. The only output is the trace itself, returned to whoever dispatched you.
|
|
419
|
+
|
|
420
|
+
**Sandbox — reuse the existing worktree hooks, read-only.** Per the story decision behind this mode (see `project/documentation/plans/uncharted-interactive-elicitation.md` and its solution assessment, Problem 7 / Solution A), do not invent a new isolation mechanism. Mount a throwaway worktree via the same `WorktreeCreate` hook the developer agent uses for normal tasks, purely to get a disposable, isolated checkout to read from, and tear it down via `WorktreeRemove` once the investigation ends. This is convention-enforced, not filesystem-enforced: the hook mounts an ordinary writable worktree, and it is Investigative Mode's contract — not a permission bit — that keeps it read-only. Do not execute the test suite in a way that writes fixtures, snapshots, or coverage artifacts back into that worktree; reading existing test files and existing coverage output (if already present) is the mode's ceiling.
|
|
421
|
+
|
|
422
|
+
**What you trace — the distinct vantage point.** Where the developer's Investigative Mode pass traces what the code *does*, yours traces what the test suite *actually exercises and verifies* for that same flow: which tests touch it, what they assert (and what they merely execute without asserting), and where the coverage gap is — untested branches, unasserted side effects, error paths with no test at all, or a flow that "passes" only because nothing checks the part that matters. These are two distinct vantage points on the same flow, not two names for the same read; do not simply restate the developer's trace with "and there's a test for it" appended.
|
|
423
|
+
|
|
424
|
+
**Human-oracle-availability limitation.** The same limitation the developer faces applies to you, with an added facet: a passing test suite doesn't clarify intent either — a test can be green because it correctly verifies the right behavior, or green because it asserts nothing meaningful, and the test's own docstring or name can be as misleading as the code's. When you cannot determine from the tests (or their absence) what the intended behavior actually is, say so explicitly — report the uncertainty and the specific gap you couldn't close, rather than presenting a guess as a settled coverage verdict. This is an accepted, standing limitation of the mode, not something to engineer around by fabricating confidence.
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
410
428
|
## Hooks
|
|
411
429
|
|
|
412
430
|
Defined in agent frontmatter:
|
package/hooks/on_session_end.sh
CHANGED
|
@@ -308,6 +308,33 @@ for HANDOFF_FILE in "$HANDOFF_DIR"/*.json; do
|
|
|
308
308
|
echo "$TRIGGER" >> "$DEV_QUEUE"
|
|
309
309
|
echo "[on_session_end] scrum-master → developer queue: implementation_assignment"
|
|
310
310
|
fi
|
|
311
|
+
|
|
312
|
+
# A conversational architecture elicitation session (/uncharted
|
|
313
|
+
# onboard's default flow, or segment --mode investigate — E20_S08_T03)
|
|
314
|
+
# ended mid-run without converging. The session driving it is
|
|
315
|
+
# responsible for calling skills/uncharted/scripts/elicitation-state.sh
|
|
316
|
+
# pause and then writing this handoff with status "elicitation_paused"
|
|
317
|
+
# as its last action (see skills/uncharted/SKILL.md's Multi-Session
|
|
318
|
+
# Persistence subsection). This routes that pause into a resume
|
|
319
|
+
# signal for the next scrum-master session, per the existing
|
|
320
|
+
# SessionEnd/queue pattern rather than a new persistence mechanism
|
|
321
|
+
# (solution-assessment-uncharted-interactive-elicitation.md, Problem 11).
|
|
322
|
+
if [ "$HANDOFF_STATUS" = "elicitation_paused" ]; then
|
|
323
|
+
TRIGGER=$(jq -n \
|
|
324
|
+
--slurpfile h "$HANDOFF_FILE" \
|
|
325
|
+
--arg type "elicitation_resume" \
|
|
326
|
+
--arg date "$TIMESTAMP" \
|
|
327
|
+
'{
|
|
328
|
+
type: $type,
|
|
329
|
+
date: $date,
|
|
330
|
+
sender: { agent: "scrum-master", session_id: $h[0].session_id, date: $date },
|
|
331
|
+
elicitation_id: ($h[0].elicitation_id // ""),
|
|
332
|
+
state_file: ($h[0].state_file // ""),
|
|
333
|
+
message: "A conversational architecture elicitation session paused mid-run. Resume it from the persisted state file."
|
|
334
|
+
}')
|
|
335
|
+
echo "$TRIGGER" >> "$QUEUE_FILE"
|
|
336
|
+
echo "[on_session_end] scrum-master (elicitation_paused) → scrum-master queue: elicitation_resume"
|
|
337
|
+
fi
|
|
311
338
|
;;
|
|
312
339
|
|
|
313
340
|
developer)
|
package/lib/commands/init.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createInterface } from "readline";
|
|
2
|
-
import { readFileSync, writeFileSync, existsSync, appendFileSync
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, appendFileSync } from "fs";
|
|
3
3
|
import { join, dirname } from "path";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import { validateConfig } from "../config-schema.js";
|
|
6
6
|
import { injectSettings } from "../inject-settings.js";
|
|
7
7
|
import { generateAgentContext } from "../generate-agent-context.js";
|
|
8
|
+
import { generateCopilotInstructions } from "../generate-copilot-instructions.js";
|
|
8
9
|
|
|
9
10
|
const CONFIG_FILE = "jenga.cli.json";
|
|
10
11
|
|
|
@@ -125,74 +126,28 @@ export async function runInit(args, projectRoot = process.cwd()) {
|
|
|
125
126
|
console.warn("You can register it manually later by running jenga init again.");
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
// Generate .github/copilot-instructions.md from template
|
|
129
|
-
//
|
|
130
|
-
//
|
|
129
|
+
// Generate .github/copilot-instructions.md from template, via the shared generator
|
|
130
|
+
// (lib/generate-copilot-instructions.js) also used unconditionally by scripts/postinstall.js
|
|
131
|
+
// at install time (E46_S03_T01). Templates ship inside the jenga-agent package
|
|
132
|
+
// (node_modules/jenga-agent/templates/), with a bare `templates/` at the project root as a
|
|
133
|
+
// fallback for the dev-repo case — both handled inside the shared generator.
|
|
134
|
+
//
|
|
135
|
+
// skillsPath may be a string or an array (multi-target installs); pick the first existing
|
|
136
|
+
// directory — mirrored copies hold identical content. This pass uses the user's actual
|
|
137
|
+
// chosen skillsPath from the wizard, which may differ from postinstall's `.agents/skills`
|
|
138
|
+
// default and refines whatever postinstall already bootstrapped. The shared generator's
|
|
139
|
+
// idempotent marker-replace logic means running it again here never duplicates the JENGA
|
|
140
|
+
// block or corrupts content outside the markers.
|
|
131
141
|
try {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
join(projectRoot,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// array (multi-target installs); pick the first existing directory —
|
|
142
|
-
// mirrored copies hold identical content.
|
|
143
|
-
const skillsPathList = Array.isArray(config.skillsPath) ? config.skillsPath : [config.skillsPath];
|
|
144
|
-
const skillsDir = skillsPathList
|
|
145
|
-
.map(p => join(projectRoot, p))
|
|
146
|
-
.find(existsSync);
|
|
147
|
-
let skillLines = [];
|
|
148
|
-
if (skillsDir) {
|
|
149
|
-
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
150
|
-
if (!entry.isDirectory()) continue;
|
|
151
|
-
const skillMdPath = join(skillsDir, entry.name, "SKILL.md");
|
|
152
|
-
let description = "";
|
|
153
|
-
if (existsSync(skillMdPath)) {
|
|
154
|
-
const content = readFileSync(skillMdPath, "utf8");
|
|
155
|
-
const match = content.match(/^description:\s*(.+)$/m);
|
|
156
|
-
if (match) description = match[1].trim();
|
|
157
|
-
}
|
|
158
|
-
skillLines.push(`- **${entry.name}**${description ? `: ${description}` : ""}`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
const skillList = skillLines.length > 0 ? skillLines.join("\n") : "_No skills found._";
|
|
162
|
-
const rendered = tpl.replace("{{SKILL_LIST}}", skillList);
|
|
163
|
-
|
|
164
|
-
const githubDir = join(projectRoot, ".github");
|
|
165
|
-
const copilotInstructionsPath = join(githubDir, "copilot-instructions.md");
|
|
166
|
-
|
|
167
|
-
if (!existsSync(githubDir)) mkdirSync(githubDir, { recursive: true });
|
|
168
|
-
|
|
169
|
-
if (!existsSync(copilotInstructionsPath)) {
|
|
170
|
-
writeFileSync(copilotInstructionsPath, rendered, "utf8");
|
|
171
|
-
} else {
|
|
172
|
-
// Replace only the JENGA block; preserve content outside the markers
|
|
173
|
-
const existing = readFileSync(copilotInstructionsPath, "utf8");
|
|
174
|
-
const startMarker = "<!-- JENGA:START -->";
|
|
175
|
-
const endMarker = "<!-- JENGA:END -->";
|
|
176
|
-
const startIdx = existing.indexOf(startMarker);
|
|
177
|
-
const endIdx = existing.indexOf(endMarker);
|
|
178
|
-
|
|
179
|
-
// Extract the new JENGA block from rendered template
|
|
180
|
-
const renderedStart = rendered.indexOf(startMarker);
|
|
181
|
-
const renderedEnd = rendered.indexOf(endMarker);
|
|
182
|
-
const newBlock = rendered.slice(renderedStart, renderedEnd + endMarker.length);
|
|
183
|
-
|
|
184
|
-
let updated;
|
|
185
|
-
if (startIdx !== -1 && endIdx !== -1 && startIdx < endIdx) {
|
|
186
|
-
updated =
|
|
187
|
-
existing.slice(0, startIdx) +
|
|
188
|
-
newBlock +
|
|
189
|
-
existing.slice(endIdx + endMarker.length);
|
|
190
|
-
} else {
|
|
191
|
-
// No existing markers — append the block
|
|
192
|
-
updated = existing + (existing.endsWith("\n") ? "" : "\n") + newBlock + "\n";
|
|
193
|
-
}
|
|
194
|
-
writeFileSync(copilotInstructionsPath, updated, "utf8");
|
|
195
|
-
}
|
|
142
|
+
const skillsPathList = Array.isArray(config.skillsPath) ? config.skillsPath : [config.skillsPath];
|
|
143
|
+
const skillsDir = skillsPathList
|
|
144
|
+
.map(p => join(projectRoot, p))
|
|
145
|
+
.find(existsSync);
|
|
146
|
+
|
|
147
|
+
const result = generateCopilotInstructions(projectRoot, PACKAGE_ROOT, skillsDir);
|
|
148
|
+
if (result.skipped) {
|
|
149
|
+
console.warn("Warning: templates/copilot-instructions.md.tpl not found — skipped .github/copilot-instructions.md generation.");
|
|
150
|
+
} else {
|
|
196
151
|
console.log("✓ .github/copilot-instructions.md written");
|
|
197
152
|
}
|
|
198
153
|
} catch (e) {
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/generate-copilot-instructions.js — .github/copilot-instructions.md generation
|
|
4
|
+
*
|
|
5
|
+
* Single source of truth for scaffolding the Copilot routing-instructions file from
|
|
6
|
+
* templates/copilot-instructions.md.tpl (extracted from lib/commands/init.js per E46_S03_T01).
|
|
7
|
+
* Used by:
|
|
8
|
+
* - scripts/postinstall.js (runs unconditionally, non-interactively, on every `npm install`)
|
|
9
|
+
* - lib/commands/init.js (the interactive `jenga init` CLI wizard, using the user's chosen
|
|
10
|
+
* skillsPath — may differ from postinstall's default)
|
|
11
|
+
*
|
|
12
|
+
* Root-cause context: `.github/copilot-instructions.md` teaches Copilot the `/skill-name` →
|
|
13
|
+
* `.agents/skills/<name>/SKILL.md` routing convention. It was previously written only by the
|
|
14
|
+
* `jenga init` CLI wizard, never by postinstall.js — so a consumer whose very first action was
|
|
15
|
+
* typing `/init` inside Copilot (before ever running `jenga init`) got no routing at all.
|
|
16
|
+
* Extracting this into a shared, parameterized function lets postinstall.js bootstrap a working
|
|
17
|
+
* routing file immediately, with `jenga init` free to refine it later using the user's actual
|
|
18
|
+
* chosen skills path.
|
|
19
|
+
*
|
|
20
|
+
* Collision behaviour (preserved verbatim from the original inline implementation — do not
|
|
21
|
+
* rewrite this algorithm, only relocate it):
|
|
22
|
+
* - If `.github/copilot-instructions.md` does not exist, write the rendered template in full.
|
|
23
|
+
* - If it exists, replace only the content between `<!-- JENGA:START -->` and
|
|
24
|
+
* `<!-- JENGA:END -->`, preserving everything outside the markers.
|
|
25
|
+
* - If it exists but has no JENGA markers, append the block to the end of the file.
|
|
26
|
+
* This makes repeat runs (postinstall run twice, or postinstall followed by `jenga init`)
|
|
27
|
+
* idempotent: the JENGA block is never duplicated and content outside the markers is never
|
|
28
|
+
* touched.
|
|
29
|
+
*
|
|
30
|
+
* ESM, Node built-ins only — mirrors lib/generate-agent-context.js and lib/mirror.js.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs";
|
|
34
|
+
import { join, dirname } from "path";
|
|
35
|
+
import { fileURLToPath } from "url";
|
|
36
|
+
|
|
37
|
+
// This file lives at <package>/lib/generate-copilot-instructions.js — one level up is the
|
|
38
|
+
// installed jenga-agent package root, which holds templates/.
|
|
39
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
40
|
+
const DEFAULT_PACKAGE_ROOT = join(__dirname, "..");
|
|
41
|
+
|
|
42
|
+
const START_MARKER = "<!-- JENGA:START -->";
|
|
43
|
+
const END_MARKER = "<!-- JENGA:END -->";
|
|
44
|
+
|
|
45
|
+
function resolveTemplatePath(projectRoot, packageRoot) {
|
|
46
|
+
const candidates = [
|
|
47
|
+
join(packageRoot, "templates", "copilot-instructions.md.tpl"),
|
|
48
|
+
join(projectRoot, "templates", "copilot-instructions.md.tpl"),
|
|
49
|
+
];
|
|
50
|
+
return candidates.find(existsSync);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build the {{SKILL_LIST}} markdown block by scanning `skillsDir` for subdirectories
|
|
55
|
+
* containing a SKILL.md, extracting each one's `description:` frontmatter line.
|
|
56
|
+
*/
|
|
57
|
+
function buildSkillList(skillsDir) {
|
|
58
|
+
if (!skillsDir || !existsSync(skillsDir)) return "_No skills found._";
|
|
59
|
+
|
|
60
|
+
const skillLines = [];
|
|
61
|
+
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
62
|
+
if (!entry.isDirectory()) continue;
|
|
63
|
+
const skillMdPath = join(skillsDir, entry.name, "SKILL.md");
|
|
64
|
+
let description = "";
|
|
65
|
+
if (existsSync(skillMdPath)) {
|
|
66
|
+
const content = readFileSync(skillMdPath, "utf8");
|
|
67
|
+
const match = content.match(/^description:\s*(.+)$/m);
|
|
68
|
+
if (match) description = match[1].trim();
|
|
69
|
+
}
|
|
70
|
+
skillLines.push(`- **${entry.name}**${description ? `: ${description}` : ""}`);
|
|
71
|
+
}
|
|
72
|
+
return skillLines.length > 0 ? skillLines.join("\n") : "_No skills found._";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Replace only the JENGA:START..JENGA:END block in an existing copilot-instructions.md with the
|
|
77
|
+
* freshly rendered one, preserving everything outside the markers. If no existing file, write
|
|
78
|
+
* the rendered template in full. If the existing file has no markers, append the block.
|
|
79
|
+
*/
|
|
80
|
+
function writeCopilotInstructions(path, rendered) {
|
|
81
|
+
if (!existsSync(path)) {
|
|
82
|
+
writeFileSync(path, rendered, "utf8");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const existing = readFileSync(path, "utf8");
|
|
87
|
+
const startIdx = existing.indexOf(START_MARKER);
|
|
88
|
+
const endIdx = existing.indexOf(END_MARKER);
|
|
89
|
+
|
|
90
|
+
const renderedStart = rendered.indexOf(START_MARKER);
|
|
91
|
+
const renderedEnd = rendered.indexOf(END_MARKER);
|
|
92
|
+
const newBlock = rendered.slice(renderedStart, renderedEnd + END_MARKER.length);
|
|
93
|
+
|
|
94
|
+
let updated;
|
|
95
|
+
if (startIdx !== -1 && endIdx !== -1 && startIdx < endIdx) {
|
|
96
|
+
updated = existing.slice(0, startIdx) + newBlock + existing.slice(endIdx + END_MARKER.length);
|
|
97
|
+
} else {
|
|
98
|
+
// No existing markers — append the block.
|
|
99
|
+
updated = existing + (existing.endsWith("\n") ? "" : "\n") + newBlock + "\n";
|
|
100
|
+
}
|
|
101
|
+
writeFileSync(path, updated, "utf8");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Generate (or idempotently refresh) `.github/copilot-instructions.md` at projectRoot from the
|
|
106
|
+
* shared template.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} projectRoot - project root directory (default: cwd)
|
|
109
|
+
* @param {string} packageRoot - installed jenga-agent package root (default: derived from this
|
|
110
|
+
* file's own location)
|
|
111
|
+
* @param {string} skillsDir - path to the skills directory, resolved relative to projectRoot if
|
|
112
|
+
* not already absolute (e.g. ".agents/skills" from postinstall, or the user's chosen
|
|
113
|
+
* skillsPath from the `jenga init` wizard)
|
|
114
|
+
* @returns {{written: boolean, path?: string, skipped?: boolean}}
|
|
115
|
+
*/
|
|
116
|
+
export function generateCopilotInstructions(
|
|
117
|
+
projectRoot = process.cwd(),
|
|
118
|
+
packageRoot = DEFAULT_PACKAGE_ROOT,
|
|
119
|
+
skillsDir
|
|
120
|
+
) {
|
|
121
|
+
const tplPath = resolveTemplatePath(projectRoot, packageRoot);
|
|
122
|
+
if (!tplPath) {
|
|
123
|
+
return { written: false, skipped: true };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const tpl = readFileSync(tplPath, "utf8");
|
|
127
|
+
|
|
128
|
+
const resolvedSkillsDir = skillsDir
|
|
129
|
+
? (skillsDir.startsWith("/") ? skillsDir : join(projectRoot, skillsDir))
|
|
130
|
+
: undefined;
|
|
131
|
+
const skillList = buildSkillList(resolvedSkillsDir);
|
|
132
|
+
const rendered = tpl.replace("{{SKILL_LIST}}", skillList);
|
|
133
|
+
|
|
134
|
+
const githubDir = join(projectRoot, ".github");
|
|
135
|
+
const copilotInstructionsPath = join(githubDir, "copilot-instructions.md");
|
|
136
|
+
|
|
137
|
+
if (!existsSync(githubDir)) mkdirSync(githubDir, { recursive: true });
|
|
138
|
+
|
|
139
|
+
writeCopilotInstructions(copilotInstructionsPath, rendered);
|
|
140
|
+
|
|
141
|
+
return { written: true, path: copilotInstructionsPath };
|
|
142
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jenga-ai/agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Structured multi-agent development workflow for AI coding agents — scrum board, role-bounded scrum master / developer / tester agents, and 30+ slash-command skills. Works with Claude Code, GitHub Copilot, and Codex.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
"postinstall": "node scripts/postinstall.js",
|
|
17
17
|
"test": "bats tests/*.bats",
|
|
18
18
|
"validate:npm-metadata": "bash scripts/validate_npm_metadata.sh",
|
|
19
|
-
"ui:dev": "npm run ui:dev --prefix project/app",
|
|
20
|
-
"ui:build": "npm run ui:build --prefix project/app",
|
|
21
|
-
"api:start": "npm run api:start --prefix project/app",
|
|
22
|
-
"api:dev": "npm run api:dev --prefix project/app",
|
|
23
|
-
"dashboard:start": "npm run dashboard:start --prefix project/app",
|
|
24
|
-
"dashboard:open": "npm run dashboard:open --prefix project/app"
|
|
19
|
+
"ui:dev": "npm run ui:dev --prefix project/app --",
|
|
20
|
+
"ui:build": "npm run ui:build --prefix project/app --",
|
|
21
|
+
"api:start": "npm run api:start --prefix project/app --",
|
|
22
|
+
"api:dev": "npm run api:dev --prefix project/app --",
|
|
23
|
+
"dashboard:start": "npm run dashboard:start --prefix project/app --",
|
|
24
|
+
"dashboard:open": "npm run dashboard:open --prefix project/app --"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"skills/",
|
|
@@ -31,7 +31,17 @@
|
|
|
31
31
|
"templates/",
|
|
32
32
|
"bin/",
|
|
33
33
|
"lib/",
|
|
34
|
-
"mcp/",
|
|
34
|
+
"mcp/execute-ticket/*.js",
|
|
35
|
+
"mcp/execute-ticket/package.json",
|
|
36
|
+
"mcp/help/*.js",
|
|
37
|
+
"mcp/help/package.json",
|
|
38
|
+
"mcp/router/*.js",
|
|
39
|
+
"mcp/router/package.json",
|
|
40
|
+
"mcp/router/package-lock.json",
|
|
41
|
+
"mcp/router/README.md",
|
|
42
|
+
"mcp/training_runner/*.js",
|
|
43
|
+
"mcp/training_runner/package.json",
|
|
44
|
+
"mcp/training_runner/package-lock.json",
|
|
35
45
|
"README.md",
|
|
36
46
|
"LICENSE"
|
|
37
47
|
],
|
|
@@ -67,15 +77,6 @@
|
|
|
67
77
|
"url": "https://knappkod.se/jenga-ai"
|
|
68
78
|
},
|
|
69
79
|
"license": "MIT",
|
|
70
|
-
"dependencies": {
|
|
71
|
-
"@huggingface/transformers": "^4.2.0"
|
|
72
|
-
},
|
|
73
|
-
"overrides": {
|
|
74
|
-
"sharp": "^0.34.5"
|
|
75
|
-
},
|
|
76
|
-
"comments": {
|
|
77
|
-
"audit": "sharp <0.35.0 and adm-zip <0.6.0 are transitive deps of @huggingface/transformers with no upstream fix available. Not exploitable in this context: only text feature-extraction pipeline is used — no image processing or ZIP handling at application boundary. Review when @huggingface/transformers ships a patched release."
|
|
78
|
-
},
|
|
79
80
|
"devDependencies": {
|
|
80
81
|
"bats": "^1.13.0"
|
|
81
82
|
}
|
package/scripts/postinstall.js
CHANGED
|
@@ -32,6 +32,7 @@ import path from 'node:path';
|
|
|
32
32
|
import { fileURLToPath } from 'node:url';
|
|
33
33
|
|
|
34
34
|
import { mirror } from '../lib/mirror.js';
|
|
35
|
+
import { generateCopilotInstructions } from '../lib/generate-copilot-instructions.js';
|
|
35
36
|
|
|
36
37
|
// ESM equivalent of __dirname
|
|
37
38
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -153,6 +154,26 @@ function main() {
|
|
|
153
154
|
totalSkipped += result.skipped.length;
|
|
154
155
|
}
|
|
155
156
|
|
|
157
|
+
// Bootstrap .github/copilot-instructions.md unconditionally, right after the mirror step —
|
|
158
|
+
// no interactive prompts, since postinstall runs unattended during `npm install`. Without
|
|
159
|
+
// this, a consumer whose very first action is a Copilot slash command (before ever running
|
|
160
|
+
// the separate `jenga init` CLI wizard) gets no `/skill-name` routing instructions at all
|
|
161
|
+
// (E46_S03_T01). `.agents/skills` is passed as the skills directory because that's the
|
|
162
|
+
// directory the mirror step above just populated, regardless of which agentTarget the user
|
|
163
|
+
// eventually picks in `jenga init`. The shared generator's idempotent marker-replace logic
|
|
164
|
+
// (lib/generate-copilot-instructions.js) means a later `jenga init` run — using the user's
|
|
165
|
+
// actual chosen skillsPath — safely refines this file without duplicating the JENGA block.
|
|
166
|
+
try {
|
|
167
|
+
const result = generateCopilotInstructions(consumerRoot, packageRoot, '.agents/skills');
|
|
168
|
+
if (result.skipped) {
|
|
169
|
+
console.log(' ⚠ templates/copilot-instructions.md.tpl not found — skipped .github/copilot-instructions.md bootstrap');
|
|
170
|
+
} else {
|
|
171
|
+
console.log(' ✓ .github/copilot-instructions.md bootstrapped');
|
|
172
|
+
}
|
|
173
|
+
} catch (e) {
|
|
174
|
+
console.log(` ⚠ Could not bootstrap .github/copilot-instructions.md — ${e.message}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
156
177
|
// Write .jenga-version to record the installed version at consumer root
|
|
157
178
|
fs.writeFileSync(versionFile, packageVersion + '\n', 'utf8');
|
|
158
179
|
|
package/skills/commit/SKILL.md
CHANGED
|
@@ -36,7 +36,17 @@ If `--inline` is absent **and** `JENGA_COMMIT_INLINE` is not set (or is not `1`)
|
|
|
36
36
|
|
|
37
37
|
If no epic, task, or story has been implemented, exit with the message: "No implementation to commit."
|
|
38
38
|
|
|
39
|
-
1. **Reconcile first** — Invoke the `/reconcile` skill before any other action, so the board is never committed in a drifted state.
|
|
39
|
+
1. **Reconcile first, scoped to this commit** — Invoke the `/reconcile` skill before any other action, so the board is never committed in a drifted state.
|
|
40
|
+
- **Determine the scope to pass** (in order):
|
|
41
|
+
1. If `/commit` was invoked with an explicit epic/story/task id argument (e.g. `/commit E46`, `/commit E17_S04_T02`), use that id.
|
|
42
|
+
2. Otherwise, if the calling context already identifies a single task/story/epic just completed (e.g. a developer or tester agent's sender object naming `task_id`/`story_id`/`epic_id`, or a `/do` invocation for one task), use that id.
|
|
43
|
+
3. Otherwise, derive it from what's staged: run `git diff --cached --name-only` and filter to paths under `project/board/epics/`, `project/board/stories/`, and `project/board/tasks/`. Extract the id each matched filename encodes.
|
|
44
|
+
- If every extracted id shares a single common story-or-narrower ancestor (all belong to one story's own file plus any of its own tasks' files, or are all exactly one task's own file, or are all exactly one epic's own file with nothing narrower staged), use that single most-specific id (story/task id if present, else the epic id) as the scope.
|
|
45
|
+
- If matched ids span more than one distinct story, or more than one distinct epic, this case does not resolve — do not guess between them; fall through to case 4.
|
|
46
|
+
- **If no board files at all are staged, skip `/reconcile` entirely — do not fall through to case 4.** A commit that touches no board file cannot itself commit board drift, so there is nothing for a pre-commit reconcile to protect against; running a full unscoped scan here would be pure waste, not a safety net. This is distinct from the multi-item case immediately above, which still falls through to case 4 since board files genuinely are changing there.
|
|
47
|
+
4. Otherwise — no argument, no sender context, and staged board files span more than one distinct story or epic — fall back to unscoped `/reconcile` (full board), unchanged from prior behavior.
|
|
48
|
+
- **Invoke** `/reconcile <scope>` when a scope was determined in cases 1-3, unscoped `/reconcile` on the case-4 fallback, or skip the reconcile step entirely per case 3's no-board-files-staged outcome. Do not re-derive or duplicate `/reconcile`'s own default-scope-to-epic expansion here — passing a bare story or task id through is sufficient; `/reconcile` itself resolves that to the containing epic.
|
|
49
|
+
- **If `/reconcile` was skipped** (case 3's no-board-files-staged outcome) — continue silently to the next step, exactly as the no-drift outcome below.
|
|
40
50
|
- **If reconcile detects and corrects drift** — inform the user what changed (e.g. demoted/promoted statuses, merged orphaned worktrees, cleaned `todo.md` entries) before proceeding.
|
|
41
51
|
- **If reconcile finds no drift** — continue silently to the next step.
|
|
42
52
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dev-done
|
|
3
|
+
description: Commit the current work and immediately sync it into the .claude/ and .agents/ mirrors. Shortcut that chains /commit followed by /self-sync.
|
|
4
|
+
keywords:
|
|
5
|
+
- dev done
|
|
6
|
+
- commit and sync
|
|
7
|
+
- commit and mirror
|
|
8
|
+
- done syncing
|
|
9
|
+
examples:
|
|
10
|
+
- "dev-done E42_S04_T01"
|
|
11
|
+
- "commit this and sync the mirrors"
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Dev-Done — Commit, then Sync the Mirrors
|
|
15
|
+
|
|
16
|
+
Chains `/commit <scope-id>` and `/self-sync`, the same "convenience shortcut" pattern `skills/lgtm/SKILL.md`
|
|
17
|
+
uses for `/commit` + `/continue` — applied here to the commit -> mirror-sync sequence instead of the
|
|
18
|
+
commit -> next-task sequence. Useful right after implementing a root-level framework change
|
|
19
|
+
(`skills/`, `agents/`, `hooks/`, `scripts/`, `templates/`, `settings.json`), so the `.claude/` and
|
|
20
|
+
`.agents/` mirrors never sit stale waiting on a manual `/self-sync` call.
|
|
21
|
+
|
|
22
|
+
The deterministic decision of whether `/commit` halted early (nothing to commit) or completed
|
|
23
|
+
normally lives in `skills/dev-done/scripts/classify-commit-outcome.sh`, not inline here — see that
|
|
24
|
+
script's header for the exact contract.
|
|
25
|
+
|
|
26
|
+
## Instructions
|
|
27
|
+
|
|
28
|
+
1. Invoke the `/commit` skill with whatever scope-id argument `/dev-done` itself was given (e.g.
|
|
29
|
+
`/dev-done E42_S04_T01` invokes `/commit E42_S04_T01`) — the same EST scope-id argument contract
|
|
30
|
+
`/commit` already accepts (epic, story, or task id; see `skills/commit/SKILL.md`). Capture its full
|
|
31
|
+
output text.
|
|
32
|
+
|
|
33
|
+
2. Pass the captured output to the classifier script:
|
|
34
|
+
```
|
|
35
|
+
skills/dev-done/scripts/classify-commit-outcome.sh <<< "$COMMIT_OUTPUT"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
3. If the script exits `1` (HALT): print its stdout — the exact message `No implementation to
|
|
39
|
+
commit.` — to the user, and stop. Do **not** invoke `/self-sync`.
|
|
40
|
+
|
|
41
|
+
4. If the script exits `0` (PROCEED): invoke the `/self-sync` skill and wait for it to finish. This
|
|
42
|
+
happens regardless of whether `/commit` reported drift or doc-sync findings along the way — those
|
|
43
|
+
are informational, not blocking (see `skills/commit/SKILL.md`).
|
|
44
|
+
|
|
45
|
+
5. Report both steps' output to the user in sequence — the commit result first, then the self-sync
|
|
46
|
+
summary.
|