@jenga-ai/agent 3.5.0 → 4.0.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 (136) hide show
  1. package/README.md +85 -78
  2. package/agents/developer.md +1 -1
  3. package/agents/scrum-master.md +20 -2
  4. package/agents/tester.md +3 -3
  5. package/hooks/on_session_end.sh +5 -5
  6. package/lib/generate-agent-context.js +2 -2
  7. package/lib/generate-copilot-hooks.js +1 -1
  8. package/lib/generate-skill-allow-list.js +79 -8
  9. package/lib/mirror.js +1 -1
  10. package/lib/postinstall-manifest.js +1 -1
  11. package/lib/skill-allow-list.json +2 -2
  12. package/mcp/help/index.js +8 -17
  13. package/mcp/help/scan.js +73 -0
  14. package/package.json +5 -1
  15. package/project/app/api/lib/resolve-project-root.js +1 -1
  16. package/project/app/api/parsers/knowledge-graph.js +100 -9
  17. package/project/app/api/routes/health.js +36 -0
  18. package/project/app/api/scripts/capture-snapshot.js +9 -6
  19. package/project/app/package.json +4 -0
  20. package/project/app/ui/dist/assets/index-BADc5mmH.css +1 -0
  21. package/project/app/ui/dist/assets/index-C3oiuli_.js +104 -0
  22. package/project/app/ui/dist/index.html +2 -2
  23. package/project/app/ui/package.json +4 -0
  24. package/project/app/ui/scripts/build-snapshot-html.cjs +63 -2
  25. package/scripts/acquire-concurrency-slot.sh +35 -5
  26. package/scripts/apply-j-prefix.sh +46 -5
  27. package/scripts/build-pages-site.sh +1 -1
  28. package/scripts/check-public-playbook-steps.sh +158 -52
  29. package/scripts/check-publicignore-match.sh +2 -2
  30. package/scripts/compute-deploy-reconcile.sh +5 -5
  31. package/scripts/delete-bare-skill-dirs.sh +329 -0
  32. package/scripts/generate-legacy-shipped-paths.js +2 -2
  33. package/scripts/idea_manager.sh +273 -3
  34. package/scripts/mark-deployed.sh +2 -2
  35. package/scripts/populate-knowledge-graph.entity-resolution.test.js +254 -0
  36. package/scripts/populate-knowledge-graph.js +213 -5
  37. package/scripts/populate-knowledge-graph.staleness.test.js +130 -0
  38. package/scripts/postinstall.js +1 -1
  39. package/scripts/release-concurrency-slot.sh +34 -4
  40. package/scripts/render-ranked-list.sh +270 -0
  41. package/scripts/repoint-dead-bare-path-prose.py +81 -0
  42. package/scripts/repoint-skill-refs.sh +539 -0
  43. package/scripts/rewrite-stale-skill-preambles.py +188 -0
  44. package/scripts/strip-polyfill-frontmatter.py +166 -0
  45. package/scripts/todo_manager.sh +16 -1
  46. package/scripts/validate-typed-object.sh +750 -0
  47. package/scripts/verify-legacy-seed-reconcile.sh +10 -10
  48. package/scripts/verify-postinstall-reconcile.sh +7 -7
  49. package/scripts/write-context-digest.sh +1 -1
  50. package/skills/j-brainstorm/SKILL.md +3 -4
  51. package/skills/j-btw/SKILL.md +3 -4
  52. package/skills/j-clearify/SKILL.md +5 -6
  53. package/skills/j-close-story/SKILL.md +11 -12
  54. package/skills/j-close-story/scripts/check-privatized.sh +4 -4
  55. package/skills/j-close-story/scripts/check-story-closeable.sh +11 -4
  56. package/skills/j-commit/SKILL.md +3 -4
  57. package/skills/j-continue/SKILL.md +5 -6
  58. package/skills/j-deep-dive/SKILL.md +3 -4
  59. package/skills/j-distribute/CONFIG_SCHEMA.md +82 -5
  60. package/skills/j-distribute/SKILL.md +3 -4
  61. package/skills/j-do/SKILL.md +100 -18
  62. package/skills/j-doc/SKILL.md +3 -4
  63. package/skills/j-doc-sync/SKILL.md +4 -4
  64. package/skills/j-dooo/SKILL.md +6 -15
  65. package/skills/j-error/SKILL.md +3 -4
  66. package/skills/j-evaluate/SKILL.md +3 -4
  67. package/skills/j-examplify/SKILL.md +3 -4
  68. package/skills/j-gitignore/SKILL.md +157 -0
  69. package/skills/j-gitignore/assets/jenga-paths.txt +50 -0
  70. package/skills/j-gitignore/scripts/_catalog.sh +105 -0
  71. package/skills/j-gitignore/scripts/audit-gitignore.sh +194 -0
  72. package/skills/j-gitignore/scripts/repair-gitignore.sh +226 -0
  73. package/skills/j-gitignore/scripts/untrack-jenga-files.sh +210 -0
  74. package/skills/j-help/SKILL.md +3 -4
  75. package/skills/j-idea/SKILL.md +80 -9
  76. package/skills/j-idea/assets/idea_template.md +1 -1
  77. package/skills/j-improve/SKILL.md +4 -5
  78. package/skills/j-init/SKILL.md +23 -14
  79. package/skills/j-init/assets/scope-thresholds_template.json +5 -2
  80. package/skills/j-init/scripts/apply-scaffold-visibility.sh +9 -7
  81. package/skills/j-init/scripts/init.sh +4 -4
  82. package/skills/j-jbp/SKILL.md +3 -4
  83. package/skills/j-lgtm/SKILL.md +3 -4
  84. package/skills/j-pi-plan/SKILL.md +3 -4
  85. package/skills/j-playbook/SKILL.md +1 -1
  86. package/skills/j-proceed/SKILL.md +3 -4
  87. package/skills/j-publish/SKILL.md +4 -5
  88. package/skills/j-publish/adapters/npm-ci.md +6 -1
  89. package/skills/j-publish/adapters/npm.md +1 -1
  90. package/skills/j-publish/scripts/generate_release_notes.sh +1 -1
  91. package/skills/j-publish/scripts/npm_stage_inspect.sh +61 -0
  92. package/skills/j-publish/scripts/run_gates.sh +1 -1
  93. package/skills/j-reconcile/SKILL.md +40 -7
  94. package/skills/j-reconcile/assets/report_format.md +11 -0
  95. package/skills/j-reconcile/scripts/detect-unlinked-code.sh +13 -13
  96. package/skills/j-reconcile-origin/SKILL.md +3 -4
  97. package/skills/j-redo/SKILL.md +4 -5
  98. package/skills/j-skillify/SKILL.md +3 -4
  99. package/skills/j-spinoff/SKILL.md +4 -5
  100. package/skills/j-status/SKILL.md +18 -4
  101. package/skills/j-todo/SKILL.md +44 -5
  102. package/skills/j-todo/scripts/argument-is-not-ranked-list.sh +92 -0
  103. package/skills/j-todo/scripts/argument-is-ranked-list.sh +78 -0
  104. package/skills/j-uncharted/SKILL.md +252 -13
  105. package/skills/j-uncharted/scripts/detect-dependencies.sh +80 -22
  106. package/skills/j-uncharted/scripts/detect-tests.sh +1 -1
  107. package/skills/j-uncharted/scripts/diff-since-baseline.sh +600 -0
  108. package/skills/j-uncharted/scripts/elicitation-state.sh +1 -1
  109. package/skills/j-uncharted/scripts/find-scan-baseline.sh +545 -0
  110. package/skills/j-uncharted/scripts/run-engine.sh +36 -2
  111. package/skills/j-uncharted/scripts/validate-proposed-items.sh +1 -1
  112. package/skills/j-uncharted/scripts/write-scan-record.sh +361 -0
  113. package/skills/j-wtf/SKILL.md +4 -5
  114. package/skills/jenga/SKILL.md +106 -11
  115. package/skills/jenga/playbooks/board-hygiene.json +32 -0
  116. package/skills/jenga/playbooks/schema.json +73 -6
  117. package/skills/jenga/playbooks/understand-then-commit.json +19 -0
  118. package/skills/jenga/scripts/load-nl-catalog.js +5 -2
  119. package/skills/jenga/scripts/load-nl-catalog.sh +1 -1
  120. package/skills/jenga/scripts/load-playbooks.sh +290 -5
  121. package/skills/jenga/scripts/match-playbook.sh +4 -4
  122. package/skills/jenga/scripts/run-playbook-step.sh +267 -1
  123. package/templates/SCRUM_BOARD_SCHEMA.md +14 -1
  124. package/templates/SKILL_TEMPLATE.md +12 -0
  125. package/templates/permission-levels/level-1-locked.json +1 -1
  126. package/templates/permission-levels/level-2-guarded.json +1 -1
  127. package/templates/permission-levels/level-3-standard.json +1 -1
  128. package/templates/permission-levels/level-4-elevated.json +2 -2
  129. package/templates/permission-levels/level-5-unrestricted.json +2 -2
  130. package/templates/playbook-types.json +40 -6
  131. package/project/app/ui/dist/assets/index-7fj-vllY.js +0 -104
  132. package/project/app/ui/dist/assets/index-CdK3Qrep.css +0 -1
  133. package/scripts/audit-twin-divergence.sh +0 -625
  134. package/scripts/generate-j-alias.sh +0 -333
  135. package/skills/j-dev-done/SKILL.md +0 -53
  136. package/skills/j-dev-done/scripts/classify-commit-outcome.sh +0 -114
