@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 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` — generated by `/init` today |
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
@@ -1,10 +1,11 @@
1
1
  import { createInterface } from "readline";
2
- import { readFileSync, writeFileSync, existsSync, appendFileSync, mkdirSync, readdirSync } from "fs";
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
- // Templates ship inside the jenga-agent package (node_modules/jenga-agent/templates/).
130
- // Fall back to a bare `templates/` at the project root only for the dev-repo case.
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 tplCandidates = [
133
- join(PACKAGE_ROOT, "templates", "copilot-instructions.md.tpl"),
134
- join(projectRoot, "templates", "copilot-instructions.md.tpl"),
135
- ];
136
- const tplPath = tplCandidates.find(existsSync);
137
- if (tplPath) {
138
- const tpl = readFileSync(tplPath, "utf8");
139
-
140
- // Build skill list from skillsPath. skillsPath may be a string or an
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",
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": {
@@ -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