Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@
All notable changes to RStack are documented here. Entries are user-focused:
what you can now do that you couldn't before.

## [1.8.0] - 2026-06-02

### Added
- **See what your agents actually produced — as infographics.** A new **Run
Report** page turns every run's 15 stage deliverables into eye-catching
cards: a security threat-severity donut with the release gate, a compliance
score gauge, test pass/fail bars, a cost flashcard, requirement and
architecture counts, planning milestones, and the open-risk / release-gate
summary. Numbers count up, charts fill in, blocked gates pulse.
- **The Studio 3D agents now report their real work.** Click any agent in the
3D studio and its panel shows that stage's infographic — threat counts,
compliance score, test results, cost — pulled live from the run.
- New `GET /api/run-report` endpoint serves a run's parsed stage reports in one
sandboxed call.

## [1.7.1] - 2026-06-02

### Security
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rstack-agents",
"version": "1.7.1",
"version": "1.8.0",
"description": "Production-ready agentic SDLC framework for Pi and coding agents — orchestrator, builder/validator teams, lifecycle state, and specialist reuse",
"type": "module",
"main": "src/index.js",
Expand Down
40 changes: 34 additions & 6 deletions src/observability/dashboard/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { dashboardHtml } from './ui.js';
import { studio3dHtml } from './ui/studio3d.js';
import { buildFullState, resolveDashboardApproval, toClientState } from './state/index.js';
import { sourceRoots } from './state/roots.js';
import { collectStageReports } from './state/stage-reports.js';

// owner: RStack developed by Richardson Gunde

