@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.
Files changed (92) hide show
  1. package/README.md +52 -12
  2. package/agents/developer.md +31 -16
  3. package/agents/scrum-master.md +18 -17
  4. package/agents/tester.md +25 -15
  5. package/bin/jenga.js +10 -0
  6. package/lib/commands/dashboard.js +92 -0
  7. package/lib/skill-allow-list.json +7 -2
  8. package/package.json +21 -2
  9. package/project/app/api/lib/resolve-project-root.js +120 -0
  10. package/project/app/api/package.json +16 -0
  11. package/project/app/api/parsers/architecture.js +72 -0
  12. package/project/app/api/parsers/board.js +141 -0
  13. package/project/app/api/parsers/documentation.js +125 -0
  14. package/project/app/api/parsers/git-log.js +52 -0
  15. package/project/app/api/parsers/ideas.js +62 -0
  16. package/project/app/api/parsers/knowledge-graph.js +73 -0
  17. package/project/app/api/parsers/lib/markdown-dir-reader.js +163 -0
  18. package/project/app/api/parsers/rapports.js +148 -0
  19. package/project/app/api/parsers/todo.js +179 -0
  20. package/project/app/api/response.js +47 -0
  21. package/project/app/api/routes/architecture.js +23 -0
  22. package/project/app/api/routes/board.js +46 -0
  23. package/project/app/api/routes/documentation.js +24 -0
  24. package/project/app/api/routes/health.js +25 -0
  25. package/project/app/api/routes/history.js +55 -0
  26. package/project/app/api/routes/rapports.js +24 -0
  27. package/project/app/api/scripts/capture-snapshot.js +294 -0
  28. package/project/app/api/server.js +112 -0
  29. package/project/app/api/types.js +40 -0
  30. package/project/app/package.json +21 -0
  31. package/project/app/ui/dist/assets/index-7fj-vllY.js +104 -0
  32. package/project/app/ui/dist/assets/index-CdK3Qrep.css +1 -0
  33. package/project/app/ui/dist/index.html +13 -0
  34. package/project/app/ui/package.json +23 -0
  35. package/project/app/ui/scripts/build-snapshot-html.cjs +214 -0
  36. package/project/app/ui/scripts/dashboard-open.cjs +88 -0
  37. package/project/app/ui/scripts/dashboard-start.cjs +87 -0
  38. package/scripts/acquire-concurrency-slot.sh +220 -0
  39. package/scripts/audit-twin-divergence.sh +625 -0
  40. package/scripts/check-public-playbook-steps.sh +136 -0
  41. package/scripts/compute-deploy-reconcile.sh +439 -0
  42. package/scripts/jenga-permission-level-switch.sh +19 -3
  43. package/scripts/mark-deployed.sh +532 -0
  44. package/scripts/populate-knowledge-graph.js +429 -0
  45. package/scripts/release-concurrency-slot.sh +129 -0
  46. package/scripts/validate-board.sh +60 -2
  47. package/scripts/verify-consumer-install.sh +470 -0
  48. package/skills/j-close-story/SKILL.md +1 -1
  49. package/skills/j-cloud-connect/SKILL.md +95 -0
  50. package/skills/j-cloud-connect/scripts/configure-backend.sh +267 -0
  51. package/skills/j-cloud-connect/scripts/install-rclone.sh +153 -0
  52. package/skills/j-dashboard/SKILL.md +144 -0
  53. package/skills/j-dashboard/scripts/launch.sh +121 -0
  54. package/skills/j-dashboard/scripts/resolve-app-dir.sh +164 -0
  55. package/skills/j-dashboard/scripts/snapshot.sh +267 -0
  56. package/skills/j-dashboard-share/SKILL.md +96 -0
  57. package/skills/j-dashboard-share/scripts/upload-snapshot.sh +173 -0
  58. package/skills/j-do/SKILL.md +19 -19
  59. package/skills/j-doc-sync/SKILL.md +12 -1
  60. package/skills/j-idea/SKILL.md +1 -1
  61. package/skills/j-init/SKILL.md +5 -4
  62. package/skills/j-init/assets/directory_structure.txt +1 -0
  63. package/skills/j-init/scripts/detect-existing-codebase.sh +2 -2
  64. package/skills/j-init/scripts/init.sh +13 -2
  65. package/skills/j-playbook/SKILL.md +93 -0
  66. package/skills/j-playbook-new/SKILL.md +155 -0
  67. package/skills/j-playbook-new/scripts/playbook-new.sh +332 -0
  68. package/skills/j-proceed/SKILL.md +1 -1
  69. package/skills/j-publish/SKILL.md +1 -1
  70. package/skills/j-publish/adapters/npm-ci.md +29 -0
  71. package/skills/j-publish/scripts/npm_ci_pipeline.sh +9 -0
  72. package/skills/j-publish/scripts/npm_pipeline.sh +18 -0
  73. package/skills/j-publish/scripts/npm_stage_pipeline.sh +81 -41
  74. package/skills/j-reconcile/SKILL.md +1 -0
  75. package/skills/j-redo/SKILL.md +1 -1
  76. package/skills/j-status/SKILL.md +12 -0
  77. package/skills/j-todo/SKILL.md +2 -2
  78. package/skills/j-uncharted/SKILL.md +8 -7
  79. package/skills/j-uncharted/scripts/validate-proposed-items.sh +18 -2
  80. package/skills/jenga/SKILL.md +55 -16
  81. package/skills/jenga/playbooks/idea-to-committed.json +20 -0
  82. package/skills/jenga/playbooks/schema.json +1 -1
  83. package/skills/jenga/scripts/load-nl-catalog.js +22 -6
  84. package/skills/jenga/scripts/load-playbooks.sh +968 -41
  85. package/skills/jenga/scripts/match-playbook.sh +1 -1
  86. package/skills/jenga/scripts/render-playbook-confirmation.sh +162 -8
  87. package/skills/jenga/scripts/run-playbook-step.sh +535 -42
  88. package/skills/jenga-permission-level/SKILL.md +4 -4
  89. package/templates/KNOWLEDGE_GRAPH_STUB_SCHEMA_TEMPLATE.md +128 -0
  90. package/templates/SCRUM_BOARD_SCHEMA.md +18 -6
  91. package/templates/playbook-types.json +8 -0
  92. 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-08T21:53:54.204Z",
3
- "skill_count": 36,
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.1.1",
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 };