@jenga-ai/agent 3.2.0 → 3.5.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 +16 -1
- package/agents/scrum-master.md +1 -0
- package/bin/jenga.js +10 -0
- package/lib/commands/dashboard.js +92 -0
- package/lib/skill-allow-list.json +6 -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/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-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-init/SKILL.md +52 -13
- package/skills/j-init/assets/.gitignore_template +1 -2
- package/skills/j-init/scripts/apply-scaffold-visibility.sh +192 -0
- package/skills/j-init/scripts/init.sh +19 -5
- package/skills/j-playbook/SKILL.md +12 -0
- package/skills/j-playbook-new/SKILL.md +155 -0
- package/skills/j-playbook-new/scripts/playbook-new.sh +332 -0
- package/skills/j-publish/scripts/npm_ci_pipeline.sh +6 -0
- package/skills/j-skillify/assets/init-new/assets/.gitignore_template +1 -2
- package/skills/j-uncharted/SKILL.md +54 -7
- package/skills/j-uncharted/assets/NODE_QUESTION_TEMPLATE.md +69 -0
- package/skills/j-uncharted/scripts/elicitation-state.sh +45 -7
- package/skills/jenga/scripts/load-nl-catalog.js +22 -6
- package/skills/jenga/scripts/load-playbooks.sh +146 -35
- 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;
|