Expand Down Expand Up @@ -184,6 +185,33 @@ async function handleApproval(req, res, decision) {
const ARTIFACT_MAX_BYTES = 512 * 1024;
const ARTIFACT_EXTENSIONS = new Set(['.md', '.json', '.jsonl', '.txt', '.yml', '.yaml']);

// Locate a run directory by id across the known project roots, rejecting any
// id that could traverse the filesystem. Returns null if not found / unsafe.
async function resolveRunDir(runId) {
if (!runId || runId.includes('/') || runId.includes('..') || runId.includes('\\')) return null;
const roots = await sourceRoots(PROJECT_ROOT, {});
return roots
.map((root) => join(root, '.rstack', 'runs', runId))
.find((dir) => existsSync(dir)) ?? null;
}

async function handleRunReport(url, res) {
const sendJson = (status, body) => {
res.writeHead(status, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(body));
};
try {
const runId = url.searchParams.get('run') ?? '';
if (!runId) return sendJson(400, { error: 'run is required' });
const runDir = await resolveRunDir(runId);
if (!runDir) return sendJson(404, { error: 'run not found' });
const { stages, deliverables } = await collectStageReports(runDir);
sendJson(200, { run: runId, stages, deliverables });
} catch (err) {
sendJson(500, { error: String(err?.message) });
}
}

async function handleArtifact(url, res) {
const sendJson = (status, body) => {
res.writeHead(status, { 'Content-Type': 'application/json' });
Expand All @@ -193,12 +221,7 @@ async function handleArtifact(url, res) {
const runId = url.searchParams.get('run') ?? '';
const relPath = url.searchParams.get('path') ?? '';
if (!runId || !relPath) return sendJson(400, { error: 'run and path are required' });
if (runId.includes('/') || runId.includes('..')) return sendJson(400, { error: 'invalid run id' });

const roots = await sourceRoots(PROJECT_ROOT, {});
const runDir = roots
.map((root) => join(root, '.rstack', 'runs', runId))
.find((dir) => existsSync(dir));
const runDir = await resolveRunDir(runId);
if (!runDir) return sendJson(404, { error: 'run not found' });

const target = resolve(runDir, relPath);
Expand Down Expand Up @@ -273,6 +296,11 @@ const server = createServer(async (req, res) => {
return;
}

if (url.pathname === '/api/run-report' && req.method === 'GET') {
await handleRunReport(url, res);
return;
}

res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(dashboardHtml(PORT));
});
Expand Down
1 change: 1 addition & 0 deletions src/observability/dashboard/state/client-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export function toClientState(state) {
ts: entry.ts, task_id: entry.task_id, kind: entry.kind, status: entry.status, evidence: entry.evidence,
})),
artifactIndex: (run.artifactIndex ?? []).slice(0, 80),
stageReports: run.stageReports ?? [],
timeline: (run.timeline ?? []).slice(0, 120),
totals: run.totals ?? null,
stageElapsed: run.stageElapsed ?? {},
Expand Down
2 changes: 2 additions & 0 deletions src/observability/dashboard/state/runs.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { readFile, readdir, stat } from 'node:fs/promises';
import { join } from 'node:path';
import { CANONICAL_SDLC_STAGES } from '../../../core/harness/stages.js';
import { deriveRunTimeline, deriveRunTotals, deriveStageElapsed } from '../../metrics/derive.js';
import { stageReportIndex } from './stage-reports.js';
import { readJson, readJsonlSync } from './files.js';

// owner: RStack developed by Richardson Gunde
Expand Down Expand Up @@ -167,6 +168,7 @@ export async function getRunsForRoot(projectRoot) {
evidence,
approvals: Array.isArray(runApprovals) ? runApprovals : [],
artifactIndex: await indexArtifacts(runDir),
stageReports: await stageReportIndex(runDir),
activityTimeline: buildActivityTimeline(events),
timeline: deriveRunTimeline(events, rawTasks),
totals: deriveRunTotals(events),
Expand Down
74 changes: 74 additions & 0 deletions src/observability/dashboard/state/stage-reports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* Stage-report collector — reads the structured deliverables a run produced
* (the 15 canonical stage artifacts + key top-level reports) and returns them
* parsed, so the UI can render them as infographics.
*
* Everything here is read-only and size-capped. Path safety is the caller's
* responsibility (server validates the runId + containment before calling).
*
* owner: RStack developed by Richardson Gunde
*/

import { readFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { join } from 'node:path';

const MAX_REPORT_BYTES = 256 * 1024;

// stage id → the artifact filename it writes under artifacts/stages/<id>/.
export const STAGE_ARTIFACTS = Object.freeze({
'00-environment': 'environment_report.json',
'01-transcript': 'transcript.json',
'02-requirements': 'requirements.json',
'03-documentation': 'documentation.json',
'04-planning': 'plan.json',
'05-jira': 'jira_tickets.json',
'06-architecture': 'system_design.json',
'07-code': 'code_report.json',
'08-testing': 'test_report.json',
'09-deployment': 'deployment_report.json',
'10-summary': 'summary.json',
'11-feedback-loop': 'feedback.json',
'12-security-threat-model': 'threat_model.json',
'13-compliance-checker': 'compliance_report.json',
'14-cost-estimation': 'cost_estimate.json',
});

// Top-level cross-stage deliverables worth surfacing on their own.
const DELIVERABLES = Object.freeze({
'release-readiness': 'artifacts/release-readiness.json',
summary: 'artifacts/stages/10-summary/summary.json',
});

async function readCappedJson(path) {
if (!existsSync(path)) return null;
try {
const raw = await readFile(path, 'utf8');
if (raw.length > MAX_REPORT_BYTES) return { _truncated: true, _bytes: raw.length };
return JSON.parse(raw);
} catch {
return null;
}
}

/**
* Collect parsed stage reports for one run.
* Returns { stages: { [stageId]: data|null }, deliverables: { [key]: data|null } }.
*/
export async function collectStageReports(runDir) {
const stages = {};
await Promise.all(Object.entries(STAGE_ARTIFACTS).map(async ([stageId, file]) => {
stages[stageId] = await readCappedJson(join(runDir, 'artifacts', 'stages', stageId, file));
}));
const deliverables = {};
await Promise.all(Object.entries(DELIVERABLES).map(async ([key, rel]) => {
deliverables[key] = await readCappedJson(join(runDir, rel));
}));
return { stages, deliverables };
}

/** Which stage ids actually produced a report (for snapshot indexing). */
export async function stageReportIndex(runDir) {
return Object.keys(STAGE_ARTIFACTS).filter((stageId) =>
existsSync(join(runDir, 'artifacts', 'stages', stageId, STAGE_ARTIFACTS[stageId])));
}
Loading
Loading