@jenga-ai/agent 1.2.0 → 1.2.2
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 +26 -0
- package/lib/commands/init.js +19 -0
- package/lib/config-schema.js +4 -4
- package/lib/generate-agent-context.js +233 -0
- package/package.json +34 -5
- package/scripts/validate_npm_metadata.sh +142 -0
- package/skills/distribute/scripts/distribute-changes.sh +2 -2
- package/skills/init/scripts/init.sh +12 -1
- package/skills/publish/adapters/npm-ci.md +1 -1
- package/skills/publish/scripts/publish_deploy.sh +10 -1
- package/skills/reconcile/scripts/detect-unlinked-code.sh +4 -2
- package/templates/agent-context.md.tpl +46 -0
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
|
+
[](https://www.npmjs.com/package/@jenga-ai/agent)
|
|
6
|
+
[](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
|
package/lib/commands/init.js
CHANGED
|
@@ -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;
|
package/lib/config-schema.js
CHANGED
|
@@ -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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jenga-ai/agent",
|
|
3
|
-
"version": "1.2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.2.2",
|
|
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": {
|
|
7
7
|
"access": "public"
|
|
@@ -9,8 +9,12 @@
|
|
|
9
9
|
"bin": {
|
|
10
10
|
"jenga": "./bin/jenga.js"
|
|
11
11
|
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=14.13.1"
|
|
14
|
+
},
|
|
12
15
|
"scripts": {
|
|
13
16
|
"postinstall": "node scripts/postinstall.js",
|
|
17
|
+
"validate:npm-metadata": "bash scripts/validate_npm_metadata.sh",
|
|
14
18
|
"ui:dev": "npm run ui:dev --prefix project/app",
|
|
15
19
|
"ui:build": "npm run ui:build --prefix project/app",
|
|
16
20
|
"api:start": "npm run api:start --prefix project/app",
|
|
@@ -32,10 +36,35 @@
|
|
|
32
36
|
],
|
|
33
37
|
"repository": {
|
|
34
38
|
"type": "git",
|
|
35
|
-
"url": "https://github.com/samwelmunga/jenga-npm"
|
|
39
|
+
"url": "git+https://github.com/samwelmunga/jenga-npm.git"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/samwelmunga/jenga-npm#readme",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/samwelmunga/jenga-npm/issues"
|
|
44
|
+
},
|
|
45
|
+
"keywords": [
|
|
46
|
+
"ai-agents",
|
|
47
|
+
"agentic",
|
|
48
|
+
"agentic-workflow",
|
|
49
|
+
"multi-agent",
|
|
50
|
+
"agent-framework",
|
|
51
|
+
"ai-development",
|
|
52
|
+
"claude",
|
|
53
|
+
"claude-code",
|
|
54
|
+
"agents-md",
|
|
55
|
+
"copilot",
|
|
56
|
+
"github-copilot",
|
|
57
|
+
"codex",
|
|
58
|
+
"skills",
|
|
59
|
+
"project-management",
|
|
60
|
+
"developer-tools",
|
|
61
|
+
"cli"
|
|
62
|
+
],
|
|
63
|
+
"author": {
|
|
64
|
+
"name": "Samwel Munga",
|
|
65
|
+
"email": "samwelmunga88@hotmail.se",
|
|
66
|
+
"url": "https://knappkod.se/jenga-ai"
|
|
36
67
|
},
|
|
37
|
-
"keywords": [],
|
|
38
|
-
"author": "",
|
|
39
68
|
"license": "MIT",
|
|
40
69
|
"dependencies": {
|
|
41
70
|
"@huggingface/transformers": "^4.2.0"
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# validate_npm_metadata.sh — deterministic check that package.json carries
|
|
4
|
+
# the metadata npm/GitHub discovery depends on (E41_S01_T02).
|
|
5
|
+
#
|
|
6
|
+
# Rationale: per the repo's "Scripts Over Inline Logic" principle
|
|
7
|
+
# (CLAUDE.md), this is a script rather than an agent instruction so that
|
|
8
|
+
# future drift in package.json's publish-facing fields (e.g. keywords
|
|
9
|
+
# accidentally cleared, or repository.url silently re-pointed at the
|
|
10
|
+
# private samwelmunga/JengaAgent repo instead of the public jenga-npm
|
|
11
|
+
# mirror) is caught deterministically instead of relying on an agent
|
|
12
|
+
# noticing during review.
|
|
13
|
+
#
|
|
14
|
+
# IMPORTANT — no pinned version check:
|
|
15
|
+
# This script intentionally does NOT compare package.json's `version`
|
|
16
|
+
# field against any hardcoded literal. E41_S01_T01 established that the
|
|
17
|
+
# locally committed version and the version actually published to npm
|
|
18
|
+
# drift independently over time, so pinning a literal here would go
|
|
19
|
+
# stale on the very next release. If a version check is wanted at all,
|
|
20
|
+
# it is structural only (present + semver-shaped), never a pinned value.
|
|
21
|
+
#
|
|
22
|
+
# Usage:
|
|
23
|
+
# bash scripts/validate_npm_metadata.sh
|
|
24
|
+
# npm run validate:npm-metadata
|
|
25
|
+
#
|
|
26
|
+
# Exit codes:
|
|
27
|
+
# 0 All checks passed.
|
|
28
|
+
# 1 One or more checks failed (each failure is printed with a
|
|
29
|
+
# descriptive message; a summary count follows).
|
|
30
|
+
#
|
|
31
|
+
# Dependencies: node only (already a required runtime for this project —
|
|
32
|
+
# see package.json's "engines" field). No new dependency is introduced.
|
|
33
|
+
|
|
34
|
+
set -euo pipefail
|
|
35
|
+
|
|
36
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
37
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
38
|
+
PACKAGE_JSON="${1:-$REPO_ROOT/package.json}"
|
|
39
|
+
|
|
40
|
+
if [[ ! -f "$PACKAGE_JSON" ]]; then
|
|
41
|
+
echo "FAIL: package.json not found at $PACKAGE_JSON" >&2
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# All validation logic lives in one embedded Node script to avoid the
|
|
46
|
+
# quoting/escaping pitfalls of parsing JSON arrays and nested objects from
|
|
47
|
+
# bash. Node prints one "PASS: ..." or "FAIL: ..." line per check to
|
|
48
|
+
# stdout, plus a final "SUMMARY: <n> failed" line. Bash only interprets
|
|
49
|
+
# the final summary line to decide the exit code and print the banner.
|
|
50
|
+
NODE_OUTPUT="$(node -e '
|
|
51
|
+
const fs = require("fs");
|
|
52
|
+
|
|
53
|
+
const pkgPath = process.argv[1];
|
|
54
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
55
|
+
|
|
56
|
+
const failures = [];
|
|
57
|
+
const pass = (msg) => console.log("PASS: " + msg);
|
|
58
|
+
const fail = (msg) => { failures.push(msg); console.log("FAIL: " + msg); };
|
|
59
|
+
|
|
60
|
+
// keywords: must exist, be a non-empty array
|
|
61
|
+
if (!Array.isArray(pkg.keywords) || pkg.keywords.length === 0) {
|
|
62
|
+
fail("keywords is empty or missing — package.json must declare a non-empty keywords array for npm/GitHub discoverability.");
|
|
63
|
+
} else {
|
|
64
|
+
pass("keywords is present with " + pkg.keywords.length + " entries.");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// author: must exist and be non-empty (string or object form)
|
|
68
|
+
const authorEmpty = (() => {
|
|
69
|
+
if (pkg.author == null) return true;
|
|
70
|
+
if (typeof pkg.author === "string") return pkg.author.trim().length === 0;
|
|
71
|
+
if (typeof pkg.author === "object") {
|
|
72
|
+
return !pkg.author.name || String(pkg.author.name).trim().length === 0;
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
})();
|
|
76
|
+
if (authorEmpty) {
|
|
77
|
+
fail("author is empty or missing — package.json must declare an author (string or {name, ...} object).");
|
|
78
|
+
} else {
|
|
79
|
+
pass("author is present.");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// repository / homepage / bugs: must all be present
|
|
83
|
+
if (pkg.repository == null || (typeof pkg.repository === "object" && Object.keys(pkg.repository).length === 0)) {
|
|
84
|
+
fail("repository is missing from package.json.");
|
|
85
|
+
} else {
|
|
86
|
+
pass("repository is present.");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!pkg.homepage || String(pkg.homepage).trim().length === 0) {
|
|
90
|
+
fail("homepage is missing from package.json.");
|
|
91
|
+
} else {
|
|
92
|
+
pass("homepage is present.");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (pkg.bugs == null || (typeof pkg.bugs === "object" && Object.keys(pkg.bugs).length === 0)) {
|
|
96
|
+
fail("bugs is missing from package.json.");
|
|
97
|
+
} else {
|
|
98
|
+
pass("bugs is present.");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// repository.url (object form) or repository (string form) must point at
|
|
102
|
+
// the PUBLIC mirror (samwelmunga/jenga-npm), never the private repo.
|
|
103
|
+
const PUBLIC_REPO_MARKER = "samwelmunga/jenga-npm";
|
|
104
|
+
if (pkg.repository != null) {
|
|
105
|
+
const repoUrl = typeof pkg.repository === "string"
|
|
106
|
+
? pkg.repository
|
|
107
|
+
: (pkg.repository.url || "");
|
|
108
|
+
|
|
109
|
+
if (!repoUrl || repoUrl.trim().length === 0) {
|
|
110
|
+
fail("repository is present but has no resolvable url.");
|
|
111
|
+
} else if (!repoUrl.includes(PUBLIC_REPO_MARKER)) {
|
|
112
|
+
fail("repository.url (\"" + repoUrl + "\") does not point at the public " + PUBLIC_REPO_MARKER + " repo — it must not reference a private repo (e.g. samwelmunga/JengaAgent) or any other location.");
|
|
113
|
+
} else {
|
|
114
|
+
pass("repository.url points at the public " + PUBLIC_REPO_MARKER + " repo.");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// version: structural semver check only — NEVER a pinned literal value.
|
|
119
|
+
// See header comment for why a pinned check would go stale immediately.
|
|
120
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+/;
|
|
121
|
+
if (!pkg.version || !SEMVER_RE.test(String(pkg.version))) {
|
|
122
|
+
fail("version (\"" + pkg.version + "\") is missing or not a valid semver-shaped string.");
|
|
123
|
+
} else {
|
|
124
|
+
pass("version (\"" + pkg.version + "\") is a valid semver-shaped string (no pinned-value check performed, by design).");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log("SUMMARY: " + failures.length + " failed");
|
|
128
|
+
process.exit(failures.length > 0 ? 1 : 0);
|
|
129
|
+
' "$PACKAGE_JSON")" && NODE_EXIT=0 || NODE_EXIT=$?
|
|
130
|
+
|
|
131
|
+
echo "$NODE_OUTPUT"
|
|
132
|
+
echo
|
|
133
|
+
|
|
134
|
+
FAILED_COUNT="$(echo "$NODE_OUTPUT" | grep -c '^FAIL:' || true)"
|
|
135
|
+
|
|
136
|
+
if [[ "$NODE_EXIT" -ne 0 || "$FAILED_COUNT" -gt 0 ]]; then
|
|
137
|
+
echo "npm publish metadata validation FAILED ($FAILED_COUNT check(s) failed). See FAIL lines above." >&2
|
|
138
|
+
exit 1
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
echo "All npm publish metadata checks passed."
|
|
142
|
+
exit 0
|
|
@@ -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
|
|
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="
|
|
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.
|
|
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"
|
|
@@ -190,7 +190,7 @@ A successful non-dry-run produces:
|
|
|
190
190
|
- A history entry in `project/logs/publish-history.json` written by
|
|
191
191
|
`publish_deploy.sh` with `platform_state: "triggered"`
|
|
192
192
|
|
|
193
|
-
## Post-
|
|
193
|
+
## Post-deploy manual steps
|
|
194
194
|
|
|
195
195
|
After a successful deploy trigger, the adapter prints the workflow run URL.
|
|
196
196
|
The operator should:
|
|
@@ -465,9 +465,18 @@ print_manual_steps() {
|
|
|
465
465
|
droplet)
|
|
466
466
|
adapter_doc="$PUBLISH_REPO_ROOT/skills/publish/adapters/droplet.md"
|
|
467
467
|
;;
|
|
468
|
-
|
|
468
|
+
npm)
|
|
469
|
+
adapter_doc="$PUBLISH_REPO_ROOT/skills/publish/adapters/npm.md"
|
|
470
|
+
;;
|
|
471
|
+
npm-ci)
|
|
472
|
+
adapter_doc="$PUBLISH_REPO_ROOT/skills/publish/adapters/npm-ci.md"
|
|
473
|
+
;;
|
|
474
|
+
mobile-ios)
|
|
469
475
|
adapter_doc="$PUBLISH_REPO_ROOT/skills/publish/adapters/mobile-ios.md"
|
|
470
476
|
;;
|
|
477
|
+
*)
|
|
478
|
+
return 0
|
|
479
|
+
;;
|
|
471
480
|
esac
|
|
472
481
|
[[ -f "$adapter_doc" ]] || return 0
|
|
473
482
|
local manual_steps
|
|
@@ -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 (
|
|
513
|
-
#
|
|
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 -->
|