@jenga-ai/agent 3.1.1 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -12
- package/agents/developer.md +31 -16
- package/agents/scrum-master.md +18 -17
- package/agents/tester.md +25 -15
- package/bin/jenga.js +10 -0
- package/lib/commands/dashboard.js +92 -0
- package/lib/skill-allow-list.json +7 -2
- package/package.json +21 -2
- package/project/app/api/lib/resolve-project-root.js +120 -0
- package/project/app/api/package.json +16 -0
- package/project/app/api/parsers/architecture.js +72 -0
- package/project/app/api/parsers/board.js +141 -0
- package/project/app/api/parsers/documentation.js +125 -0
- package/project/app/api/parsers/git-log.js +52 -0
- package/project/app/api/parsers/ideas.js +62 -0
- package/project/app/api/parsers/knowledge-graph.js +73 -0
- package/project/app/api/parsers/lib/markdown-dir-reader.js +163 -0
- package/project/app/api/parsers/rapports.js +148 -0
- package/project/app/api/parsers/todo.js +179 -0
- package/project/app/api/response.js +47 -0
- package/project/app/api/routes/architecture.js +23 -0
- package/project/app/api/routes/board.js +46 -0
- package/project/app/api/routes/documentation.js +24 -0
- package/project/app/api/routes/health.js +25 -0
- package/project/app/api/routes/history.js +55 -0
- package/project/app/api/routes/rapports.js +24 -0
- package/project/app/api/scripts/capture-snapshot.js +294 -0
- package/project/app/api/server.js +112 -0
- package/project/app/api/types.js +40 -0
- package/project/app/package.json +21 -0
- package/project/app/ui/dist/assets/index-7fj-vllY.js +104 -0
- package/project/app/ui/dist/assets/index-CdK3Qrep.css +1 -0
- package/project/app/ui/dist/index.html +13 -0
- package/project/app/ui/package.json +23 -0
- package/project/app/ui/scripts/build-snapshot-html.cjs +214 -0
- package/project/app/ui/scripts/dashboard-open.cjs +88 -0
- package/project/app/ui/scripts/dashboard-start.cjs +87 -0
- package/scripts/acquire-concurrency-slot.sh +220 -0
- package/scripts/audit-twin-divergence.sh +625 -0
- package/scripts/check-public-playbook-steps.sh +136 -0
- package/scripts/compute-deploy-reconcile.sh +439 -0
- package/scripts/jenga-permission-level-switch.sh +19 -3
- package/scripts/mark-deployed.sh +532 -0
- package/scripts/populate-knowledge-graph.js +429 -0
- package/scripts/release-concurrency-slot.sh +129 -0
- package/scripts/validate-board.sh +60 -2
- package/scripts/verify-consumer-install.sh +470 -0
- package/skills/j-close-story/SKILL.md +1 -1
- package/skills/j-cloud-connect/SKILL.md +95 -0
- package/skills/j-cloud-connect/scripts/configure-backend.sh +267 -0
- package/skills/j-cloud-connect/scripts/install-rclone.sh +153 -0
- package/skills/j-dashboard/SKILL.md +144 -0
- package/skills/j-dashboard/scripts/launch.sh +121 -0
- package/skills/j-dashboard/scripts/resolve-app-dir.sh +164 -0
- package/skills/j-dashboard/scripts/snapshot.sh +267 -0
- package/skills/j-dashboard-share/SKILL.md +96 -0
- package/skills/j-dashboard-share/scripts/upload-snapshot.sh +173 -0
- package/skills/j-do/SKILL.md +19 -19
- package/skills/j-doc-sync/SKILL.md +12 -1
- package/skills/j-idea/SKILL.md +1 -1
- package/skills/j-init/SKILL.md +5 -4
- package/skills/j-init/assets/directory_structure.txt +1 -0
- package/skills/j-init/scripts/detect-existing-codebase.sh +2 -2
- package/skills/j-init/scripts/init.sh +13 -2
- package/skills/j-playbook/SKILL.md +93 -0
- package/skills/j-playbook-new/SKILL.md +155 -0
- package/skills/j-playbook-new/scripts/playbook-new.sh +332 -0
- package/skills/j-proceed/SKILL.md +1 -1
- package/skills/j-publish/SKILL.md +1 -1
- package/skills/j-publish/adapters/npm-ci.md +29 -0
- package/skills/j-publish/scripts/npm_ci_pipeline.sh +9 -0
- package/skills/j-publish/scripts/npm_pipeline.sh +18 -0
- package/skills/j-publish/scripts/npm_stage_pipeline.sh +81 -41
- package/skills/j-reconcile/SKILL.md +1 -0
- package/skills/j-redo/SKILL.md +1 -1
- package/skills/j-status/SKILL.md +12 -0
- package/skills/j-todo/SKILL.md +2 -2
- package/skills/j-uncharted/SKILL.md +8 -7
- package/skills/j-uncharted/scripts/validate-proposed-items.sh +18 -2
- package/skills/jenga/SKILL.md +55 -16
- package/skills/jenga/playbooks/idea-to-committed.json +20 -0
- package/skills/jenga/playbooks/schema.json +1 -1
- package/skills/jenga/scripts/load-nl-catalog.js +22 -6
- package/skills/jenga/scripts/load-playbooks.sh +968 -41
- package/skills/jenga/scripts/match-playbook.sh +1 -1
- package/skills/jenga/scripts/render-playbook-confirmation.sh +162 -8
- package/skills/jenga/scripts/run-playbook-step.sh +535 -42
- package/skills/jenga-permission-level/SKILL.md +4 -4
- package/templates/KNOWLEDGE_GRAPH_STUB_SCHEMA_TEMPLATE.md +128 -0
- package/templates/SCRUM_BOARD_SCHEMA.md +18 -6
- package/templates/playbook-types.json +8 -0
- package/skills/jenga/playbooks/brainstorm-to-mirror.json +0 -22
package/bin/jenga.js
CHANGED
|
@@ -21,6 +21,11 @@ Usage:
|
|
|
21
21
|
jenga status Show router status and active session
|
|
22
22
|
jenga doctor Scan .agents/ and .claude/ for orphaned package files and clean up
|
|
23
23
|
interactively (alias: jenga clean). Add --dry-run to preview only.
|
|
24
|
+
jenga dashboard start [--port <n>] [--serve-app]
|
|
25
|
+
Start the project dashboard API server (and optionally serve the
|
|
26
|
+
built UI). Port defaults to 3001, or JENGA_API_PORT if set.
|
|
27
|
+
jenga dashboard open [--port <n>]
|
|
28
|
+
Health-check the dashboard and open it in your browser.
|
|
24
29
|
|
|
25
30
|
Options:
|
|
26
31
|
--version, -v Print version
|
|
@@ -67,6 +72,11 @@ async function main() {
|
|
|
67
72
|
await runDoctor(args);
|
|
68
73
|
break;
|
|
69
74
|
}
|
|
75
|
+
case "dashboard": {
|
|
76
|
+
const { runDashboard } = await import("../lib/commands/dashboard.js");
|
|
77
|
+
await runDashboard(args);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
70
80
|
default:
|
|
71
81
|
console.error(`Unknown command: ${cmd}\n`);
|
|
72
82
|
console.log(USAGE);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/commands/dashboard.js — `jenga dashboard start` / `jenga dashboard open` (E47_S03_T01)
|
|
3
|
+
*
|
|
4
|
+
* Reuse decision (documented per this task's `needs_docs: true`)
|
|
5
|
+
* ────────────────────────────────────────────────────────────────
|
|
6
|
+
* `project/app/ui/scripts/dashboard-start.cjs` and `dashboard-open.cjs` are standalone CommonJS
|
|
7
|
+
* scripts that parse `process.argv.slice(2)` directly at the top level and act on it immediately
|
|
8
|
+
* (start listening / run a health check then `process.exit`). They are not written as importable,
|
|
9
|
+
* args-parameterized functions.
|
|
10
|
+
*
|
|
11
|
+
* Two reuse strategies were considered:
|
|
12
|
+
* 1. Refactor their bodies into an exported function both the `.cjs` entry points and this
|
|
13
|
+
* module call.
|
|
14
|
+
* 2. Spawn the existing scripts as a child process, forwarding args and exit code.
|
|
15
|
+
*
|
|
16
|
+
* Chosen: (2), spawning as a child process. Refactoring (1) would require restructuring two
|
|
17
|
+
* scripts that are independently relied on elsewhere (root `package.json`'s `dashboard:start`/
|
|
18
|
+
* `dashboard:open` npm scripts, `skills/j-dashboard`'s launcher, and
|
|
19
|
+
* `scripts/verify-consumer-install.sh`'s Scenario E regression check) for a task whose job is CLI
|
|
20
|
+
* wiring, not a dashboard-scripts refactor. Spawning with `stdio: 'inherit'` reuses the scripts
|
|
21
|
+
* completely verbatim — zero duplicated port-parsing / `--serve-app` / health-check logic — and
|
|
22
|
+
* guarantees byte-identical output and exit-code behavior to running the `.cjs` scripts directly
|
|
23
|
+
* (this is what AC1/AC2's "same behavior" requirement asks for literally). It also avoids the
|
|
24
|
+
* process-global side effects a same-process dynamic `import()` would risk: both scripts call
|
|
25
|
+
* `process.exit()` directly at top level, which would kill the parent `bin/jenga.js` process
|
|
26
|
+
* immediately and unrecoverably if loaded in-process.
|
|
27
|
+
*/
|
|
28
|
+
import { spawn } from "child_process";
|
|
29
|
+
import { existsSync } from "fs";
|
|
30
|
+
import { join, dirname } from "path";
|
|
31
|
+
import { fileURLToPath } from "url";
|
|
32
|
+
|
|
33
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
const projectRoot = join(__dirname, "..", "..");
|
|
35
|
+
|
|
36
|
+
const SUBCOMMANDS = {
|
|
37
|
+
start: "dashboard-start.cjs",
|
|
38
|
+
open: "dashboard-open.cjs",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const DASHBOARD_USAGE = `
|
|
42
|
+
Usage:
|
|
43
|
+
jenga dashboard start [--port <n>] [--serve-app] Start the dashboard API server
|
|
44
|
+
(and optionally serve the built UI)
|
|
45
|
+
jenga dashboard open [--port <n>] Health-check the dashboard and open it
|
|
46
|
+
in your browser
|
|
47
|
+
`.trim();
|
|
48
|
+
|
|
49
|
+
export async function runDashboard(args, root = projectRoot) {
|
|
50
|
+
const [sub, ...rest] = args;
|
|
51
|
+
|
|
52
|
+
if (!sub || !(sub in SUBCOMMANDS)) {
|
|
53
|
+
if (sub) {
|
|
54
|
+
console.error(`Unknown dashboard subcommand: ${sub}\n`);
|
|
55
|
+
} else {
|
|
56
|
+
console.error("Missing dashboard subcommand.\n");
|
|
57
|
+
}
|
|
58
|
+
console.log(DASHBOARD_USAGE);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const scriptPath = join(root, "project", "app", "ui", "scripts", SUBCOMMANDS[sub]);
|
|
64
|
+
if (!existsSync(scriptPath)) {
|
|
65
|
+
console.error(`Error: dashboard script not found at ${scriptPath}`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
const child = spawn(process.execPath, [scriptPath, ...rest], {
|
|
72
|
+
stdio: "inherit",
|
|
73
|
+
env: process.env,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
child.on("exit", (code, signal) => {
|
|
77
|
+
if (signal) {
|
|
78
|
+
// Re-raise the same signal on ourselves so a Ctrl-C style termination propagates
|
|
79
|
+
// cleanly to any caller inspecting our own exit status, rather than reporting a
|
|
80
|
+
// fabricated exit code for a signal-based termination.
|
|
81
|
+
process.kill(process.pid, signal);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
process.exit(code === null ? 1 : code);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
child.on("error", (err) => {
|
|
88
|
+
console.error(`Error: failed to launch dashboard ${sub}: ${err.message}`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generated_at": "2026-09-
|
|
3
|
-
"skill_count":
|
|
2
|
+
"generated_at": "2026-09-14T18:42:41.263Z",
|
|
3
|
+
"skill_count": 41,
|
|
4
4
|
"skills": [
|
|
5
5
|
"brainstorm",
|
|
6
6
|
"btw",
|
|
7
7
|
"clearify",
|
|
8
8
|
"close-story",
|
|
9
|
+
"cloud-connect",
|
|
9
10
|
"commit",
|
|
10
11
|
"continue",
|
|
12
|
+
"dashboard",
|
|
13
|
+
"dashboard-share",
|
|
11
14
|
"deep-dive",
|
|
12
15
|
"dev-done",
|
|
13
16
|
"distribute",
|
|
@@ -27,6 +30,8 @@
|
|
|
27
30
|
"jenga-permission-level",
|
|
28
31
|
"lgtm",
|
|
29
32
|
"pi-plan",
|
|
33
|
+
"playbook",
|
|
34
|
+
"playbook-new",
|
|
30
35
|
"proceed",
|
|
31
36
|
"publish",
|
|
32
37
|
"reconcile",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jenga-ai/agent",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "An agentic development workflow for Claude Code, Copilot, and Codex — with a persistent Epic/Story/Task board, an isolated git worktree per task, and a separate tester agent that runs your test suite before anything is marked done.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
16
|
"postinstall": "node scripts/postinstall.js",
|
|
17
|
+
"prepack": "npm run ui:build --prefix project/app --",
|
|
17
18
|
"generate:legacy-paths": "node scripts/generate-legacy-shipped-paths.js",
|
|
19
|
+
"graph:populate": "node scripts/populate-knowledge-graph.js",
|
|
18
20
|
"test": "bats tests/*.bats",
|
|
19
21
|
"validate:npm-metadata": "bash scripts/validate_npm_metadata.sh",
|
|
20
22
|
"ui:dev": "npm run ui:dev --prefix project/app --",
|
|
@@ -44,7 +46,19 @@
|
|
|
44
46
|
"mcp/training_runner/package.json",
|
|
45
47
|
"mcp/training_runner/package-lock.json",
|
|
46
48
|
"README.md",
|
|
47
|
-
"LICENSE"
|
|
49
|
+
"LICENSE",
|
|
50
|
+
"project/app/api/**/*.js",
|
|
51
|
+
"project/app/api/routes/**",
|
|
52
|
+
"project/app/api/lib/**",
|
|
53
|
+
"project/app/api/parsers/**",
|
|
54
|
+
"project/app/api/package.json",
|
|
55
|
+
"project/app/ui/scripts/**",
|
|
56
|
+
"project/app/ui/dist/**",
|
|
57
|
+
"project/app/package.json",
|
|
58
|
+
"project/app/ui/package.json",
|
|
59
|
+
"!project/app/api/**/*.test.js",
|
|
60
|
+
"!project/app/api/node_modules/**",
|
|
61
|
+
"!project/app/ui/node_modules/**"
|
|
48
62
|
],
|
|
49
63
|
"repository": {
|
|
50
64
|
"type": "git",
|
|
@@ -82,6 +96,11 @@
|
|
|
82
96
|
"url": "https://knappkod.se/jenga-ai"
|
|
83
97
|
},
|
|
84
98
|
"license": "MIT",
|
|
99
|
+
"dependencies": {
|
|
100
|
+
"cors": "^2.8.5",
|
|
101
|
+
"express": "^4.18.2",
|
|
102
|
+
"gray-matter": "^4.0.3"
|
|
103
|
+
},
|
|
85
104
|
"devDependencies": {
|
|
86
105
|
"bats": "^1.13.0"
|
|
87
106
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file project/app/api/lib/resolve-project-root.js
|
|
3
|
+
*
|
|
4
|
+
* Resolves the invoking (consumer) project's root directory for the dashboard API, so that
|
|
5
|
+
* `project/app/api/parsers/*.js` never has to compute its data root via a fixed
|
|
6
|
+
* `path.resolve(__dirname, '../../../...')` climb — the same defect pattern `E46_S01` already fixed
|
|
7
|
+
* for `/init` (see `skills/init/scripts/init.sh`'s `PKG_ROOT` idiom and
|
|
8
|
+
* `lib/generate-agent-context.js`'s `realpathSync` symlink-safe comparison). A `__dirname` climb only
|
|
9
|
+
* ever resolves correctly when this module runs from this monorepo's own checkout; once mirrored into
|
|
10
|
+
* a real consumer's `node_modules/@jenga-ai/agent/`, climbing a fixed number of levels lands inside or
|
|
11
|
+
* above `node_modules`, never at the consuming project's own root.
|
|
12
|
+
*
|
|
13
|
+
* Resolution order:
|
|
14
|
+
* 1. Explicit override — the `JENGA_PROJECT_ROOT` env var. This is the primary mechanism for the
|
|
15
|
+
* real npm-consumer case: the dashboard's own launch path (`dashboard-start.cjs` /
|
|
16
|
+
* `dashboard-open.cjs` / `server.js`) already knows the invoking `cwd` and can set this before
|
|
17
|
+
* the parsers ever load.
|
|
18
|
+
* 2. Walk up from `cwd` (default `process.cwd()`) looking for a `project/board` directory — the
|
|
19
|
+
* marker every Jenga-initialized project has (see `/init`'s scaffold) — bounded to a generous
|
|
20
|
+
* but finite number of parent levels.
|
|
21
|
+
* 3. Fail loudly. Never silently fall back to `__dirname`, `cwd` itself, or `null` — an unresolved
|
|
22
|
+
* project root is always a thrown `Error` with a descriptive message naming both the override
|
|
23
|
+
* var and the path that was walked.
|
|
24
|
+
*
|
|
25
|
+
* Both the override path and every step of the walk-up are resolved through `fs.realpathSync`, so a
|
|
26
|
+
* symlinked invocation path (macOS `/tmp`/`$TMPDIR`, `npm link`, a symlinked home directory — the same
|
|
27
|
+
* bug class `E46_S01` fixed) does not break resolution.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
'use strict';
|
|
31
|
+
|
|
32
|
+
const fs = require('fs');
|
|
33
|
+
const path = require('path');
|
|
34
|
+
|
|
35
|
+
const OVERRIDE_ENV_VAR = 'JENGA_PROJECT_ROOT';
|
|
36
|
+
const PROJECT_MARKER = path.join('project', 'board');
|
|
37
|
+
const MAX_WALK_LEVELS = 20;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {string} dir absolute, already-realpath'd directory
|
|
41
|
+
* @returns {boolean} true if `dir/project/board` exists and is a directory
|
|
42
|
+
*/
|
|
43
|
+
function hasProjectMarker(dir) {
|
|
44
|
+
try {
|
|
45
|
+
return fs.statSync(path.join(dir, PROJECT_MARKER)).isDirectory();
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve an absolute, symlink-free directory path, throwing a descriptive error (not a raw ENOENT)
|
|
53
|
+
* if it doesn't exist.
|
|
54
|
+
* @param {string} label human-readable label used in the error message
|
|
55
|
+
* @param {string} rawPath the path as provided (env var value or cwd)
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
function realpathOrThrow(label, rawPath) {
|
|
59
|
+
const resolved = path.resolve(rawPath);
|
|
60
|
+
try {
|
|
61
|
+
return fs.realpathSync(resolved);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`[resolve-project-root] ${label} "${rawPath}" (resolved to "${resolved}") does not exist: ${err.message}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Determine the invoking consumer project's root directory.
|
|
71
|
+
*
|
|
72
|
+
* @param {Object} [options]
|
|
73
|
+
* @param {string} [options.cwd] working directory to resolve/walk from (defaults to `process.cwd()`)
|
|
74
|
+
* @param {NodeJS.ProcessEnv} [options.env] environment to read the override from (defaults to `process.env`)
|
|
75
|
+
* @returns {string} absolute, symlink-resolved path to the project root
|
|
76
|
+
* @throws {Error} if no project root can be determined (fail loudly — never returns a wrong/empty path)
|
|
77
|
+
*/
|
|
78
|
+
function resolveProjectRoot({ cwd = process.cwd(), env = process.env } = {}) {
|
|
79
|
+
// 1. Explicit override.
|
|
80
|
+
const override = env && env[OVERRIDE_ENV_VAR];
|
|
81
|
+
if (override) {
|
|
82
|
+
const resolvedOverride = realpathOrThrow(`${OVERRIDE_ENV_VAR}`, override);
|
|
83
|
+
if (!fs.statSync(resolvedOverride).isDirectory()) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`[resolve-project-root] ${OVERRIDE_ENV_VAR} "${override}" (resolved to "${resolvedOverride}") is not a directory.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return resolvedOverride;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 2. Walk up from cwd looking for a project/board marker. realpath the starting point once so
|
|
92
|
+
// every subsequent path.dirname() step stays symlink-free.
|
|
93
|
+
let startDir;
|
|
94
|
+
try {
|
|
95
|
+
startDir = fs.realpathSync(path.resolve(cwd));
|
|
96
|
+
} catch (err) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`[resolve-project-root] cwd "${cwd}" does not exist: ${err.message}. ` +
|
|
99
|
+
`Set ${OVERRIDE_ENV_VAR} to your project's root directory instead.`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let dir = startDir;
|
|
104
|
+
for (let i = 0; i < MAX_WALK_LEVELS; i++) {
|
|
105
|
+
if (hasProjectMarker(dir)) return dir;
|
|
106
|
+
const parent = path.dirname(dir);
|
|
107
|
+
if (parent === dir) break; // reached filesystem root
|
|
108
|
+
dir = parent;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 3. Fail loudly.
|
|
112
|
+
throw new Error(
|
|
113
|
+
`[resolve-project-root] Could not locate a project root (looked for a "${PROJECT_MARKER}" ` +
|
|
114
|
+
`directory) walking up from "${startDir}" (${MAX_WALK_LEVELS} levels). ` +
|
|
115
|
+
`Set the ${OVERRIDE_ENV_VAR} environment variable to your project's root directory, or run ` +
|
|
116
|
+
`the dashboard from inside a Jenga-initialized project.`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = { resolveProjectRoot, OVERRIDE_ENV_VAR, PROJECT_MARKER, MAX_WALK_LEVELS };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jenga-dashboard-api",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Jenga AI dashboard API server",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node server.js",
|
|
8
|
+
"dev": "node --watch server.js",
|
|
9
|
+
"test": "node parsers/knowledge-graph.test.js && node lib/resolve-project-root.test.js && node parsers/lib/markdown-dir-reader.test.js && node parsers/todo.test.js && node parsers/documentation.test.js && node parsers/rapports.test.js"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"cors": "^2.8.5",
|
|
13
|
+
"express": "^4.18.2",
|
|
14
|
+
"gray-matter": "^4.0.3"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file project/app/api/parsers/architecture.js
|
|
3
|
+
* Reads package.json and project.config.json to return tech stack + dependency info.
|
|
4
|
+
* The SAD map itself is sourced from project/knowledge-graph/graph.json via ./knowledge-graph.js
|
|
5
|
+
* (E08_S05_T01) rather than parsed live from board epics/stories.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { readSADMap } = require('./knowledge-graph');
|
|
11
|
+
const { resolveProjectRoot } = require('../lib/resolve-project-root');
|
|
12
|
+
|
|
13
|
+
// Resolved relative to the invoking project's own root (E47_S02_T01/T02), not a fixed __dirname
|
|
14
|
+
// climb — see project/app/api/lib/resolve-project-root.js.
|
|
15
|
+
const ROOT = resolveProjectRoot();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Safely read and parse a JSON file.
|
|
19
|
+
* @param {string} filePath
|
|
20
|
+
* @returns {Object|null}
|
|
21
|
+
*/
|
|
22
|
+
function readJson(filePath) {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse project config files into architecture metadata.
|
|
32
|
+
* @returns {Promise<Object>}
|
|
33
|
+
*/
|
|
34
|
+
async function parseArchitecture() {
|
|
35
|
+
const pkg = readJson(path.join(ROOT, 'package.json'));
|
|
36
|
+
const config = readJson(path.join(ROOT, 'project.config.json'));
|
|
37
|
+
|
|
38
|
+
// Build tech stack from project.config.json fields + package metadata
|
|
39
|
+
const tech_stack = [];
|
|
40
|
+
if (config) {
|
|
41
|
+
if (config.workflow) tech_stack.push({ name: config.workflow, description: `Workflow engine (v${(pkg && pkg.version) || 'unknown'})` }); // E26_S01_T03: version from package.json, not deprecated workflow_version
|
|
42
|
+
if (config.description) tech_stack.push({ name: 'Jenga AI', description: config.description });
|
|
43
|
+
}
|
|
44
|
+
if (pkg) {
|
|
45
|
+
tech_stack.push({ name: 'Node.js', description: 'JavaScript runtime' });
|
|
46
|
+
const express = pkg.dependencies && pkg.dependencies['express'];
|
|
47
|
+
if (express) tech_stack.push({ name: 'Express', description: `HTTP server framework (${express})` });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Build dependency list
|
|
51
|
+
const dependencies = [];
|
|
52
|
+
if (pkg) {
|
|
53
|
+
for (const [name, version] of Object.entries(pkg.dependencies || {})) {
|
|
54
|
+
dependencies.push({ name, version, type: 'runtime' });
|
|
55
|
+
}
|
|
56
|
+
for (const [name, version] of Object.entries(pkg.devDependencies || {})) {
|
|
57
|
+
dependencies.push({ name, version, type: 'devDependency' });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
tech_stack,
|
|
63
|
+
dependencies,
|
|
64
|
+
sad_map: readSADMap(),
|
|
65
|
+
_sources: {
|
|
66
|
+
package_json: pkg ? { name: pkg.name, version: pkg.version } : null,
|
|
67
|
+
project_config: config || null,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { parseArchitecture };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file project/app/api/parsers/board.js
|
|
3
|
+
* Parses markdown files from project/board/ into a nested epic→story→task tree.
|
|
4
|
+
*
|
|
5
|
+
* E06_S05_T04 — each item whose id is named by an active entry in the project's `project/todo.md`
|
|
6
|
+
* additionally carries `_queued: true`. This is purely *informational* provenance ("the user has
|
|
7
|
+
* queued this for execution"); the decision of what to do with it belongs to the consumer. The
|
|
8
|
+
* Active Sprint tab's column logic (`project/app/ui/src/components/board/kanbanColumns.js`) uses it
|
|
9
|
+
* to promote `Pending`/`Backlog` items into the In Progress column; the Backlog tab ignores it
|
|
10
|
+
* entirely. Items that aren't referenced get **no new field at all**, so a project with no
|
|
11
|
+
* `todo.md` produces a byte-identical payload to the pre-T04 parser.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const matter = require('gray-matter');
|
|
17
|
+
const { resolveProjectRoot } = require('../lib/resolve-project-root');
|
|
18
|
+
const { readTodoRefs } = require('./todo');
|
|
19
|
+
|
|
20
|
+
// Resolved relative to the invoking project's own root (E47_S02_T01/T02), not a fixed __dirname
|
|
21
|
+
// climb — the old `path.resolve(__dirname, '../../../board')` only ever landed correctly when this
|
|
22
|
+
// module ran from this monorepo's own checkout.
|
|
23
|
+
const BOARD_ROOT = path.join(resolveProjectRoot(), 'project', 'board');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read all .md files from a directory (non-recursive).
|
|
27
|
+
* @param {string} dir
|
|
28
|
+
* @returns {{ file: string, data: Object, content: string }[]}
|
|
29
|
+
*/
|
|
30
|
+
function readMarkdownDir(dir) {
|
|
31
|
+
if (!fs.existsSync(dir)) return [];
|
|
32
|
+
return fs
|
|
33
|
+
.readdirSync(dir)
|
|
34
|
+
.filter((f) => f.endsWith('.md'))
|
|
35
|
+
.map((f) => {
|
|
36
|
+
const filePath = path.join(dir, f);
|
|
37
|
+
try {
|
|
38
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
39
|
+
const parsed = matter(raw);
|
|
40
|
+
return { file: f, data: parsed.data, content: parsed.content.trim() };
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.warn(`[board] Skipping malformed file: ${filePath} — ${err.message}`);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Board ids referenced by active entries in the project's `project/todo.md`, as an upper-cased Set
|
|
51
|
+
* for case-insensitive lookup (E06_S05_T04).
|
|
52
|
+
*
|
|
53
|
+
* Failure-tolerant by design: `readTodoRefs()` already reports a missing/unreadable file as
|
|
54
|
+
* `exists: false` rather than throwing, and this extra guard covers anything unexpected beyond that
|
|
55
|
+
* (e.g. an unresolvable project root). A broken or absent `todo.md` degrades to "nothing is queued"
|
|
56
|
+
* — i.e. exactly the pre-T04 behavior — and never turns a working board read into a 500.
|
|
57
|
+
*
|
|
58
|
+
* @returns {Set<string>}
|
|
59
|
+
*/
|
|
60
|
+
function loadQueuedIds() {
|
|
61
|
+
try {
|
|
62
|
+
const { refs } = readTodoRefs();
|
|
63
|
+
return new Set(refs.map((r) => String(r).toUpperCase()));
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.warn(`[board] Could not read todo.md refs — ${err.message}. No items will be marked queued.`);
|
|
66
|
+
return new Set();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* `{ _queued: true }` if `id` is queued, otherwise an empty object — spread into an item so
|
|
72
|
+
* unreferenced items keep their exact pre-T04 shape (no `_queued: false` noise).
|
|
73
|
+
* @param {Set<string>} queuedIds
|
|
74
|
+
* @param {string|undefined} id
|
|
75
|
+
* @returns {{ _queued?: boolean }}
|
|
76
|
+
*/
|
|
77
|
+
function queuedFlagFor(queuedIds, id) {
|
|
78
|
+
return typeof id === 'string' && queuedIds.has(id.toUpperCase()) ? { _queued: true } : {};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Parse the full board into an array of epic objects with nested stories and tasks.
|
|
83
|
+
* @returns {Promise<Object[]>}
|
|
84
|
+
*/
|
|
85
|
+
async function parseBoard() {
|
|
86
|
+
const queuedIds = loadQueuedIds();
|
|
87
|
+
|
|
88
|
+
const epicsDir = path.join(BOARD_ROOT, 'epics');
|
|
89
|
+
const storiesDir = path.join(BOARD_ROOT, 'stories');
|
|
90
|
+
const tasksDir = path.join(BOARD_ROOT, 'tasks');
|
|
91
|
+
|
|
92
|
+
const epicFiles = readMarkdownDir(epicsDir);
|
|
93
|
+
const storyFiles = readMarkdownDir(storiesDir);
|
|
94
|
+
const taskFiles = readMarkdownDir(tasksDir);
|
|
95
|
+
|
|
96
|
+
// Build tasks map keyed by story_id
|
|
97
|
+
const tasksByStory = {};
|
|
98
|
+
for (const t of taskFiles) {
|
|
99
|
+
const sid = t.data.story_id;
|
|
100
|
+
if (!sid) continue;
|
|
101
|
+
if (!tasksByStory[sid]) tasksByStory[sid] = [];
|
|
102
|
+
tasksByStory[sid].push({
|
|
103
|
+
...t.data,
|
|
104
|
+
...queuedFlagFor(queuedIds, t.data.id),
|
|
105
|
+
_content: t.content,
|
|
106
|
+
_file: t.file,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Build stories map keyed by epic_id
|
|
111
|
+
const storiesByEpic = {};
|
|
112
|
+
for (const s of storyFiles) {
|
|
113
|
+
const eid = s.data.epic_id;
|
|
114
|
+
if (!eid) continue;
|
|
115
|
+
if (!storiesByEpic[eid]) storiesByEpic[eid] = [];
|
|
116
|
+
const storyId = s.data.id;
|
|
117
|
+
storiesByEpic[eid].push({
|
|
118
|
+
...s.data,
|
|
119
|
+
...queuedFlagFor(queuedIds, storyId),
|
|
120
|
+
_content: s.content,
|
|
121
|
+
_file: s.file,
|
|
122
|
+
tasks: storyId ? (tasksByStory[storyId] || []) : [],
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Build epic objects
|
|
127
|
+
const epics = epicFiles.map((e) => {
|
|
128
|
+
const epicId = e.data.id;
|
|
129
|
+
return {
|
|
130
|
+
...e.data,
|
|
131
|
+
...queuedFlagFor(queuedIds, epicId),
|
|
132
|
+
_content: e.content,
|
|
133
|
+
_file: e.file,
|
|
134
|
+
stories: epicId ? (storiesByEpic[epicId] || []) : [],
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return epics;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = { parseBoard };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file project/app/api/parsers/documentation.js
|
|
3
|
+
* Aggregates the project's curated reference documentation into one flat, categorized list of
|
|
4
|
+
* full-content entries — the "Documentation" tab's backend source (`E58_S01_T04`, consumed by
|
|
5
|
+
* `E58_S01_T05`'s `GET /v1/documentation` route).
|
|
6
|
+
*
|
|
7
|
+
* Four sources, none of which any existing parser touches today:
|
|
8
|
+
* - `project/PROJECT_SUMMARY.md` -> category `summary`
|
|
9
|
+
* - `README.md` (repo root) -> category `readme`
|
|
10
|
+
* - `docs/STRATEGY.md` -> category `strategy`
|
|
11
|
+
* - every `.md` file under `project/documentation/examples/` -> category `example`
|
|
12
|
+
*
|
|
13
|
+
* The first three are single named files, read directly and parsed with `gray-matter` for
|
|
14
|
+
* consistency with the directory-based path even though they rarely carry frontmatter. The fourth
|
|
15
|
+
* reuses `E58_S01_T01`'s shared `readMarkdownDirRecursive()` directory reader.
|
|
16
|
+
*
|
|
17
|
+
* Every returned entry shares the same field shape used by `E58_S01_T03`'s rapports aggregate:
|
|
18
|
+
* { file, data, content, category, date }
|
|
19
|
+
* `readMarkdownDirRecursive()`'s own return contract does not include `date` (see its JSDoc), so
|
|
20
|
+
* it is added here per entry — same pattern already used by `parsers/ideas.js` (E58_S01_T02) and
|
|
21
|
+
* `parsers/rapports.js`: prefer frontmatter's `date` field, stringified, else `null`.
|
|
22
|
+
*
|
|
23
|
+
* A missing single-file source (e.g. no `docs/STRATEGY.md` yet in some projects) is skipped, not
|
|
24
|
+
* thrown — the same non-throwing precedent used throughout `api/parsers/`.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
'use strict';
|
|
28
|
+
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
const matter = require('gray-matter');
|
|
32
|
+
const { resolveProjectRoot } = require('../lib/resolve-project-root');
|
|
33
|
+
const { readMarkdownDirRecursive } = require('./lib/markdown-dir-reader');
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Derive the `date` field for an entry from its parsed frontmatter, matching the
|
|
37
|
+
* `rapports.js`/`ideas.js` convention: prefer frontmatter's `date`, stringified; else `null`.
|
|
38
|
+
*
|
|
39
|
+
* Unquoted `YYYY-MM-DD` frontmatter values are parsed by gray-matter/js-yaml into a native `Date`
|
|
40
|
+
* object rather than a plain string (the same gotcha `E06_S05_T03` hit and fixed for
|
|
41
|
+
* `isStaleDeployedProd()` — see `project/rapports/problems/E06_S05_T03-stale-filter-fails-on-real-gray-matter-date-objects.md`).
|
|
42
|
+
* A bare `String(date)` on a `Date` produces a full JS date-with-timezone string, not the
|
|
43
|
+
* `YYYY-MM-DD` form callers expect, so `Date` inputs are explicitly reduced to their ISO calendar
|
|
44
|
+
* date (`toISOString().slice(0, 10)`) instead.
|
|
45
|
+
* @param {Object} data - gray-matter-parsed frontmatter object.
|
|
46
|
+
* @returns {string|null}
|
|
47
|
+
*/
|
|
48
|
+
function deriveDate(data) {
|
|
49
|
+
if (!data || !data.date) return null;
|
|
50
|
+
if (data.date instanceof Date) return data.date.toISOString().slice(0, 10);
|
|
51
|
+
return String(data.date);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Read and parse a single markdown file into an aggregate entry, or `null` if the file does not
|
|
56
|
+
* exist or fails to read/parse (skipped with a `console.warn`, never thrown).
|
|
57
|
+
* @param {string} absPath - absolute path to the file.
|
|
58
|
+
* @param {string} fileLabel - value for the returned entry's `file` field (e.g. "README.md").
|
|
59
|
+
* @param {string} category - value for the returned entry's `category` field.
|
|
60
|
+
* @returns {{ file: string, data: Object, content: string, category: string, date: string|null }|null}
|
|
61
|
+
*/
|
|
62
|
+
function readSingleMarkdownFile(absPath, fileLabel, category) {
|
|
63
|
+
if (!fs.existsSync(absPath)) return null;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const raw = fs.readFileSync(absPath, 'utf8');
|
|
67
|
+
const parsed = matter(raw);
|
|
68
|
+
return {
|
|
69
|
+
file: fileLabel,
|
|
70
|
+
data: parsed.data,
|
|
71
|
+
content: parsed.content.trim(),
|
|
72
|
+
category,
|
|
73
|
+
date: deriveDate(parsed.data),
|
|
74
|
+
};
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.warn(`[documentation] Skipping malformed file: ${absPath} — ${err.message}`);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Aggregate all documentation sources into one flat list of full-content, categorized entries.
|
|
83
|
+
* Returns [] entries for any source that does not exist rather than throwing.
|
|
84
|
+
* @returns {Promise<Object[]>}
|
|
85
|
+
*/
|
|
86
|
+
async function readDocumentation() {
|
|
87
|
+
const root = resolveProjectRoot();
|
|
88
|
+
const results = [];
|
|
89
|
+
|
|
90
|
+
const singleFileSources = [
|
|
91
|
+
{
|
|
92
|
+
absPath: path.join(root, 'project', 'PROJECT_SUMMARY.md'),
|
|
93
|
+
fileLabel: 'PROJECT_SUMMARY.md',
|
|
94
|
+
category: 'summary',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
absPath: path.join(root, 'README.md'),
|
|
98
|
+
fileLabel: 'README.md',
|
|
99
|
+
category: 'readme',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
absPath: path.join(root, 'docs', 'STRATEGY.md'),
|
|
103
|
+
fileLabel: 'STRATEGY.md',
|
|
104
|
+
category: 'strategy',
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
for (const source of singleFileSources) {
|
|
109
|
+
const entry = readSingleMarkdownFile(source.absPath, source.fileLabel, source.category);
|
|
110
|
+
if (entry) results.push(entry);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const examplesRoot = path.join(root, 'project', 'documentation', 'examples');
|
|
114
|
+
// Fixed category for every file under this root, regardless of subdirectory — passed as a
|
|
115
|
+
// function per readMarkdownDirRecursive()'s categorize contract (a bare string is not one of
|
|
116
|
+
// its two supported forms and would silently fall back to 'uncategorized').
|
|
117
|
+
const exampleEntries = readMarkdownDirRecursive(examplesRoot, () => 'example');
|
|
118
|
+
for (const entry of exampleEntries) {
|
|
119
|
+
results.push({ ...entry, date: deriveDate(entry.data) });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return results;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = { readDocumentation };
|