@jenga-ai/agent 1.2.3 → 1.2.4
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 +3 -1
- package/lib/commands/init.js +23 -68
- package/lib/generate-copilot-instructions.js +142 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +21 -0
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
|
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.2.
|
|
3
|
+
"version": "1.2.4",
|
|
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": {
|
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
|
|