@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.
- package/README.md +85 -78
- package/agents/developer.md +1 -1
- package/agents/scrum-master.md +20 -2
- package/agents/tester.md +3 -3
- package/hooks/on_session_end.sh +5 -5
- package/lib/generate-agent-context.js +2 -2
- package/lib/generate-copilot-hooks.js +1 -1
- package/lib/generate-skill-allow-list.js +79 -8
- package/lib/mirror.js +1 -1
- package/lib/postinstall-manifest.js +1 -1
- package/lib/skill-allow-list.json +2 -2
- package/mcp/help/index.js +8 -17
- package/mcp/help/scan.js +73 -0
- package/package.json +5 -1
- package/project/app/api/lib/resolve-project-root.js +1 -1
- package/project/app/api/parsers/knowledge-graph.js +100 -9
- package/project/app/api/routes/health.js +36 -0
- package/project/app/api/scripts/capture-snapshot.js +9 -6
- package/project/app/package.json +4 -0
- package/project/app/ui/dist/assets/index-BADc5mmH.css +1 -0
- package/project/app/ui/dist/assets/index-C3oiuli_.js +104 -0
- package/project/app/ui/dist/index.html +2 -2
- package/project/app/ui/package.json +4 -0
- package/project/app/ui/scripts/build-snapshot-html.cjs +63 -2
- package/scripts/acquire-concurrency-slot.sh +35 -5
- package/scripts/apply-j-prefix.sh +46 -5
- package/scripts/build-pages-site.sh +1 -1
- package/scripts/check-public-playbook-steps.sh +158 -52
- package/scripts/check-publicignore-match.sh +2 -2
- package/scripts/compute-deploy-reconcile.sh +5 -5
- package/scripts/delete-bare-skill-dirs.sh +329 -0
- package/scripts/generate-legacy-shipped-paths.js +2 -2
- package/scripts/idea_manager.sh +273 -3
- package/scripts/mark-deployed.sh +2 -2
- package/scripts/populate-knowledge-graph.entity-resolution.test.js +254 -0
- package/scripts/populate-knowledge-graph.js +213 -5
- package/scripts/populate-knowledge-graph.staleness.test.js +130 -0
- package/scripts/postinstall.js +1 -1
- package/scripts/release-concurrency-slot.sh +34 -4
- package/scripts/render-ranked-list.sh +270 -0
- package/scripts/repoint-dead-bare-path-prose.py +81 -0
- package/scripts/repoint-skill-refs.sh +539 -0
- package/scripts/rewrite-stale-skill-preambles.py +188 -0
- package/scripts/strip-polyfill-frontmatter.py +166 -0
- package/scripts/todo_manager.sh +16 -1
- package/scripts/validate-typed-object.sh +750 -0
- package/scripts/verify-legacy-seed-reconcile.sh +10 -10
- package/scripts/verify-postinstall-reconcile.sh +7 -7
- package/scripts/write-context-digest.sh +1 -1
- package/skills/j-brainstorm/SKILL.md +3 -4
- package/skills/j-btw/SKILL.md +3 -4
- package/skills/j-clearify/SKILL.md +5 -6
- package/skills/j-close-story/SKILL.md +11 -12
- package/skills/j-close-story/scripts/check-privatized.sh +4 -4
- package/skills/j-close-story/scripts/check-story-closeable.sh +11 -4
- package/skills/j-commit/SKILL.md +3 -4
- package/skills/j-continue/SKILL.md +5 -6
- package/skills/j-deep-dive/SKILL.md +3 -4
- package/skills/j-distribute/CONFIG_SCHEMA.md +82 -5
- package/skills/j-distribute/SKILL.md +3 -4
- package/skills/j-do/SKILL.md +100 -18
- package/skills/j-doc/SKILL.md +3 -4
- package/skills/j-doc-sync/SKILL.md +4 -4
- package/skills/j-dooo/SKILL.md +6 -15
- package/skills/j-error/SKILL.md +3 -4
- package/skills/j-evaluate/SKILL.md +3 -4
- package/skills/j-examplify/SKILL.md +3 -4
- package/skills/j-gitignore/SKILL.md +157 -0
- package/skills/j-gitignore/assets/jenga-paths.txt +50 -0
- package/skills/j-gitignore/scripts/_catalog.sh +105 -0
- package/skills/j-gitignore/scripts/audit-gitignore.sh +194 -0
- package/skills/j-gitignore/scripts/repair-gitignore.sh +226 -0
- package/skills/j-gitignore/scripts/untrack-jenga-files.sh +210 -0
- package/skills/j-help/SKILL.md +3 -4
- package/skills/j-idea/SKILL.md +80 -9
- package/skills/j-idea/assets/idea_template.md +1 -1
- package/skills/j-improve/SKILL.md +4 -5
- package/skills/j-init/SKILL.md +23 -14
- package/skills/j-init/assets/scope-thresholds_template.json +5 -2
- package/skills/j-init/scripts/apply-scaffold-visibility.sh +9 -7
- package/skills/j-init/scripts/init.sh +4 -4
- package/skills/j-jbp/SKILL.md +3 -4
- package/skills/j-lgtm/SKILL.md +3 -4
- package/skills/j-pi-plan/SKILL.md +3 -4
- package/skills/j-playbook/SKILL.md +1 -1
- package/skills/j-proceed/SKILL.md +3 -4
- package/skills/j-publish/SKILL.md +4 -5
- package/skills/j-publish/adapters/npm-ci.md +6 -1
- package/skills/j-publish/adapters/npm.md +1 -1
- package/skills/j-publish/scripts/generate_release_notes.sh +1 -1
- package/skills/j-publish/scripts/npm_stage_inspect.sh +61 -0
- package/skills/j-publish/scripts/run_gates.sh +1 -1
- package/skills/j-reconcile/SKILL.md +40 -7
- package/skills/j-reconcile/assets/report_format.md +11 -0
- package/skills/j-reconcile/scripts/detect-unlinked-code.sh +13 -13
- package/skills/j-reconcile-origin/SKILL.md +3 -4
- package/skills/j-redo/SKILL.md +4 -5
- package/skills/j-skillify/SKILL.md +3 -4
- package/skills/j-spinoff/SKILL.md +4 -5
- package/skills/j-status/SKILL.md +18 -4
- package/skills/j-todo/SKILL.md +44 -5
- package/skills/j-todo/scripts/argument-is-not-ranked-list.sh +92 -0
- package/skills/j-todo/scripts/argument-is-ranked-list.sh +78 -0
- package/skills/j-uncharted/SKILL.md +252 -13
- package/skills/j-uncharted/scripts/detect-dependencies.sh +80 -22
- package/skills/j-uncharted/scripts/detect-tests.sh +1 -1
- package/skills/j-uncharted/scripts/diff-since-baseline.sh +600 -0
- package/skills/j-uncharted/scripts/elicitation-state.sh +1 -1
- package/skills/j-uncharted/scripts/find-scan-baseline.sh +545 -0
- package/skills/j-uncharted/scripts/run-engine.sh +36 -2
- package/skills/j-uncharted/scripts/validate-proposed-items.sh +1 -1
- package/skills/j-uncharted/scripts/write-scan-record.sh +361 -0
- package/skills/j-wtf/SKILL.md +4 -5
- package/skills/jenga/SKILL.md +106 -11
- package/skills/jenga/playbooks/board-hygiene.json +32 -0
- package/skills/jenga/playbooks/schema.json +73 -6
- package/skills/jenga/playbooks/understand-then-commit.json +19 -0
- package/skills/jenga/scripts/load-nl-catalog.js +5 -2
- package/skills/jenga/scripts/load-nl-catalog.sh +1 -1
- package/skills/jenga/scripts/load-playbooks.sh +290 -5
- package/skills/jenga/scripts/match-playbook.sh +4 -4
- package/skills/jenga/scripts/run-playbook-step.sh +267 -1
- package/templates/SCRUM_BOARD_SCHEMA.md +14 -1
- package/templates/SKILL_TEMPLATE.md +12 -0
- package/templates/permission-levels/level-1-locked.json +1 -1
- package/templates/permission-levels/level-2-guarded.json +1 -1
- package/templates/permission-levels/level-3-standard.json +1 -1
- package/templates/permission-levels/level-4-elevated.json +2 -2
- package/templates/permission-levels/level-5-unrestricted.json +2 -2
- package/templates/playbook-types.json +40 -6
- package/project/app/ui/dist/assets/index-7fj-vllY.js +0 -104
- package/project/app/ui/dist/assets/index-CdK3Qrep.css +0 -1
- package/scripts/audit-twin-divergence.sh +0 -625
- package/scripts/generate-j-alias.sh +0 -333
- package/skills/j-dev-done/SKILL.md +0 -53
- package/skills/j-dev-done/scripts/classify-commit-outcome.sh +0 -114
package/mcp/help/scan.js
ADDED
|
@@ -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
|
+
"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
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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/
|
|
8
|
-
* each and writes their combined JSON to a single artifact, consumable by the
|
|
9
|
-
* step in `E47_S04_T03` (
|
|
10
|
-
*
|
|
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() {
|
package/project/app/package.json
CHANGED
|
@@ -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)}}
|