@jenga-ai/agent 1.1.1 → 1.2.1

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
@@ -2,8 +2,34 @@
2
2
 
3
3
  **A structured multi-agent development workflow that works with any AI agent or AI-native IDE.** Three specialised AI agents — Scrum Master, Developer, and Tester — collaborate through a shared scrum board, an event-driven trigger queue, and 28 slash-command skills to take a project from idea to verified, committed code — across as many sessions as it takes.
4
4
 
5
+ [![npm version](https://img.shields.io/npm/v/@jenga-ai/agent.svg)](https://www.npmjs.com/package/@jenga-ai/agent)
6
+ [![license](https://img.shields.io/npm/l/@jenga-ai/agent.svg)](LICENSE)
7
+
8
+ ```sh
9
+ npm install @jenga-ai/agent
10
+ ```
11
+
12
+ ## What You Get
13
+
14
+ - **Three specialised agents** — Scrum Master, Developer, Tester — each with a distinct role and no self-graded work
15
+ - **A persistent scrum board** — Epics, Stories, and Tasks tracked as Markdown files with structured frontmatter, surviving every session boundary
16
+ - **28 slash-command skills** — from planning (`/pi-plan`, `/todo`) to execution (`/do`, `/dooo`) to review (`/status`, `/reconcile`)
17
+ - **An event-driven trigger queue** — async handoffs between agents with a full audit trail in `project/logs/events.json`
18
+ - **Isolated git worktrees per task** — the Developer never works directly on your main branch
19
+ - **Works with any AI agent or IDE** — Claude Code, GitHub Copilot, Warp, and Codex CLI are all supported today
20
+
5
21
  > 📖 **Full reference:** [project/.wiki/documentation.md](project/.wiki/documentation.md) | [Intro Guide](project/.wiki/intro-guide.md)
6
22
 
23
+ ## Platform Support
24
+
25
+ | Agent | Skills & Agents | Root Context File |
26
+ |---|---|---|
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 |
29
+ | **Codex** | `.agents/` — mirrored automatically on install | `AGENTS.md` — generated by `/init` today |
30
+
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
+
7
33
  ---
8
34
 
9
35
  ## The Problem It Solves
@@ -4,6 +4,7 @@ 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
+ import { generateAgentContext } from "../generate-agent-context.js";
7
8
 
8
9
  const CONFIG_FILE = "jenga.cli.json";
9
10
 
@@ -198,6 +199,24 @@ export async function runInit(args, projectRoot = process.cwd()) {
198
199
  console.warn(`Warning: Could not write .github/copilot-instructions.md — ${e.message}`);
199
200
  }
200
201
 
202
+ // Generate CLAUDE.md / AGENTS.md from templates/agent-context.md.tpl.
203
+ // Unconditional — never gated on agentTarget (E41_S04): every agent
204
+ // benefits from these files being cheap to write, and users switch
205
+ // tools mid-project. Applies the J- collision rule and idempotent
206
+ // managed-block updates; see lib/generate-agent-context.js.
207
+ try {
208
+ const result = generateAgentContext(projectRoot, PACKAGE_ROOT);
209
+ if (result.skipped) {
210
+ console.warn("Warning: templates/agent-context.md.tpl not found — skipped CLAUDE.md/AGENTS.md generation.");
211
+ } else {
212
+ for (const { filename, mode } of result.written) {
213
+ console.log(`✓ ${filename} written (${mode})`);
214
+ }
215
+ }
216
+ } catch (e) {
217
+ console.warn(`Warning: Could not write CLAUDE.md/AGENTS.md — ${e.message}`);
218
+ }
219
+
201
220
  console.log('\nRun `jenga start` to start the router.');
202
221
 
203
222
  return config;
@@ -18,8 +18,8 @@ export const configSchema = {
18
18
  sessionTimeout: { type: "number", minimum: 0 },
19
19
  agentTarget: {
20
20
  oneOf: [
21
- { type: "string", enum: ["claude", "copilot", "custom"] },
22
- { type: "array", items: { type: "string", enum: ["claude", "copilot", "custom"] }, minItems: 1 }
21
+ { type: "string", enum: ["claude", "copilot", "custom", "codex"] },
22
+ { type: "array", items: { type: "string", enum: ["claude", "copilot", "custom", "codex"] }, minItems: 1 }
23
23
  ]
24
24
  }
25
25
  },
@@ -45,10 +45,10 @@ export function validateConfig(config) {
45
45
  if (typeof config.sessionTimeout !== "number" || config.sessionTimeout < 0) {
46
46
  return { valid: false, error: "sessionTimeout must be a non-negative number" };
47
47
  }
48
- const validTargets = ["claude", "copilot", "custom"];
48
+ const validTargets = ["claude", "copilot", "custom", "codex"];
49
49
  const targets = Array.isArray(config.agentTarget) ? config.agentTarget : [config.agentTarget];
50
50
  if (targets.length === 0 || !targets.every(t => validTargets.includes(t))) {
51
- return { valid: false, error: 'agentTarget must be one of: "claude", "copilot", "custom" (or an array of those values)' };
51
+ return { valid: false, error: 'agentTarget must be one of: "claude", "copilot", "custom", "codex" (or an array of those values)' };
52
52
  }
53
53
  return { valid: true };
54
54
  }
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/generate-agent-context.js — CLAUDE.md / AGENTS.md generation
4
+ *
5
+ * Single source of truth for scaffolding the two root-level agent-context
6
+ * files from templates/agent-context.md.tpl (E41_S04_T02). Used by:
7
+ * - lib/commands/init.js (published `jenga init` CLI)
8
+ * - skills/init/scripts/init.sh (this repo's own board-scaffolding flow,
9
+ * invoked via `node` — see the CLI guard
10
+ * at the bottom of this file)
11
+ *
12
+ * Design constraints (per E41_S04 story — do not re-litigate):
13
+ * - CLAUDE.md and AGENTS.md are written unconditionally, never gated on
14
+ * `agentTarget`.
15
+ * - Both are written as real files — never a symlink.
16
+ * - No AGENT.md (singular) is ever scaffolded.
17
+ * - Collision rule: if the target file doesn't exist, write it directly.
18
+ * If it already exists, Jenga's copy goes to `J-<NAME>.md` instead, and
19
+ * a short, clearly-marked reference is inserted near the top of the
20
+ * existing file. The J- prefix always belongs to the incoming Jenga
21
+ * file; the user's file keeps its name and content.
22
+ * - Idempotent across repeat runs: managed blocks update in place, the
23
+ * reference line is never duplicated, and a J- file is updated in place
24
+ * rather than re-triggering collision handling (no J-J-CLAUDE.md).
25
+ *
26
+ * ESM, Node built-ins only — mirrors lib/mirror.js and lib/inject-settings.js.
27
+ */
28
+
29
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs";
30
+ import { join, dirname } from "path";
31
+ import { fileURLToPath } from "url";
32
+
33
+ // This file lives at <package>/lib/generate-agent-context.js — one level up
34
+ // is the installed jenga-agent package root, which holds templates/.
35
+ const __dirname = dirname(fileURLToPath(import.meta.url));
36
+ const DEFAULT_PACKAGE_ROOT = join(__dirname, "..");
37
+
38
+ const CONTENT_START = "<!-- JENGA:START -->";
39
+ const CONTENT_END = "<!-- JENGA:END -->";
40
+ const REF_START = "<!-- JENGA:REF:START -->";
41
+ const REF_END = "<!-- JENGA:REF:END -->";
42
+
43
+ // One entry per generated file. skillDiscoveryPath reflects the reading
44
+ // agent's own discovery convention: Claude Code reads .claude/skills/,
45
+ // every other agent (Codex, generic AGENTS.md consumers, ...) reads
46
+ // .agents/skills/.
47
+ const TARGETS = [
48
+ { filename: "CLAUDE.md", skillDiscoveryPath: ".claude/skills/" },
49
+ { filename: "AGENTS.md", skillDiscoveryPath: ".agents/skills/" },
50
+ ];
51
+
52
+ /**
53
+ * Build the rendered {{SKILL_LIST}} block by scanning the first existing
54
+ * skills directory among .claude/skills/ and .agents/skills/ — mirrored
55
+ * copies hold identical content, so either is representative. Mirrors the
56
+ * skill-list extraction already used for the Copilot template in
57
+ * lib/commands/init.js.
58
+ */
59
+ function buildSkillList(projectRoot) {
60
+ const candidates = [".claude/skills", ".agents/skills"].map((p) => join(projectRoot, p));
61
+ const skillsDir = candidates.find(existsSync);
62
+ if (!skillsDir) return "_No skills found._";
63
+
64
+ const skillLines = [];
65
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
66
+ if (!entry.isDirectory()) continue;
67
+ const skillMdPath = join(skillsDir, entry.name, "SKILL.md");
68
+ let description = "";
69
+ if (existsSync(skillMdPath)) {
70
+ const content = readFileSync(skillMdPath, "utf8");
71
+ const match = content.match(/^description:\s*(.+)$/m);
72
+ if (match) description = match[1].trim();
73
+ }
74
+ skillLines.push(`- **${entry.name}**${description ? `: ${description}` : ""}`);
75
+ }
76
+ return skillLines.length > 0 ? skillLines.join("\n") : "_No skills found._";
77
+ }
78
+
79
+ function resolveTemplatePath(projectRoot, packageRoot) {
80
+ const candidates = [
81
+ join(packageRoot, "templates", "agent-context.md.tpl"),
82
+ join(projectRoot, "templates", "agent-context.md.tpl"),
83
+ ];
84
+ return candidates.find(existsSync);
85
+ }
86
+
87
+ /**
88
+ * Replace only the JENGA:START..JENGA:END block in `existingContent` with
89
+ * `newBlock` (which itself includes the marker comments), preserving
90
+ * everything outside the markers. If no markers are present, append the
91
+ * block. Identical technique to the Copilot managed-block replace in
92
+ * lib/commands/init.js.
93
+ */
94
+ function replaceManagedBlock(existingContent, newBlock) {
95
+ const startIdx = existingContent.indexOf(CONTENT_START);
96
+ const endIdx = existingContent.indexOf(CONTENT_END);
97
+ if (startIdx !== -1 && endIdx !== -1 && startIdx < endIdx) {
98
+ return existingContent.slice(0, startIdx) + newBlock + existingContent.slice(endIdx + CONTENT_END.length);
99
+ }
100
+ return existingContent + (existingContent.endsWith("\n") ? "" : "\n") + newBlock + "\n";
101
+ }
102
+
103
+ /**
104
+ * Write `rendered` (full file content, including the JENGA:START/END block)
105
+ * to `path`. If the file doesn't exist, write it in full. If it does,
106
+ * replace only the managed block, preserving any content outside it (e.g.
107
+ * a user's own edits to the title line of a Jenga-owned file).
108
+ */
109
+ function writeManagedFile(path, rendered) {
110
+ if (!existsSync(path)) {
111
+ writeFileSync(path, rendered, "utf8");
112
+ return;
113
+ }
114
+ const existing = readFileSync(path, "utf8");
115
+ const renderedStart = rendered.indexOf(CONTENT_START);
116
+ const renderedEnd = rendered.indexOf(CONTENT_END);
117
+ const newBlock = rendered.slice(renderedStart, renderedEnd + CONTENT_END.length);
118
+ const updated = replaceManagedBlock(existing, newBlock);
119
+ if (updated !== existing) writeFileSync(path, updated, "utf8");
120
+ }
121
+
122
+ /**
123
+ * Insert (or, on repeat runs, update in place) a short Jenga-marked
124
+ * reference block near the very top of a pre-existing user file, pointing
125
+ * at the J-<NAME>.md copy. Idempotent via its own REF marker pair, kept
126
+ * distinct from CONTENT_START/CONTENT_END so the two replace passes never
127
+ * interfere with each other in the same file.
128
+ */
129
+ function insertOrUpdateReference(path, jFilename) {
130
+ const refBlock =
131
+ `${REF_START}\n` +
132
+ `> **Jenga framework note:** This project also uses [Jenga](https://github.com/samwelmunga/jenga-npm) — ` +
133
+ `see \`${jFilename}\` for Jenga's framework-specific agent context (skills, board, workflow). ` +
134
+ `This file is unrelated to Jenga and was left untouched.\n` +
135
+ `${REF_END}`;
136
+
137
+ const existing = readFileSync(path, "utf8");
138
+ const startIdx = existing.indexOf(REF_START);
139
+ const endIdx = existing.indexOf(REF_END);
140
+
141
+ let updated;
142
+ if (startIdx !== -1 && endIdx !== -1 && startIdx < endIdx) {
143
+ updated = existing.slice(0, startIdx) + refBlock + existing.slice(endIdx + REF_END.length);
144
+ } else {
145
+ // No existing reference — insert at the very top, ahead of any content,
146
+ // satisfying "early / before the first major section" regardless of
147
+ // the existing file's structure.
148
+ updated = refBlock + "\n\n" + existing;
149
+ }
150
+ if (updated !== existing) writeFileSync(path, updated, "utf8");
151
+ }
152
+
153
+ /**
154
+ * Generate CLAUDE.md and AGENTS.md at projectRoot from the shared template,
155
+ * applying the J- collision rule and idempotent managed-block updates.
156
+ *
157
+ * @param {string} projectRoot - project root directory (default: cwd)
158
+ * @param {string} packageRoot - installed jenga-agent package root (default:
159
+ * derived from this file's own location)
160
+ * @returns {{written: Array<{filename: string, mode: string}>, skipped?: boolean}}
161
+ */
162
+ export function generateAgentContext(projectRoot = process.cwd(), packageRoot = DEFAULT_PACKAGE_ROOT) {
163
+ const tplPath = resolveTemplatePath(projectRoot, packageRoot);
164
+ if (!tplPath) {
165
+ return { written: [], skipped: true };
166
+ }
167
+
168
+ const tpl = readFileSync(tplPath, "utf8");
169
+ const skillList = buildSkillList(projectRoot);
170
+ const written = [];
171
+
172
+ for (const target of TARGETS) {
173
+ const rendered = tpl
174
+ .split("{{TARGET_FILENAME}}").join(target.filename)
175
+ .split("{{SKILL_DISCOVERY_PATH}}").join(target.skillDiscoveryPath)
176
+ .split("{{SKILL_LIST}}").join(skillList);
177
+
178
+ const targetPath = join(projectRoot, target.filename);
179
+ const jFilename = `J-${target.filename}`;
180
+ const jPath = join(projectRoot, jFilename);
181
+
182
+ if (existsSync(jPath)) {
183
+ // A J- file can only ever have been created by Jenga — this is a
184
+ // repeat run after a prior collision. Update it in place; never
185
+ // re-derive collision handling from it (no J-J-CLAUDE.md).
186
+ writeManagedFile(jPath, rendered);
187
+ if (existsSync(targetPath)) {
188
+ insertOrUpdateReference(targetPath, jFilename);
189
+ }
190
+ written.push({ filename: target.filename, mode: "updated-j-file" });
191
+ continue;
192
+ }
193
+
194
+ if (existsSync(targetPath)) {
195
+ const existingContent = readFileSync(targetPath, "utf8");
196
+ if (existingContent.includes(CONTENT_START)) {
197
+ // Jenga's own file from a prior *non-collision* run — update in
198
+ // place rather than treating it as a fresh collision.
199
+ writeManagedFile(targetPath, rendered);
200
+ written.push({ filename: target.filename, mode: "direct-update" });
201
+ } else {
202
+ // Genuine pre-existing user file — collision. Write Jenga's copy
203
+ // as J-<NAME>.md and insert a reference into the user's file.
204
+ writeFileSync(jPath, rendered, "utf8");
205
+ insertOrUpdateReference(targetPath, jFilename);
206
+ written.push({ filename: target.filename, mode: "collision-created-j-file" });
207
+ }
208
+ continue;
209
+ }
210
+
211
+ // Neither the target nor its J- copy exists — first-run, no collision.
212
+ writeManagedFile(targetPath, rendered);
213
+ written.push({ filename: target.filename, mode: "direct-write" });
214
+ }
215
+
216
+ return { written };
217
+ }
218
+
219
+ // CLI guard — allows `node lib/generate-agent-context.js [projectRoot]`,
220
+ // used by skills/init/scripts/init.sh (this repo's own board-scaffolding
221
+ // flow, where node is guaranteed available). The published npm CLI path
222
+ // (lib/commands/init.js) imports generateAgentContext() directly instead.
223
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
224
+ const projectRoot = process.argv[2] || process.cwd();
225
+ const result = generateAgentContext(projectRoot);
226
+ if (result.skipped) {
227
+ console.warn("Warning: agent-context.md.tpl not found — skipped CLAUDE.md/AGENTS.md generation.");
228
+ } else {
229
+ for (const { filename, mode } of result.written) {
230
+ console.log(`✓ ${filename} (${mode})`);
231
+ }
232
+ }
233
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jenga-ai/agent",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "description": "JengaAgent — agentic project management CLI",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -13,7 +13,7 @@
13
13
  # 2. Reads <project_path>/jenga.config.json for target_dir and project_name.
14
14
  # 3. Loads per-project exclusions from <project_path>/.jenga_ignore (warn, not error).
15
15
  # 4. Rsyncs each included item to both <target_dir>/ and .claude/ in the project.
16
- # 5. Copies AGENT.md, CLAUDE.md, WARP.md to the project root.
16
+ # 5. Copies AGENTS.md, CLAUDE.md, WARP.md to the project root.
17
17
  # 6. Writes jenga.config.json atomically (temp + mv).
18
18
  #
19
19
  # Exit codes:
@@ -282,7 +282,7 @@ done
282
282
  # Step 6: Copy root-level docs to the consuming project root
283
283
  # ---------------------------------------------------------------------------
284
284
 
285
- ROOT_DOCS="AGENT.md CLAUDE.md WARP.md"
285
+ ROOT_DOCS="AGENTS.md CLAUDE.md WARP.md"
286
286
 
287
287
  for doc in $ROOT_DOCS; do
288
288
  src="$REPO_ROOT/$doc"
@@ -73,7 +73,18 @@ cp "$SCRIPT_DIR/../../../templates/CHANGELOG_TEMPLATE.md" CHANGELOG.md
73
73
  echo "→ Applying project files visibility ($VISIBILITY)..."
74
74
  bash "$VISIBILITY_SCRIPT" "$VISIBILITY" "$PWD"
75
75
 
76
- # ─── 12. Initial commit ──────────────────────────────────────────────────────
76
+ # ─── 12. Generate CLAUDE.md / AGENTS.md ──────────────────────────────────────
77
+ # Unconditional — never gated on agentTarget (E41_S04). Applies the J-
78
+ # collision rule and idempotent managed-block updates; see
79
+ # lib/generate-agent-context.js (shared with the published `jenga init` CLI).
80
+ echo "→ Generating CLAUDE.md / AGENTS.md..."
81
+ if command -v node >/dev/null 2>&1; then
82
+ node "$SCRIPT_DIR/../../../lib/generate-agent-context.js" "$PWD"
83
+ else
84
+ echo " Warning: node not found — skipped CLAUDE.md/AGENTS.md generation." >&2
85
+ fi
86
+
87
+ # ─── 13. Initial commit ──────────────────────────────────────────────────────
77
88
  echo "→ Staging and committing scaffolded files..."
78
89
  git add -A
79
90
  git commit -m "init: scaffold project structure and workflow config"
@@ -509,8 +509,10 @@ linked_by_board = linked_by_commit = 0
509
509
 
510
510
  for rec in linkage.get("results", []):
511
511
  # Key on the path we ASKED about, not the resolver's `target_relative`. The resolver
512
- # realpath()s its target, so a tracked symlink (`AGENTS.md -> AGENT.md`) reports its
513
- # destination and would silently overwrite that destination's own record.
512
+ # realpath()s its target, so a tracked symlink (e.g. this repo's now-retired
513
+ # `AGENTS.md -> AGENT.md`, before E41_S04_T05 made AGENTS.md a real file) reports its
514
+ # destination and would silently overwrite that destination's own record. The general
515
+ # case (any tracked symlink) still applies even though that specific example is gone.
514
516
  path = rec.get("argument") or rec.get("target_relative")
515
517
  if not path:
516
518
  continue
@@ -0,0 +1,46 @@
1
+ # {{TARGET_FILENAME}}
2
+
3
+ This file gives AI coding agents context on this project. The block between the Jenga markers is managed automatically by `jenga init` — do not edit it manually.
4
+
5
+ <!-- JENGA:START -->
6
+ ## Jenga Agent Framework
7
+
8
+ This project uses **Jenga** — a skill-based AI agent framework. Jenga organises project work into Epics, Stories, and Tasks managed on a scrum board. It routes user messages to specialised **skills** that execute defined workflows.
9
+
10
+ ### How Jenga Works
11
+
12
+ - Each **skill** is a self-contained instruction set stored under `{{SKILL_DISCOVERY_PATH}}<skill-name>/`.
13
+ - Skills are invoked by typing `/skill-name` in the chat prompt.
14
+ - The active project directory is available via the `JENGA_PROJECT_DIR` environment variable. **Use `JENGA_PROJECT_DIR` — not `CLAUDE_PROJECT_DIR` or any other agent-specific variable** — as the canonical path to the project folder.
15
+
16
+ ### Skill Routing
17
+
18
+ When a user's message matches a known skill keyword or intent, invoke the skill immediately using the slash-command syntax:
19
+
20
+ ```
21
+ /skill-name
22
+ ```
23
+
24
+ Do **not** answer skill-related requests with free-form prose if a matching skill exists — delegate to the skill instead.
25
+
26
+ For free-form questions (architecture, code review, debugging, general Q&A) that do not match a skill, answer directly using your full capabilities.
27
+
28
+ #### Routing decision table
29
+
30
+ | Situation | Action |
31
+ |-----------|--------|
32
+ | Message matches a skill keyword or intent | Invoke `/skill-name` |
33
+ | Message is a general coding or project question | Answer directly |
34
+ | Ambiguous — could be skill or free-form | Prefer the skill; mention it is being invoked |
35
+
36
+ ### Available Skills
37
+
38
+ {{SKILL_LIST}}
39
+
40
+ ### Notes
41
+
42
+ - Always resolve file paths relative to `JENGA_PROJECT_DIR`.
43
+ - When a skill asks you to read a file such as `SKILL.md` or a task file, look for it inside `JENGA_PROJECT_DIR/{{SKILL_DISCOVERY_PATH}}` or `JENGA_PROJECT_DIR/project/board/` respectively.
44
+ - Commit messages and branch names follow the EST naming convention (`E<n>_S<n>_T<n>`).
45
+ - Agent definitions (scrum-master, developer, tester, and any others) live in the `agents/` directory that sits alongside the skills directory shown above, at the same discovery root.
46
+ <!-- JENGA:END -->