@@ -0,0 +1,73 @@
1
+ /**
2
+ * mcp/help/scan.js — pure directory-scan logic behind the `help` MCP tool (E50_S12_T06).
3
+ *
4
+ * Split out of index.js so it is importable and unit-testable without starting an MCP
5
+ * server. index.js instantiates an `McpServer` and ends with a top-level
6
+ * `await server.connect(transport)` that blocks on stdin — importing that file as a
7
+ * whole would hang any test that tried it, the exact untestability
8
+ * mcp/router/allow-list-guard.js's header describes for mcp/router/index.js, and the
9
+ * same reason mcp/router's Stage 1 decision was extracted into its own module by
10
+ * E50_S07_T02. This file mirrors that precedent: plain Node built-ins only, no MCP SDK
11
+ * import, no side effects at module load time.
12
+ *
13
+ * Confirmed by direct reading (E50_S12_T06): this is a pure `readdirSync`/`statSync`
14
+ * scan. It never parses a SKILL.md's frontmatter, never strips a `j.`/`j:` prefix, and
15
+ * never compares against the allow-list — it returns whatever directory names exist
16
+ * under `.claude/skills`/`.agents/skills` verbatim. A `j-<name>` twin directory and a
17
+ * bare `<name>` directory are both just directory names to this code; neither is
18
+ * treated differently in any way, so nothing here needs to change for a twin-only tree.
19
+ */
20
+
21
+ import { existsSync, readdirSync, statSync } from "fs";
22
+ import { join } from "path";
23
+
24
+ /**
25
+ * Resolves which of the two generated skill mirrors exists under `root`. The
26
+ * jenga-agent postinstall mirrors skills/ into both .claude/ (Claude Code) and
27
+ * .agents/ (Copilot / custom agents); this prefers .claude/, falling back to .agents/.
28
+ *
29
+ * @param {string} root - project root to scan (already resolved to an absolute path)
30
+ * @returns {string|null} the first candidate skills directory that exists, or null
31
+ */
32
+ export function resolveSkillsDir(root) {
33
+ const candidates = [
34
+ join(root, ".claude", "skills"),
35
+ join(root, ".agents", "skills"),
36
+ ];
37
+ return candidates.find(existsSync) ?? null;
38
+ }
39
+
40
+ /**
41
+ * The two candidate mirror paths under `root`, in preference order — exposed
42
+ * separately from resolveSkillsDir() so a caller can report "looked in: ..." even when
43
+ * neither candidate exists (mirrors index.js's `help` tool's own "No skills found"
44
+ * message).
45
+ *
46
+ * @param {string} root
47
+ * @returns {string[]}
48
+ */
49
+ export function candidateSkillsDirs(root) {
50
+ return [
51
+ join(root, ".claude", "skills"),
52
+ join(root, ".agents", "skills"),
53
+ ];
54
+ }
55
+
56
+ /**
57
+ * Lists the immediate subdirectory names of `skillsDir` — the exact set the `help` tool
58
+ * reports. See this module's header for why this is safe to call against a twin-only
59
+ * tree with no code change: it is a verbatim directory listing, format-agnostic.
60
+ *
61
+ * @param {string} skillsDir - an existing directory (typically resolveSkillsDir()'s result)
62
+ * @returns {string[]} immediate subdirectory names, in readdirSync's natural order
63
+ */
64
+ export function listSkillFolders(skillsDir) {
65
+ const entries = readdirSync(skillsDir);
66
+ return entries.filter((entry) => {
67
+ try {
68
+ return statSync(join(skillsDir, entry)).isDirectory();
69
+ } catch {
70
+ return false;
71
+ }
72
+ });
73
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jenga-ai/agent",
3
- "version": "3.5.0",
3
+ "version": "4.0.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": {
@@ -12,6 +12,10 @@
12
12
  "engines": {
13
13
  "node": ">=14.13.1"
14
14
  },
15
+ "jenga": {
16
+ "requiresLifecycleScripts": true,
17
+ "note": "postinstall (scripts/postinstall.js) mirrors skills/agents/hooks into the consumer project and is load-bearing for the whole framework install. On package managers that block lifecycle scripts by default (pnpm v10+, npm 11.16+ via allowScripts), an explicit consumer-side opt-in is required — see docs/distribution.md."
18
+ },
15
19
  "scripts": {
16
20
  "postinstall": "node scripts/postinstall.js",
17
21
  "prepack": "npm run ui:build --prefix project/app --",
@@ -4,7 +4,7 @@
4
4
  * Resolves the invoking (consumer) project's root directory for the dashboard API, so that
5
5
  * `project/app/api/parsers/*.js` never has to compute its data root via a fixed
6
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
7
+ * for `/init` (see `skills/j-init/scripts/init.sh`'s `PKG_ROOT` idiom and
8
8
  * `lib/generate-agent-context.js`'s `realpathSync` symlink-safe comparison). A `__dirname` climb only
9
9
  * ever resolves correctly when this module runs from this monorepo's own checkout; once mirrored into
10
10
  * a real consumer's `node_modules/@jenga-ai/agent/`, climbing a fixed number of levels lands inside or
@@ -37,11 +37,56 @@ function readJsonSafe(filePath) {
37
37
  }
38
38
  }
39
39
 
40
+ // E20_S10_T01: nodes whose provenance is genuinely architectural — a human-confirmed elicitation
41
+ // (`source: 'human'`) or a mechanical AST-derived extraction (`source: 'ast'`), per
42
+ // STUB_SCHEMA.md's Provenance Field section.
43
+ const VERIFIED_SOURCES = new Set(['human', 'ast']);
44
+
45
+ function isVerifiedSource(node) {
46
+ return Boolean(node) && VERIFIED_SOURCES.has(node.source);
47
+ }
48
+
40
49
  /**
41
50
  * Read and transform `project/knowledge-graph/graph.json` into the UI-facing SAD map shape.
42
- * `status: superseded` nodes are excluded. Never throws — returns `{ nodes: [], edges: [] }` for
43
- * a missing, empty, or malformed graph.json.
44
- * @returns {{ nodes: Array<{id: string, label: string, type: string}>, edges: Array<{from: string, to: string, label: string}> }}
51
+ *
52
+ * E20_S10_T01 provenance-gating contract (the single authoritative filtering point for any
53
+ * architecture-view consumer — see the story's Acceptance Criteria): only `source: 'human'` or
54
+ * `source: 'ast'` nodes are ever rendered as full architecture nodes. `source: 'board'` nodes
55
+ * (mechanical Epic/Story restatements — see STUB_SCHEMA.md's "Where `board` Fits") are excluded
56
+ * from `nodes` here, but this is a READ-TIME filter only: `graph.json` itself is never modified by
57
+ * this reader, so `board` nodes remain available on disk for any other consumer that still wants
58
+ * board/PM traceability. Filtering lives here (rather than being duplicated in `architecture.js`,
59
+ * which just forwards this function's return value, or in `SADMap.jsx`) because this is already
60
+ * the single place that transforms the raw on-disk graph shape into the UI-facing shape — any
61
+ * future architecture-view consumer should call `readSADMap()` rather than reimplementing its own
62
+ * provenance filter.
63
+ *
64
+ * `status: superseded` nodes are also excluded, regardless of provenance (pre-existing behavior,
65
+ * unchanged by E20_S10_T01). Never throws — returns `{ nodes: [], edges: [], coverage: {shown: 0,
66
+ * hidden: 0, total: 0, needsRevalidation: 0} }` for a missing, empty, or malformed graph.json.
67
+ *
68
+ * E20_S10_T05 staleness/revalidation: a verified node MAY carry `needs_revalidation: true` (set by
69
+ * `scripts/populate-knowledge-graph.js`'s staleness check comparing a node's stored `content_hash`
70
+ * against its current source — see that script's `checkStaleness()`). This is surfaced two ways:
71
+ * per-node (`needsRevalidation` on the rendered node, for a UI badge) and aggregated into
72
+ * `coverage.needsRevalidation`, through the SAME pass as `shown`/`hidden`/`total`, per this
73
+ * function's existing never-drift contract. The flag is additive — it never removes a node from
74
+ * `nodes`, only marks it for a "needs re-verification" badge.
75
+ *
76
+ * E20_S10_T03 mixed-provenance edge handling: an unverified (`board`) node that is nonetheless an
77
+ * edge endpoint paired with a verified (`human`/`ast`) node is NOT dropped from `nodes` — it is
78
+ * still included, tagged `verified: false`, so `SADMap.jsx` can render it as a dimmed/"ghost" stub
79
+ * (label + provenance badge, no full detail) rather than silently dropping the edge or promoting
80
+ * the unverified node to full status. Every other `board` node (not an edge endpoint of a verified
81
+ * node) stays excluded per `E20_S10_T01`. Ghost-stub nodes are still counted as `hidden`, never
82
+ * `shown`, in `coverage` — they were never confirmed as architecture, only surfaced for edge
83
+ * continuity.
84
+ *
85
+ * @returns {{
86
+ * nodes: Array<{id: string, label: string, type: string, verified: boolean, source: string, needsRevalidation: boolean}>,
87
+ * edges: Array<{from: string, to: string, label: string}>,
88
+ * coverage: {shown: number, hidden: number, total: number, needsRevalidation: number}
89
+ * }}
45
90
  */
46
91
  function readSADMap() {
47
92
  try {
@@ -50,13 +95,50 @@ function readSADMap() {
50
95
  const rawNodes = graph && Array.isArray(graph.nodes) ? graph.nodes : [];
51
96
  const rawEdges = graph && Array.isArray(graph.edges) ? graph.edges : [];
52
97
 
53
- const nodes = rawNodes
54
- .filter((node) => node && node.status !== 'superseded')
55
- .map((node) => ({
98
+ const activeNodes = rawNodes.filter((node) => node && node.status !== 'superseded');
99
+ const activeNodeById = new Map(activeNodes.map((node) => [node.id, node]));
100
+
101
+ const verifiedNodes = activeNodes.filter(isVerifiedSource);
102
+ const verifiedIds = new Set(verifiedNodes.map((node) => node.id));
103
+
104
+ // E20_S10_T03: an unverified node earns a ghost-stub slot only when it is a genuine edge
105
+ // endpoint whose *other* endpoint is verified — never merely for being present in the graph.
106
+ // This keeps the bulk of unverified nodes hidden while preserving the traceability link a full
107
+ // drop would sever.
108
+ const ghostIds = new Set();
109
+ for (const edge of rawEdges) {
110
+ if (!edge) continue;
111
+ considerGhostEndpoint(edge.to, edge.from);
112
+ considerGhostEndpoint(edge.from, edge.to);
113
+ }
114
+ function considerGhostEndpoint(candidateId, otherId) {
115
+ if (!verifiedIds.has(otherId)) return; // other endpoint must be verified
116
+ if (verifiedIds.has(candidateId)) return; // candidate already fully rendered
117
+ const candidate = activeNodeById.get(candidateId);
118
+ if (candidate && !isVerifiedSource(candidate)) ghostIds.add(candidateId);
119
+ }
120
+
121
+ const nodes = [
122
+ ...verifiedNodes.map((node) => ({
56
123
  id: node.id,
57
124
  label: node.label,
58
125
  type: node.type,
59
- }));
126
+ verified: true,
127
+ source: node.source,
128
+ needsRevalidation: node.needs_revalidation === true,
129
+ })),
130
+ ...[...ghostIds].map((id) => {
131
+ const node = activeNodeById.get(id);
132
+ return {
133
+ id: node.id,
134
+ label: node.label,
135
+ type: node.type,
136
+ verified: false,
137
+ source: node.source,
138
+ needsRevalidation: false, // staleness only applies to verified (non-board) nodes
139
+ };
140
+ }),
141
+ ];
60
142
 
61
143
  const edges = rawEdges.map((edge) => ({
62
144
  from: edge.from,
@@ -64,9 +146,18 @@ function readSADMap() {
64
146
  label: edge.description || edge.type,
65
147
  }));
66
148
 
67
- return { nodes, edges };
149
+ // E20_S10_T02: coverage counts are derived from this same pass (activeNodes / verifiedNodes),
150
+ // never a separately cached computation, so they can never drift from what's actually
151
+ // rendered. `shown` counts only fully-verified nodes — ghost stubs never count as shown.
152
+ const shown = verifiedNodes.length;
153
+ const total = activeNodes.length;
154
+ const hidden = total - shown;
155
+ // E20_S10_T05: counted from the same verifiedNodes pass — never separately cached.
156
+ const needsRevalidation = verifiedNodes.filter((node) => node.needs_revalidation === true).length;
157
+
158
+ return { nodes, edges, coverage: { shown, hidden, total, needsRevalidation } };
68
159
  } catch {
69
- return { nodes: [], edges: [] };
160
+ return { nodes: [], edges: [], coverage: { shown: 0, hidden: 0, total: 0, needsRevalidation: 0 } };
70
161
  }
71
162
  }
72
163
 
@@ -3,14 +3,49 @@
3
3
  * GET /health — server liveness check
4
4
  */
5
5
 
6
+ const fs = require('fs');
7
+ const path = require('path');
6
8
  const { Router } = require('express');
7
9
  const { successResponse } = require('../response');
8
10
  const { API_VERSION } = require('../response');
11
+ const { resolveProjectRoot } = require('../lib/resolve-project-root');
9
12
 
10
13
  const router = Router();
11
14
 
12
15
  const startTime = Date.now();
13
16
 
17
+ /**
18
+ * Resolve the consuming project's name from its own root `package.json`, via the shared
19
+ * `resolveProjectRoot()` (see E47_S02) — never this monorepo's own `package.json` when running
20
+ * against a consumer install.
21
+ *
22
+ * Resolved once at module load, mirroring `startTime` above: the consuming project's own
23
+ * `package.json` does not change for the lifetime of a running server process.
24
+ *
25
+ * Graceful by design (E47_S06_T01): a missing project root, missing/unreadable `package.json`, a
26
+ * parse failure, or a missing/non-string `name` field all resolve to `null` rather than throwing or
27
+ * propagating "undefined" — the health check's liveness contract must never fail because project
28
+ * metadata could not be resolved, and the UI is expected to fall back to a sensible default title
29
+ * when this is `null`.
30
+ *
31
+ * @returns {string|null}
32
+ */
33
+ function resolveProjectName() {
34
+ try {
35
+ const root = resolveProjectRoot();
36
+ const pkgRaw = fs.readFileSync(path.join(root, 'package.json'), 'utf8');
37
+ const pkg = JSON.parse(pkgRaw);
38
+ if (typeof pkg.name === 'string' && pkg.name.trim().length > 0) {
39
+ return pkg.name.trim();
40
+ }
41
+ return null;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ const projectName = resolveProjectName();
48
+
14
49
  router.get('/', (req, res) => {
15
50
  const uptime = (Date.now() - startTime) / 1000;
16
51
  res.json(
@@ -18,6 +53,7 @@ router.get('/', (req, res) => {
18
53
  status: 'ok',
19
54
  version: API_VERSION,
20
55
  uptime,
56
+ projectName,
21
57
  })
22
58
  );
23
59
  });
@@ -4,10 +4,11 @@
4
4
  *
5
5
  * E47_S04_T02 — capture step for `j.dashboard --snapshot`.
6
6
  *
7
- * Calls the dashboard API's `/v1/board`, `/v1/history`, and `/v1/architecture` routes exactly once
8
- * each and writes their combined JSON to a single artifact, consumable by the bundling/inlining
9
- * step in `E47_S04_T03` (not yet implemented — this script's job ends at "artifact written to
10
- * disk").
7
+ * Calls the dashboard API's `/v1/board`, `/v1/history`, `/v1/architecture`, and `/v1/health` routes
8
+ * exactly once each and writes their combined JSON to a single artifact, consumable by the
9
+ * bundling/inlining step in `E47_S04_T03` (implemented) and, for `/v1/health` specifically, by
10
+ * `E47_S06_T02`'s snapshot `<title>` rewrite in `../../ui/scripts/build-snapshot-html.cjs`, which
11
+ * needs the captured project name to bake into the exported HTML's title at build time.
11
12
  *
12
13
  * ── Why an ad-hoc, in-process server instead of hitting an already-running one ──────────────────
13
14
  * This script always spins up its own short-lived, ephemeral (`--port 0` by default) server
@@ -45,7 +46,8 @@
45
46
  * "routes": {
46
47
  * "board": <full API envelope from GET /v1/board, i.e. { data, meta, error }>,
47
48
  * "history": <full API envelope from GET /v1/history, i.e. { data, meta, error }>,
48
- * "architecture": <full API envelope from GET /v1/architecture, i.e. { data, meta, error }>
49
+ * "architecture": <full API envelope from GET /v1/architecture, i.e. { data, meta, error }>,
50
+ * "health": <full API envelope from GET /v1/health, i.e. { data, meta, error }>
49
51
  * }
50
52
  * }
51
53
  * Each `routes.<name>` value is the *full* envelope exactly as the route returned it (see
@@ -90,11 +92,12 @@ const path = require('path');
90
92
  const http = require('http');
91
93
  const fs = require('fs');
92
94
 
93
- const ROUTES = ['/v1/board', '/v1/history', '/v1/architecture'];
95
+ const ROUTES = ['/v1/board', '/v1/history', '/v1/architecture', '/v1/health'];
94
96
  const ROUTE_KEYS = {
95
97
  '/v1/board': 'board',
96
98
  '/v1/history': 'history',
97
99
  '/v1/architecture': 'architecture',
100
+ '/v1/health': 'health',
98
101
  };
99
102
 
100
103
  function printUsage() {
@@ -17,5 +17,9 @@
17
17
  "ui:build": "npm run build --workspace=ui",
18
18
  "dashboard:start": "node ui/scripts/dashboard-start.cjs",
19
19
  "dashboard:open": "node ui/scripts/dashboard-open.cjs"
20
+ },
21
+ "allowScripts": {
22
+ "esbuild@0.21.5": true,
23
+ "fsevents@2.3.3": true
20
24
  }
21
25
  }
@@ -0,0 +1 @@
1
+ .loading-spinner{padding:2rem;text-align:center;color:#888;font-size:1rem}.error-message{padding:1rem;background:#fef2f2;border:1px solid #fca5a5;border-radius:4px;color:#b91c1c}.history-tab h2{margin-bottom:1rem}.history-empty{color:#888;padding:2rem;text-align:center}.history-list{list-style:none;padding:0;margin:0;border:1px solid #e5e7eb;border-radius:6px;overflow:hidden}.history-entry{display:flex;align-items:center;gap:.75rem;padding:.65rem 1rem;cursor:pointer;border-bottom:1px solid #f3f4f6;transition:background .12s}.history-entry:last-child{border-bottom:none}.history-entry:hover,.history-entry:focus{background:#f0f7ff;outline:none}.history-entry.selected{background:#dbeafe}.history-badge{flex-shrink:0;font-size:.75rem;font-weight:600;padding:.2rem .55rem;border-radius:999px;white-space:nowrap}.badge-commit{background:#dbeafe;color:#1d4ed8}.badge-rapport{background:#dcfce7;color:#15803d}.history-subject{flex:1;font-size:.9rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#1f2937}.history-date{flex-shrink:0;font-size:.78rem;color:#9ca3af}.panel-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000040;z-index:100}.detail-panel{position:fixed;top:0;right:0;bottom:0;width:min(480px,100vw);background:#fff;box-shadow:-4px 0 24px #00000026;z-index:101;overflow-y:auto;padding:1.5rem;animation:slideIn .2s ease-out}@keyframes slideIn{0%{transform:translate(100%)}to{transform:translate(0)}}.panel-close{float:right;background:none;border:none;font-size:1.25rem;cursor:pointer;color:#6b7280;padding:.25rem;line-height:1}.panel-close:hover{color:#111827}.panel-title{margin:0 0 1rem;font-size:1.1rem;color:#1f2937}.panel-meta{display:grid;grid-template-columns:auto 1fr;gap:.3rem 1rem;font-size:.85rem;margin-bottom:1rem}.panel-meta dt{font-weight:600;color:#6b7280;white-space:nowrap}.panel-meta dd{margin:0;color:#1f2937;word-break:break-all}.panel-subject{font-size:.95rem;color:#1f2937}.panel-body{font-size:.82rem;white-space:pre-wrap;background:#f9fafb;border:1px solid #e5e7eb;border-radius:4px;padding:.75rem;max-height:40vh;overflow-y:auto}.panel-empty{color:#9ca3af;font-style:italic}.markdown-body{font-size:.9rem;line-height:1.6;color:#1f2937}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4{margin:1em 0 .4em;line-height:1.3}.markdown-body p{margin:.5em 0}.markdown-body code{background:#f3f4f6;padding:.1em .35em;border-radius:3px;font-size:.85em}.markdown-body pre{background:#f3f4f6;padding:.75rem;border-radius:4px;overflow-x:auto;font-size:.82em}.markdown-body pre code{background:none;padding:0}.markdown-body blockquote{border-left:3px solid #d1d5db;margin:.75em 0;padding:.25em 1em;color:#6b7280}.markdown-body ul,.markdown-body ol{padding-left:1.5em;margin:.5em 0}@media (max-width: 480px){.history-entry{flex-wrap:wrap;row-gap:var(--space-xs)}.history-subject{flex-basis:100%;order:3;white-space:normal}.detail-panel{box-sizing:border-box;padding:var(--space-md)}.panel-close{padding:var(--space-sm);font-size:1.5rem;display:inline-flex;align-items:center;justify-content:center;min-width:44px;min-height:44px}.panel-meta{grid-template-columns:1fr;gap:.15rem 0}.panel-meta dt{margin-top:var(--space-xs)}.markdown-body code{overflow-wrap:break-word;word-break:break-word}.markdown-body table{display:block;max-width:100%;overflow-x:auto}}.board-view{display:flex;flex-direction:column;gap:1.5rem}.epic-card{background:#fff;border:1px solid #e5e7eb;border-radius:6px;padding:1rem}.epic-header{display:flex;align-items:center;gap:.75rem;margin-bottom:.75rem}.epic-title{font-size:1.1rem;font-weight:600;margin:0;cursor:pointer}.epic-id{color:#6b7280;font-size:.85rem}.stories-list{display:flex;flex-direction:column;gap:.75rem;padding-left:1rem}.story-card{background:#f9fafb;border:1px solid #e5e7eb;border-radius:4px;padding:.75rem}.story-header{display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem}.story-title{font-size:1rem;font-weight:500;margin:0;cursor:pointer}.story-id{color:#6b7280;font-size:.8rem}.tasks-list{display:flex;flex-direction:column;gap:.4rem;padding-left:1rem}.task-card{background:#fff;border:1px solid #e5e7eb;border-radius:3px;padding:.5rem .75rem;display:flex;align-items:center;gap:.5rem}.task-title{font-size:.9rem;flex:1;cursor:pointer}.task-id{color:#9ca3af;font-size:.75rem}.task-assigned{color:#6b7280;font-size:.8rem}.status-badge{padding:.2rem .5rem;border-radius:9999px;font-size:.75rem;font-weight:500}.badge-pending{background:#f3f4f6;color:#374151}.badge-running{background:#dbeafe;color:#1d4ed8}.badge-in-progress{background:#ffedd5;color:#c2410c}.badge-passed{background:#dcfce7;color:#15803d}.badge-passed-remarks{background:#fff;color:#15803d;border:1px solid #dcfce7}.badge-done{background:#dbeafe;color:#1d4ed8}.badge-failed{background:#fee2e2;color:#b91c1c}.empty-state{padding:2rem;text-align:center;color:#6b7280}.status-filter{display:flex;flex-wrap:wrap;gap:.75rem;padding:.75rem 1rem;background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px}.status-filter-option{display:flex;align-items:center;gap:.35rem;font-size:.85rem;color:#374151;cursor:pointer}.epic-range-filter{display:flex;flex-wrap:wrap;gap:.75rem;padding:.75rem 1rem;margin-bottom:.5rem;background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px}.epic-range-option{display:flex;align-items:center;gap:.35rem;font-size:.85rem;color:#374151;cursor:pointer}.epic-range-option select{font-size:.85rem;color:#374151;padding:.15rem .35rem;border:1px solid #d1d5db;border-radius:4px;background:#fff;cursor:pointer}@media (max-width: 480px){.epic-header,.story-header{flex-wrap:wrap;row-gap:var(--space-xs)}.epic-title,.story-title{flex-basis:100%}.task-card{flex-wrap:wrap;row-gap:var(--space-xs)}.task-card .task-title{flex-basis:100%}.stories-list,.tasks-list{padding-left:var(--space-sm)}.epic-range-filter{flex-direction:column;align-items:stretch}.epic-range-option{justify-content:space-between}}.kanban-board{display:flex;align-items:flex-start;gap:.75rem;overflow-x:auto;padding-bottom:.5rem}.kanban-column{flex:0 0 220px;width:220px;background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px;padding:.6rem;display:flex;flex-direction:column;gap:.5rem}.kanban-column-empty{flex:0 0 140px;width:140px;opacity:.6}.kanban-column-header{display:flex;align-items:center;justify-content:space-between;gap:.4rem}.kanban-column-title{font-size:.8rem;font-weight:600;color:#374151;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.kanban-column-count{font-size:.7rem;color:#6b7280;background:#e5e7eb;border-radius:9999px;padding:.05rem .4rem}.kanban-column-empty-state{font-size:.75rem;color:#9ca3af;text-align:center;padding:.5rem 0}.kanban-column-items{display:flex;flex-direction:column;gap:.5rem}.kanban-card{background:#fff;border:1px solid #e5e7eb;border-radius:4px;padding:.5rem .6rem;display:flex;flex-direction:column;gap:.3rem}.kanban-card-header{display:flex;align-items:center;justify-content:space-between;gap:.4rem}.kanban-card-type{font-size:.65rem;text-transform:uppercase;letter-spacing:.03em;color:#9ca3af}.kanban-card-id{font-size:.7rem;color:#9ca3af}.kanban-card-title{font-size:.85rem;color:#111827;cursor:pointer}.kanban-card-queued{border-style:dashed;border-color:#c4b5fd;background:#fdfcff}.kanban-card-footer{display:flex;align-items:center;justify-content:space-between;gap:.4rem}.kanban-card-queued-marker{font-size:.65rem;text-transform:uppercase;letter-spacing:.03em;color:#6d28d9;background:#ede9fe;border:1px solid #ddd6fe;border-radius:9999px;padding:.05rem .4rem;white-space:nowrap;cursor:help}@media (max-width: 480px){.kanban-board{position:relative;gap:var(--space-sm);scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch}.kanban-column{flex:0 0 85%;width:85%;scroll-snap-align:start;scroll-snap-stop:always}.kanban-column-empty{flex:0 0 140px;width:140px}.kanban-board:after{content:"";position:sticky;top:0;right:0;flex:0 0 var(--space-xl);align-self:stretch;background:linear-gradient(to right,transparent,rgba(249,250,251,.95));pointer-events:none}}.architecture-tab{overflow-x:hidden}.architecture-tab h2{margin-bottom:1rem}.arch-empty{color:#9ca3af;font-style:italic}.arch-section{margin-bottom:2rem}.arch-section h3{font-size:1rem;font-weight:600;color:#374151;margin-bottom:.75rem;border-bottom:1px solid #e5e7eb;padding-bottom:.35rem}.tech-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:.75rem}.tech-card{background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:.85rem 1rem;display:flex;flex-direction:column;gap:.3rem;box-shadow:0 1px 3px #0000000d}.tech-name{font-weight:600;font-size:.9rem;color:#1f2937}.tech-description{font-size:.78rem;color:#6b7280}.dep-table{width:100%;border-collapse:collapse;font-size:.875rem}.dep-table th{text-align:left;padding:.5rem .75rem;background:#f9fafb;border:1px solid #e5e7eb;font-weight:600;color:#374151}.dep-table td{padding:.45rem .75rem;border:1px solid #f3f4f6;color:#1f2937}.dep-table tr:hover td{background:#f9fafb}.dep-name{font-family:monospace}.dep-version{color:#6b7280;font-family:monospace}.dep-badge{display:inline-block;font-size:.7rem;font-weight:600;padding:.18rem .55rem;border-radius:999px;white-space:nowrap}.dep-runtime{background:#dbeafe;color:#1d4ed8}.dep-dev{background:#f3f4f6;color:#4b5563}.dep-table-wrap{overflow-x:auto}.sad-map-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:.4rem;margin-bottom:.6rem}.sad-type-chip{--chip-color: #94a3b8;display:inline-flex;align-items:center;gap:.35rem;font-size:.75rem;font-weight:600;color:#374151;background:#fff;border:1px solid #e5e7eb;border-radius:999px;padding:.25rem .65rem;cursor:pointer;line-height:1.2}.sad-type-chip.is-collapsed{background:#f3f4f6;border-color:var(--chip-color)}.sad-type-chip-dot{width:8px;height:8px;border-radius:50%;background:var(--chip-color);flex-shrink:0}.sad-reset-view-btn{font-size:.75rem;font-weight:600;color:#374151;background:#fff;border:1px solid #e5e7eb;border-radius:6px;padding:.25rem .65rem;cursor:pointer;margin-left:auto}.sad-map-scroll{width:100%;min-height:320px;overflow:hidden;-webkit-overflow-scrolling:touch;border:1px solid var(--border, #e2e8f0);border-radius:8px;background:var(--bg-secondary, #f8fafc)}.sad-map-svg{display:block;width:100%;cursor:grab}.sad-map-svg:active{cursor:grabbing}.sad-empty{color:var(--text-muted, #94a3b8);font-style:italic}.sad-coverage-indicator{font-size:.78rem;color:#6b7280;margin:0 0 .5rem}.sad-coverage-shown{font-weight:600;color:#374151}.sad-coverage-hidden{color:#9ca3af}.sad-coverage-stale{color:#b45309;font-weight:600}.sad-empty-state{display:flex;flex-direction:column;gap:.35rem}.sad-empty-detail{font-size:.8rem;color:#9ca3af;margin:0}.sad-ghost-legend{display:flex;align-items:center;gap:.4rem;font-size:.75rem;color:#9ca3af;margin:0 0 .6rem}.sad-ghost-legend-swatch{display:inline-block;width:12px;height:12px;flex-shrink:0;border-radius:3px;background:#94a3b8;opacity:.4;border:1.5px dashed #1f2937}@media (max-width: 480px){.architecture-tab{padding-bottom:var(--space-sm)}.arch-section{margin-bottom:var(--space-lg)}.arch-section h3{padding-bottom:var(--space-xs)}.tech-grid{gap:var(--space-sm)}.tech-card{padding:var(--space-sm) var(--space-md)}.sad-map-scroll{min-height:180px}}.entry-list-empty{color:#888;padding:2rem;text-align:center}.entry-list{list-style:none;padding:0;margin:0;border:1px solid #e5e7eb;border-radius:6px;overflow:hidden}.entry-list-item{display:flex;align-items:center;gap:.75rem;padding:.65rem 1rem;cursor:pointer;border-bottom:1px solid #f3f4f6;transition:background .12s}.entry-list-item:last-child{border-bottom:none}.entry-list-item:hover,.entry-list-item:focus{background:#f0f7ff;outline:none}.entry-list-item.selected{background:#dbeafe}.entry-badge{flex-shrink:0;font-size:.75rem;font-weight:600;padding:.2rem .55rem;border-radius:999px;white-space:nowrap;text-transform:capitalize;background:#ede9fe;color:#6d28d9}.entry-title{flex:1;font-size:.9rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#1f2937}.entry-date{flex-shrink:0;font-size:.78rem;color:#9ca3af}@media (max-width: 480px){.entry-list-item{flex-wrap:wrap;row-gap:var(--space-xs)}.entry-title{flex-basis:100%;order:3;white-space:normal}}.pagination-controls{display:flex;align-items:center;justify-content:center;gap:var(--space-md);margin-top:var(--space-md)}.pagination-btn{padding:.4rem .9rem;font-size:.85rem;border:1px solid #e5e7eb;border-radius:6px;background:#fff;color:#1f2937;cursor:pointer}.pagination-btn:hover:not(:disabled){background:#f0f7ff;border-color:#93c5fd}.pagination-btn:disabled{opacity:.45;cursor:not-allowed}.pagination-status{font-size:.82rem;color:#6b7280;white-space:nowrap}@media (max-width: 480px){.pagination-controls{gap:var(--space-sm)}.pagination-btn{padding:.5rem .85rem;font-size:.9rem}.pagination-status{font-size:.78rem}}.rapports-search-input{display:block;width:100%;box-sizing:border-box;padding:.55rem .75rem;margin-bottom:var(--space-md);font-size:.9rem;border:1px solid #e5e7eb;border-radius:6px;color:#1f2937}.rapports-search-input:focus{outline:none;border-color:#93c5fd;box-shadow:0 0 0 2px #3b82f640}@media (max-width: 480px){.rapports-search-input{font-size:1rem;padding:.65rem .75rem}}:root{--space-xs: .25rem;--space-sm: .5rem;--space-md: 1rem;--space-lg: 1.5rem;--space-xl: 2rem;--breakpoint-mobile: 480px;--breakpoint-tablet: 768px}body{font-family:sans-serif;margin:0;background:#f5f5f5}.app{max-width:1200px;margin:0 auto;padding:1rem}.tabs{display:flex;gap:0;border-bottom:1px solid #ddd}.tab-btn{padding:.6rem 1.2rem;border:none;background:transparent;cursor:pointer;font-size:1rem;color:#555;border-bottom:2px solid transparent}.tab-btn.active{color:#2563eb;border-bottom-color:#2563eb;font-weight:600}.tab-content{padding:1rem 0}@media (max-width: 480px){.app{width:100%;box-sizing:border-box;padding:var(--space-sm)}.tabs{flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch}.tab-btn{flex:0 0 auto;padding:var(--space-sm) var(--space-md)}}