@jenga-ai/agent 3.2.0 → 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 (60) hide show
  1. package/README.md +52 -12
  2. package/agents/developer.md +16 -1
  3. package/agents/scrum-master.md +1 -0
  4. package/bin/jenga.js +10 -0
  5. package/lib/commands/dashboard.js +92 -0
  6. package/lib/skill-allow-list.json +6 -2
  7. package/package.json +21 -2
  8. package/project/app/api/lib/resolve-project-root.js +120 -0
  9. package/project/app/api/package.json +16 -0
  10. package/project/app/api/parsers/architecture.js +72 -0
  11. package/project/app/api/parsers/board.js +141 -0
  12. package/project/app/api/parsers/documentation.js +125 -0
  13. package/project/app/api/parsers/git-log.js +52 -0
  14. package/project/app/api/parsers/ideas.js +62 -0
  15. package/project/app/api/parsers/knowledge-graph.js +73 -0
  16. package/project/app/api/parsers/lib/markdown-dir-reader.js +163 -0
  17. package/project/app/api/parsers/rapports.js +148 -0
  18. package/project/app/api/parsers/todo.js +179 -0
  19. package/project/app/api/response.js +47 -0
  20. package/project/app/api/routes/architecture.js +23 -0
  21. package/project/app/api/routes/board.js +46 -0
  22. package/project/app/api/routes/documentation.js +24 -0
  23. package/project/app/api/routes/health.js +25 -0
  24. package/project/app/api/routes/history.js +55 -0
  25. package/project/app/api/routes/rapports.js +24 -0
  26. package/project/app/api/scripts/capture-snapshot.js +294 -0
  27. package/project/app/api/server.js +112 -0
  28. package/project/app/api/types.js +40 -0
  29. package/project/app/package.json +21 -0
  30. package/project/app/ui/dist/assets/index-7fj-vllY.js +104 -0
  31. package/project/app/ui/dist/assets/index-CdK3Qrep.css +1 -0
  32. package/project/app/ui/dist/index.html +13 -0
  33. package/project/app/ui/package.json +23 -0
  34. package/project/app/ui/scripts/build-snapshot-html.cjs +214 -0
  35. package/project/app/ui/scripts/dashboard-open.cjs +88 -0
  36. package/project/app/ui/scripts/dashboard-start.cjs +87 -0
  37. package/scripts/acquire-concurrency-slot.sh +220 -0
  38. package/scripts/compute-deploy-reconcile.sh +439 -0
  39. package/scripts/jenga-permission-level-switch.sh +19 -3
  40. package/scripts/mark-deployed.sh +532 -0
  41. package/scripts/populate-knowledge-graph.js +429 -0
  42. package/scripts/release-concurrency-slot.sh +129 -0
  43. package/scripts/validate-board.sh +60 -2
  44. package/scripts/verify-consumer-install.sh +470 -0
  45. package/skills/j-cloud-connect/SKILL.md +95 -0
  46. package/skills/j-cloud-connect/scripts/configure-backend.sh +267 -0
  47. package/skills/j-cloud-connect/scripts/install-rclone.sh +153 -0
  48. package/skills/j-dashboard/SKILL.md +144 -0
  49. package/skills/j-dashboard/scripts/launch.sh +121 -0
  50. package/skills/j-dashboard/scripts/resolve-app-dir.sh +164 -0
  51. package/skills/j-dashboard/scripts/snapshot.sh +267 -0
  52. package/skills/j-dashboard-share/SKILL.md +96 -0
  53. package/skills/j-dashboard-share/scripts/upload-snapshot.sh +173 -0
  54. package/skills/j-playbook/SKILL.md +12 -0
  55. package/skills/j-playbook-new/SKILL.md +155 -0
  56. package/skills/j-playbook-new/scripts/playbook-new.sh +332 -0
  57. package/skills/j-publish/scripts/npm_ci_pipeline.sh +6 -0
  58. package/skills/jenga/scripts/load-nl-catalog.js +22 -6
  59. package/skills/jenga/scripts/load-playbooks.sh +123 -24
  60. package/templates/SCRUM_BOARD_SCHEMA.md +18 -6
@@ -0,0 +1,148 @@
1
+ /**
2
+ * @file project/app/api/parsers/rapports.js
3
+ * Lists rapport markdown files and extracts summary metadata.
4
+ *
5
+ * Also exports `readRapportsFull()` (E58_S01_T03) — a full-content, 6-category aggregate built
6
+ * on `./lib/markdown-dir-reader`'s `readMarkdownDirRecursive()` (E58_S01_T01) and `./ideas`'s
7
+ * `readIdeas()` (E58_S01_T02), covering `project/rapports/{analysis,problems,tests}`,
8
+ * `project/documentation/{summaries,plans}`, and `project/ideas.md`. This coexists with, and does
9
+ * not alter, the original `readRapports()` below — that function and its truncated
10
+ * `content_summary` shape are `GET /v1/history`'s exclusive, unchanged dependency.
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const matter = require('gray-matter');
16
+ const { resolveProjectRoot } = require('../lib/resolve-project-root');
17
+ const { readMarkdownDirRecursive } = require('./lib/markdown-dir-reader');
18
+ const { readIdeas } = require('./ideas');
19
+
20
+ // Resolved relative to the invoking project's own root (E47_S02_T01/T02), not a fixed __dirname
21
+ // climb — see project/app/api/lib/resolve-project-root.js.
22
+ const RAPPORTS_ROOT = path.join(resolveProjectRoot(), 'project', 'rapports');
23
+ const DOCUMENTATION_SUMMARIES_ROOT = path.join(
24
+ resolveProjectRoot(),
25
+ 'project',
26
+ 'documentation',
27
+ 'summaries'
28
+ );
29
+ const DOCUMENTATION_PLANS_ROOT = path.join(
30
+ resolveProjectRoot(),
31
+ 'project',
32
+ 'documentation',
33
+ 'plans'
34
+ );
35
+ const DATE_PATTERN = /(\d{4}-\d{2}-\d{2})/;
36
+
37
+ // Maps project/rapports/*'s top-level subdirectories onto the 6-category taxonomy named in
38
+ // E58_S01's story description ("Rapports: analysis/problems/tests/summaries/plans/ideas").
39
+ const RAPPORTS_CATEGORIZE = { analysis: 'analysis', problems: 'problems', tests: 'tests' };
40
+
41
+ /**
42
+ * Recursively list .md files under a directory.
43
+ * @param {string} dir
44
+ * @returns {string[]} absolute file paths
45
+ */
46
+ function listMdFiles(dir) {
47
+ if (!fs.existsSync(dir)) return [];
48
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
49
+ const files = [];
50
+ for (const entry of entries) {
51
+ const full = path.join(dir, entry.name);
52
+ if (entry.isDirectory()) {
53
+ files.push(...listMdFiles(full));
54
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
55
+ files.push(full);
56
+ }
57
+ }
58
+ return files;
59
+ }
60
+
61
+ /**
62
+ * Read rapport files and return summary objects.
63
+ * Returns [] if the rapports directory does not exist.
64
+ * @returns {Promise<Object[]>}
65
+ */
66
+ async function readRapports() {
67
+ if (!fs.existsSync(RAPPORTS_ROOT)) return [];
68
+
69
+ const files = listMdFiles(RAPPORTS_ROOT);
70
+ const results = [];
71
+
72
+ for (const filePath of files) {
73
+ try {
74
+ const raw = fs.readFileSync(filePath, 'utf8');
75
+ const { data: frontmatter, content } = matter(raw);
76
+ const filename = path.relative(RAPPORTS_ROOT, filePath);
77
+
78
+ // Date: prefer frontmatter, then extract from filename
79
+ let date = frontmatter.date || null;
80
+ if (!date) {
81
+ const match = DATE_PATTERN.exec(path.basename(filePath));
82
+ if (match) date = match[1];
83
+ }
84
+
85
+ const content_summary = content.trim().slice(0, 300);
86
+
87
+ results.push({
88
+ type: 'rapport',
89
+ filename,
90
+ date: date ? String(date) : null,
91
+ content_summary,
92
+ });
93
+ } catch (err) {
94
+ console.warn(`[rapports] Skipping malformed file: ${filePath} — ${err.message}`);
95
+ }
96
+ }
97
+
98
+ return results;
99
+ }
100
+
101
+ /**
102
+ * Resolve a `date` field for a `readMarkdownDirRecursive()` entry, mirroring the exact
103
+ * frontmatter-then-filename fallback rule `readRapports()` already uses above — factored out here
104
+ * so `readRapportsFull()` can apply the identical rule across all of its directory-reader sources
105
+ * without duplicating it per call site. Does not alter `readRapports()` itself.
106
+ * @param {Object} data - gray-matter-parsed frontmatter object.
107
+ * @param {string} relFile - path relative to the source root (used for filename-based fallback).
108
+ * @returns {string|null}
109
+ */
110
+ function extractDate(data, relFile) {
111
+ let date = (data && data.date) || null;
112
+ if (!date) {
113
+ const match = DATE_PATTERN.exec(path.basename(relFile));
114
+ if (match) date = match[1];
115
+ }
116
+ return date ? String(date) : null;
117
+ }
118
+
119
+ /**
120
+ * Aggregate full (untruncated) content across the 6-category taxonomy named in E58_S01's story
121
+ * description: `analysis`/`problems`/`tests` (from `project/rapports/*`), `summaries`/`plans`
122
+ * (from `project/documentation/*`), and `idea` (from `project/ideas.md`).
123
+ *
124
+ * Each entry carries: `{ file, data, content, category, date }` — `file` is a path relative to
125
+ * its own source root (POSIX-style, per `readMarkdownDirRecursive()`'s contract), `data` is the
126
+ * parsed frontmatter object (`{}` if none), `content` is the full untruncated markdown body, and
127
+ * `date` follows the same frontmatter-then-filename fallback rule as `readRapports()`. A missing
128
+ * source directory/file contributes zero entries rather than throwing (inherited from
129
+ * `readMarkdownDirRecursive()`'s and `readIdeas()`'s own non-throwing guards).
130
+ *
131
+ * Does not alter `readRapports()` or its truncated `content_summary` shape — `GET /v1/history`
132
+ * is unaffected by this function's existence.
133
+ * @returns {Promise<Object[]>}
134
+ */
135
+ async function readRapportsFull() {
136
+ const withDates = (entries) => entries.map((e) => ({ ...e, date: extractDate(e.data, e.file) }));
137
+
138
+ const rapportEntries = withDates(readMarkdownDirRecursive(RAPPORTS_ROOT, RAPPORTS_CATEGORIZE));
139
+ const summaryEntries = withDates(
140
+ readMarkdownDirRecursive(DOCUMENTATION_SUMMARIES_ROOT, () => 'summaries')
141
+ );
142
+ const planEntries = withDates(readMarkdownDirRecursive(DOCUMENTATION_PLANS_ROOT, () => 'plans'));
143
+ const ideaEntries = await readIdeas();
144
+
145
+ return [...rapportEntries, ...summaryEntries, ...planEntries, ...ideaEntries];
146
+ }
147
+
148
+ module.exports = { readRapports, readRapportsFull };
@@ -0,0 +1,179 @@
1
+ /**
2
+ * @file project/app/api/parsers/todo.js
3
+ *
4
+ * Reads the invoking project's `project/todo.md` and extracts the board refs its **active**
5
+ * (non-comment) entries name, so the dashboard can tell which board items the user has already
6
+ * queued for execution. Consumed by `parsers/board.js`, which tags each matching epic/story/task
7
+ * with `_queued: true` (E06_S05_T04) — the Active Sprint tab's In Progress column promotes those
8
+ * items even though their board status hasn't moved yet.
9
+ *
10
+ * ── Path resolution ───────────────────────────────────────────────────────────────────────────
11
+ * `todo.md` is resolved via `resolveProjectRoot()` (`../lib/resolve-project-root.js`), exactly the
12
+ * way `parsers/board.js` resolves `BOARD_ROOT` — never a fixed `path.resolve(__dirname, '../../..')`
13
+ * climb. A `__dirname` climb only ever lands correctly inside this monorepo's own checkout; from a
14
+ * consumer's `node_modules/@jenga-ai/agent/` it lands inside or above `node_modules` and silently
15
+ * reads the wrong file (or none). That is the exact E46 / E47_S02 defect pattern.
16
+ *
17
+ * Unlike `board.js`, the path is resolved **inside** `readTodoRefs()` rather than at module load.
18
+ * Two reasons: requiring this module can then never throw at import time (a board read must not be
19
+ * taken down by an unresolvable root before the route handler can even produce an error envelope),
20
+ * and tests can pass an explicit path without touching `JENGA_PROJECT_ROOT`. Since `server.js`
21
+ * pins the resolved root into that env var before any parser loads, the per-call resolution is a
22
+ * cheap env-var read, not a repeated filesystem walk.
23
+ *
24
+ * ── Line shapes in the real file ──────────────────────────────────────────────────────────────
25
+ * `project/todo.md`'s documented format is `<mission title>: <E##_S##>` with the ref **optional**
26
+ * (its own header comment says so). The live file contains all of these, and all of them must be
27
+ * handled:
28
+ *
29
+ * 1. `Implement set-break.sh and clear-break.sh: E44_S01_T02` → task ref, extracted
30
+ * 2. `Handoff document schema & ... checkpoint: E23_S01` → story ref, extracted
31
+ * 3. `invoke /brainstorm project/rapports/analysis/...md` → no ref, ignored
32
+ * 4. `<!-- RECONCILED: ...: E32_S01_T01 -->` → comment, skipped entirely
33
+ * 5. `# Todo` / `<!-- Format: ... -->` / blank lines → ignored
34
+ * 6. `Fix hardcoded project/ paths ... (see project/rapports/problems/E31_S05_T01-hidden-mode-path-resolution-gaps.md, F1/F2): E34`
35
+ *
36
+ * Shape 6 is why the ref pattern is **end-anchored and colon-preceded** rather than "any ref
37
+ * anywhere in the line": that single real line both embeds `E31_S05_T01` inside a rapport filename
38
+ * and ends in the epic-only ref `E34`. A loose `/E\d+_S\d+(_T\d+)?/g` scan would spuriously report
39
+ * `E31_S05_T01` as queued — a board item that line is merely *citing*, not queueing.
40
+ *
41
+ * ── Epic-only refs are deliberately not matched ───────────────────────────────────────────────
42
+ * A trailing `E##` (e.g. `E34`, `E15_S04` is a story so it matches, but bare `E34` does not) is
43
+ * ignored. E06_S05_T04's Acceptance Criteria scope resolvable refs to `E##_S##` / `E##_S##_T##`,
44
+ * and its Description states a ref "may point at a story or at a task". Promoting a whole epic off
45
+ * a single queued line would also drag an item with a much wider blast radius into the In Progress
46
+ * column than the user queued. This is a decision, not an oversight.
47
+ */
48
+
49
+ 'use strict';
50
+
51
+ const fs = require('fs');
52
+ const path = require('path');
53
+ const { resolveProjectRoot } = require('../lib/resolve-project-root');
54
+
55
+ /**
56
+ * Matches a whole line of the documented `<mission title>: <ref>` form, where `<ref>` is a story or
57
+ * task id and is the last non-whitespace token on the line. `.*?` for the title is lazy but the
58
+ * `$` anchor forces the ref to be the final token regardless. Case-insensitive so a hand-typed
59
+ * `e06_s05_t04` still resolves; refs are upper-cased on the way out and `board.js` matches ids
60
+ * case-insensitively too, so the whole path is case-agnostic.
61
+ */
62
+ const TODO_ENTRY_PATTERN = /^(.*?):[ \t]*(E\d+_S\d+(?:_T\d+)?)[ \t]*$/i;
63
+
64
+ /** A balanced HTML comment block, `s`-flag-free so it works on older Node too. */
65
+ const HTML_COMMENT_BLOCK = /<!--[\s\S]*?-->/g;
66
+
67
+ /**
68
+ * Removes every balanced `<!-- ... -->` block, then drops everything from an unterminated trailing
69
+ * `<!--` onward (matching how a browser/markdown renderer treats it: the rest of the document is
70
+ * commented out). Blocks are replaced with a single space rather than deleted so a comment that
71
+ * opens mid-line can't silently glue the surrounding text into a fake `title: ref` pair.
72
+ *
73
+ * The replacement is **newline-preserving**: a block spanning N newlines is replaced with a space
74
+ * followed by those N newlines, so every surviving line keeps its original 1-based line number and
75
+ * any text trailing a multi-line comment's `-->` stays on the line it was actually written on.
76
+ * Collapsing the block to a bare space instead (the original behavior) shifted every subsequent
77
+ * line up by N, which made `parseTodoContent`'s `entries[].line` wrong for any file containing a
78
+ * multi-line comment — and `project/todo.md` contains several.
79
+ *
80
+ * The dangling-open truncation needs no equivalent treatment: it only ever discards lines *after*
81
+ * the unterminated `<!--`, so the line numbers of everything that survives are untouched.
82
+ *
83
+ * @param {string} content
84
+ * @returns {string}
85
+ */
86
+ function stripComments(content) {
87
+ const withoutBalanced = String(content == null ? '' : content).replace(
88
+ HTML_COMMENT_BLOCK,
89
+ (block) => ' ' + '\n'.repeat((block.match(/\n/g) || []).length)
90
+ );
91
+ const danglingOpen = withoutBalanced.indexOf('<!--');
92
+ return danglingOpen === -1 ? withoutBalanced : withoutBalanced.slice(0, danglingOpen);
93
+ }
94
+
95
+ /**
96
+ * Pure parse of `todo.md` content into the board refs its active entries name.
97
+ *
98
+ * @param {string} content raw file content
99
+ * @returns {{ refs: string[], entries: { title: string, ref: string, line: number }[] }}
100
+ * `refs` is upper-cased and de-duplicated, in first-appearance order. `entries` keeps every
101
+ * matching line for debuggability; its 1-based `line` numbers always match the original content,
102
+ * since `stripComments` preserves the newlines of the blocks it removes.
103
+ */
104
+ function parseTodoContent(content) {
105
+ const lines = stripComments(content).split(/\r?\n/);
106
+ const entries = [];
107
+ const seen = new Set();
108
+ const refs = [];
109
+
110
+ lines.forEach((rawLine, index) => {
111
+ const line = rawLine.trim();
112
+ if (!line || line.startsWith('#')) return;
113
+
114
+ const match = TODO_ENTRY_PATTERN.exec(line);
115
+ if (!match) return;
116
+
117
+ const title = match[1].trim();
118
+ const ref = match[2].toUpperCase();
119
+
120
+ entries.push({ title, ref, line: index + 1 });
121
+ if (!seen.has(ref)) {
122
+ seen.add(ref);
123
+ refs.push(ref);
124
+ }
125
+ });
126
+
127
+ return { refs, entries };
128
+ }
129
+
130
+ /**
131
+ * Absolute path to the invoking project's `project/todo.md`.
132
+ * @returns {string}
133
+ */
134
+ function resolveTodoPath() {
135
+ return path.join(resolveProjectRoot(), 'project', 'todo.md');
136
+ }
137
+
138
+ /**
139
+ * Read and parse the project's `todo.md`.
140
+ *
141
+ * A missing file is a first-class, non-exceptional case: it returns `{ exists: false, refs: [] }`,
142
+ * which makes every downstream consumer behave exactly as it did before this feature existed. An
143
+ * unreadable file (permissions, a directory where a file was expected) is likewise reported as
144
+ * `exists: false` with the reason attached, never thrown — a broken `todo.md` must not be able to
145
+ * take down `GET /v1/board`.
146
+ *
147
+ * @param {string} [filePath] explicit path; defaults to `<projectRoot>/project/todo.md`
148
+ * @returns {{ exists: boolean, path: string, refs: string[], entries: Object[], error?: string }}
149
+ */
150
+ function readTodoRefs(filePath) {
151
+ let todoPath;
152
+ try {
153
+ todoPath = filePath || resolveTodoPath();
154
+ } catch (err) {
155
+ return { exists: false, path: '', refs: [], entries: [], error: err.message };
156
+ }
157
+
158
+ let raw;
159
+ try {
160
+ raw = fs.readFileSync(todoPath, 'utf8');
161
+ } catch (err) {
162
+ if (err.code !== 'ENOENT') {
163
+ console.warn(`[todo] Could not read ${todoPath} — ${err.message}`);
164
+ return { exists: false, path: todoPath, refs: [], entries: [], error: err.message };
165
+ }
166
+ return { exists: false, path: todoPath, refs: [], entries: [] };
167
+ }
168
+
169
+ const { refs, entries } = parseTodoContent(raw);
170
+ return { exists: true, path: todoPath, refs, entries };
171
+ }
172
+
173
+ module.exports = {
174
+ readTodoRefs,
175
+ parseTodoContent,
176
+ stripComments,
177
+ resolveTodoPath,
178
+ TODO_ENTRY_PATTERN,
179
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @file api/response.js
3
+ * Helper functions for building consistent API response envelopes.
4
+ */
5
+
6
+ const API_VERSION = '1.0.0';
7
+
8
+ /**
9
+ * Build a success envelope.
10
+ * @template T
11
+ * @param {T} data
12
+ * @param {Object} [extraMeta] - Additional meta fields to merge
13
+ * @returns {import('./types').ApiEnvelope<T>}
14
+ */
15
+ function successResponse(data, extraMeta = {}) {
16
+ return {
17
+ data,
18
+ meta: {
19
+ timestamp: new Date().toISOString(),
20
+ version: API_VERSION,
21
+ ...extraMeta,
22
+ },
23
+ error: null,
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Build an error envelope.
29
+ * @param {string} code - Error code from ERROR_CODES
30
+ * @param {string} message - Human-readable message
31
+ * @param {Object} [details]
32
+ * @returns {import('./types').ApiEnvelope<null>}
33
+ */
34
+ function errorResponse(code, message, details) {
35
+ const err = { code, message };
36
+ if (details !== undefined) err.details = details;
37
+ return {
38
+ data: null,
39
+ meta: {
40
+ timestamp: new Date().toISOString(),
41
+ version: API_VERSION,
42
+ },
43
+ error: err,
44
+ };
45
+ }
46
+
47
+ module.exports = { successResponse, errorResponse, API_VERSION };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @file project/app/api/routes/architecture.js
3
+ * GET /architecture — tech stack and dependency metadata
4
+ */
5
+
6
+ const { Router } = require('express');
7
+ const { parseArchitecture } = require('../parsers/architecture');
8
+ const { successResponse, errorResponse } = require('../response');
9
+ const { ERROR_CODES } = require('../types');
10
+
11
+ const router = Router();
12
+
13
+ router.get('/', async (req, res) => {
14
+ try {
15
+ const arch = await parseArchitecture();
16
+ res.json(successResponse(arch));
17
+ } catch (err) {
18
+ console.error('[architecture] error:', err.message);
19
+ res.status(500).json(errorResponse(ERROR_CODES.INTERNAL_ERROR, err.message));
20
+ }
21
+ });
22
+
23
+ module.exports = router;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @file project/app/api/routes/board.js
3
+ * GET /board — full nested board
4
+ * GET /board/:epicId — single epic (case-insensitive)
5
+ */
6
+
7
+ const { Router } = require('express');
8
+ const { parseBoard } = require('../parsers/board');
9
+ const { successResponse, errorResponse } = require('../response');
10
+ const { ERROR_CODES } = require('../types');
11
+
12
+ const router = Router();
13
+
14
+ router.get('/', async (req, res) => {
15
+ try {
16
+ const board = await parseBoard();
17
+ res.json(successResponse(board));
18
+ } catch (err) {
19
+ console.error('[board] parse error:', err.message);
20
+ res.status(500).json(errorResponse(ERROR_CODES.PARSE_ERROR, err.message));
21
+ }
22
+ });
23
+
24
+ router.get('/:epicId', async (req, res) => {
25
+ try {
26
+ const board = await parseBoard();
27
+ const id = req.params.epicId.toLowerCase();
28
+ const epic = board.find((e) => e.id && e.id.toLowerCase() === id);
29
+ if (!epic) {
30
+ return res
31
+ .status(404)
32
+ .json(
33
+ errorResponse(
34
+ ERROR_CODES.EPIC_NOT_FOUND,
35
+ `Epic '${req.params.epicId}' not found`
36
+ )
37
+ );
38
+ }
39
+ res.json(successResponse(epic));
40
+ } catch (err) {
41
+ console.error('[board/:epicId] parse error:', err.message);
42
+ res.status(500).json(errorResponse(ERROR_CODES.PARSE_ERROR, err.message));
43
+ }
44
+ });
45
+
46
+ module.exports = router;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @file project/app/api/routes/documentation.js
3
+ * GET / — full documentation aggregate (summary/readme/strategy/example), untruncated, no query
4
+ * params or pagination — matches GET /v1/board's bulk-fetch precedent.
5
+ */
6
+
7
+ const { Router } = require('express');
8
+ const { readDocumentation } = require('../parsers/documentation');
9
+ const { successResponse, errorResponse } = require('../response');
10
+ const { ERROR_CODES } = require('../types');
11
+
12
+ const router = Router();
13
+
14
+ router.get('/', async (req, res) => {
15
+ try {
16
+ const documentation = await readDocumentation();
17
+ res.json(successResponse(documentation));
18
+ } catch (err) {
19
+ console.error('[documentation] parse error:', err.message);
20
+ res.status(500).json(errorResponse(ERROR_CODES.PARSE_ERROR, err.message));
21
+ }
22
+ });
23
+
24
+ module.exports = router;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @file project/app/api/routes/health.js
3
+ * GET /health — server liveness check
4
+ */
5
+
6
+ const { Router } = require('express');
7
+ const { successResponse } = require('../response');
8
+ const { API_VERSION } = require('../response');
9
+
10
+ const router = Router();
11
+
12
+ const startTime = Date.now();
13
+
14
+ router.get('/', (req, res) => {
15
+ const uptime = (Date.now() - startTime) / 1000;
16
+ res.json(
17
+ successResponse({
18
+ status: 'ok',
19
+ version: API_VERSION,
20
+ uptime,
21
+ })
22
+ );
23
+ });
24
+
25
+ module.exports = router;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @file project/app/api/routes/history.js
3
+ * GET /history — merged git commits + rapport files, sorted by date desc
4
+ * Query params:
5
+ * ?limit=N — return first N items
6
+ * ?type=git_commit|rapport — filter by type
7
+ */
8
+
9
+ const { Router } = require('express');
10
+ const { readGitLog } = require('../parsers/git-log');
11
+ const { readRapports } = require('../parsers/rapports');
12
+ const { successResponse, errorResponse } = require('../response');
13
+ const { ERROR_CODES } = require('../types');
14
+
15
+ const router = Router();
16
+
17
+ router.get('/', async (req, res) => {
18
+ const { limit, type } = req.query;
19
+
20
+ if (limit !== undefined && (isNaN(Number(limit)) || Number(limit) < 1)) {
21
+ return res
22
+ .status(400)
23
+ .json(
24
+ errorResponse(ERROR_CODES.INVALID_QUERY_PARAM, '`limit` must be a positive integer')
25
+ );
26
+ }
27
+ if (type !== undefined && !['git_commit', 'rapport'].includes(type)) {
28
+ return res
29
+ .status(400)
30
+ .json(
31
+ errorResponse(ERROR_CODES.INVALID_QUERY_PARAM, '`type` must be git_commit or rapport')
32
+ );
33
+ }
34
+
35
+ try {
36
+ const [commits, rapports] = await Promise.all([readGitLog(), readRapports()]);
37
+ let items = [...commits, ...rapports];
38
+
39
+ items.sort((a, b) => {
40
+ const da = new Date(a.date || 0).getTime();
41
+ const db = new Date(b.date || 0).getTime();
42
+ return db - da;
43
+ });
44
+
45
+ if (type) items = items.filter((i) => i.type === type);
46
+ if (limit) items = items.slice(0, Number(limit));
47
+
48
+ res.json(successResponse(items));
49
+ } catch (err) {
50
+ console.error('[history] error:', err.message);
51
+ res.status(500).json(errorResponse(ERROR_CODES.INTERNAL_ERROR, err.message));
52
+ }
53
+ });
54
+
55
+ module.exports = router;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @file project/app/api/routes/rapports.js
3
+ * GET / — full rapports aggregate (analysis/problems/tests/summaries/plans/idea), untruncated,
4
+ * no query params or pagination — matches GET /v1/board's bulk-fetch precedent.
5
+ */
6
+
7
+ const { Router } = require('express');
8
+ const { readRapportsFull } = require('../parsers/rapports');
9
+ const { successResponse, errorResponse } = require('../response');
10
+ const { ERROR_CODES } = require('../types');
11
+
12
+ const router = Router();
13
+
14
+ router.get('/', async (req, res) => {
15
+ try {
16
+ const rapports = await readRapportsFull();
17
+ res.json(successResponse(rapports));
18
+ } catch (err) {
19
+ console.error('[rapports] parse error:', err.message);
20
+ res.status(500).json(errorResponse(ERROR_CODES.PARSE_ERROR, err.message));
21
+ }
22
+ });
23
+
24
+ module.exports = router;